diff --git a/.gitignore b/.gitignore index e16768c66..539e2d082 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ .env docs .claude -__pycache__ \ No newline at end of file +__pycache__ +.codex diff --git a/Cargo.lock b/Cargo.lock index a0dcb4fc5..acc65f5d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1076,7 +1076,7 @@ checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", - "yoke", + "yoke 0.8.1", "zerofrom", "zerovec", ] @@ -1143,7 +1143,7 @@ dependencies = [ "displaydoc", "icu_locale_core", "writeable", - "yoke", + "yoke 0.8.1", "zerofrom", "zerotrie", "zerovec", @@ -2470,6 +2470,7 @@ dependencies = [ "phf_codegen", "serde", "serde_json", + "yoke 0.7.5", ] [[package]] @@ -2993,6 +2994,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive 0.7.5", + "zerofrom", +] + [[package]] name = "yoke" version = "0.8.1" @@ -3000,10 +3013,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ "stable_deref_trait", - "yoke-derive", + "yoke-derive 0.8.1", "zerofrom", ] +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "yoke-derive" version = "0.8.1" @@ -3064,7 +3089,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", - "yoke", + "yoke 0.8.1", "zerofrom", ] @@ -3074,7 +3099,7 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ - "yoke", + "yoke 0.8.1", "zerofrom", "zerovec-derive", ] diff --git a/Cargo.toml b/Cargo.toml index 813f2ff5a..e4ab68850 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,7 @@ wacore-derive = { path = "./wacore/derive", version = "0.5.0" } wacore-libsignal = { path = "./wacore/libsignal", version = "0.5.0" } wacore-noise = { path = "./wacore/noise", version = "0.5.0" } waproto = { path = "./waproto", version = "0.5.0" } +yoke = { version = "0.7", features = ["derive"] } [features] debug-diagnostics = ["wacore/debug-diagnostics"] diff --git a/src/client.rs b/src/client.rs index dbc944a09..9d150f645 100644 --- a/src/client.rs +++ b/src/client.rs @@ -14,10 +14,10 @@ use futures::FutureExt; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; -use wacore::xml::DisplayableNode; +use wacore::xml::{DisplayableNode, DisplayableNodeRef}; +use wacore_binary::JidExt; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::JidExt; -use wacore_binary::node::{Attrs, Node, NodeValue}; +use wacore_binary::{Attrs, Node, NodeValue}; use crate::appstate_sync::AppStateProcessor; use crate::handlers::chatstate::ChatStateEvent; @@ -30,7 +30,7 @@ use log::{debug, error, info, trace, warn}; use rand::{Rng, RngExt}; use scopeguard; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; use portable_atomic::AtomicU64; use std::sync::Arc; @@ -78,16 +78,21 @@ impl NodeFilter { self.attr("from", jid.to_string()) } - fn matches(&self, node: &Node) -> bool { - node.tag == self.tag - && self - .attrs - .iter() - .all(|(k, v)| node.attrs.get(k.as_str()).is_some_and(|attr| *attr == *v)) + fn matches(&self, node: &wacore_binary::NodeRef<'_>) -> bool { + node.tag == self.tag.as_str() + && self.attrs.iter().all(|(k, v)| { + node.get_attr(k.as_str()) + .is_some_and(|attr| attr.as_str() == v.as_str()) + }) } } struct NodeWaiter { + filter: NodeFilter, + tx: futures::channel::oneshot::Sender>, +} + +struct SentNodeWaiter { filter: NodeFilter, tx: futures::channel::oneshot::Sender>, } @@ -95,8 +100,9 @@ struct NodeWaiter { fn resolve_waiters( waiters_mutex: &std::sync::Mutex>, counter: &AtomicUsize, - node: &Arc, + node: &Arc, ) { + let nr = node.get(); let mut waiters = waiters_mutex .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); @@ -105,7 +111,7 @@ fn resolve_waiters( if waiters[i].tx.is_canceled() { waiters.swap_remove(i); counter.fetch_sub(1, Ordering::Release); - } else if waiters[i].filter.matches(node) { + } else if waiters[i].filter.matches(nr) { let w = waiters.swap_remove(i); counter.fetch_sub(1, Ordering::Release); let _ = w.tx.send(Arc::clone(node)); @@ -304,8 +310,9 @@ pub struct Client { pub(crate) transport_factory: Arc, pub(crate) noise_socket: Arc>>>, - pub(crate) response_waiters: - Arc>>>, + pub(crate) response_waiters: Arc< + Mutex>>>, + >, /// Generic node waiters for waiting on specific stanzas by tag/attributes. /// Uses std::sync::Mutex (not tokio) since the critical section is trivial. @@ -313,7 +320,7 @@ pub struct Client { node_waiters: std::sync::Mutex>, node_waiter_count: AtomicUsize, /// Waiters for raw outgoing nodes before encryption. - sent_node_waiters: std::sync::Mutex>, + sent_node_waiters: std::sync::Mutex>, sent_node_waiter_count: AtomicUsize, pub(crate) unique_id: String, @@ -342,7 +349,8 @@ pub struct Client { /// Per-chat message queues for sequential message processing. /// Prevents race conditions where a later message is processed before /// the PreKey message that establishes the Signal session. - pub(crate) message_queues: Cache>>, + pub(crate) message_queues: + Cache>>, /// Cache for LID to Phone Number mappings (bidirectional). /// When we receive a message with sender_lid/sender_pn attributes, we store the mapping here. @@ -1411,7 +1419,7 @@ impl Client { // is set up before offline messages are processed // - Everything else: spawned concurrently for parallelism let process_inline = matches!( - node.tag.as_ref(), + node.tag(), "success" | "failure" | "stream:error" | "message" | "ib" ); @@ -1470,12 +1478,12 @@ impl Client { } } - /// Decrypt a frame and return the parsed node. + /// Decrypt a frame and return the parsed node as a zero-copy OwnedNodeRef. /// This must be called sequentially due to noise protocol counter requirements. pub(crate) async fn decrypt_frame( self: &Arc, encrypted_frame: &bytes::Bytes, - ) -> Option { + ) -> Option { let noise_socket = match self.get_noise_socket().await { Ok(s) => s, Err(_) => { @@ -1500,8 +1508,10 @@ impl Client { } }; - match wacore_binary::marshal::unmarshal_ref(unpacked_data_cow.as_ref()) { - Ok(node_ref) => Some(node_ref.to_owned()), + // Convert Cow to owned Vec for yoke to own the buffer + let buffer = unpacked_data_cow.into_owned(); + match wacore_binary::OwnedNodeRef::new(buffer) { + Ok(owned) => Some(owned), Err(e) => { log::warn!(target: "Client/Recv", "Failed to unmarshal node: {e}"); None @@ -1512,24 +1522,28 @@ impl Client { /// Process an already-decrypted node. /// This can be spawned concurrently since it doesn't depend on noise protocol state. /// The node is wrapped in Arc to avoid cloning when passing through handlers. - pub(crate) async fn process_decrypted_node(self: &Arc, node: wacore_binary::node::Node) { + pub(crate) async fn process_decrypted_node( + self: &Arc, + node: wacore_binary::OwnedNodeRef, + ) { // Wrap in Arc once - all handlers will share this same allocation let node_arc = Arc::new(node); self.process_node(node_arc).await; } /// Process a node wrapped in Arc. Handlers receive the Arc and can share/store it cheaply. - pub(crate) async fn process_node(self: &Arc, node: Arc) { - use wacore::xml::DisplayableNode; + pub(crate) async fn process_node(self: &Arc, node: Arc) { + use wacore::xml::DisplayableNodeRef; + let nr = node.get(); // --- Offline Sync Tracking --- - if node.tag.as_ref() == "ib" { + if nr.tag.as_ref() == "ib" { // Check for offline_preview child to get expected count - if let Some(preview) = node.get_optional_child("offline_preview") { + if let Some(preview) = nr.get_optional_child("offline_preview") { let count: usize = preview - .attrs - .get("count") - .and_then(|v| v.as_str().parse().ok()) + .get_attr("count") + .map(|v| v.as_str()) + .and_then(|s| s.parse().ok()) .unwrap_or(0); if count == 0 { @@ -1555,7 +1569,7 @@ impl Client { debug!(target: "Client/OfflineSync", "Sync STARTED: Expecting {} items.", count); } } else if self.offline_sync_metrics.active.load(Ordering::Acquire) - && node.get_optional_child("offline").is_some() + && nr.get_optional_child("offline").is_some() { // Handle end marker: signals sync completion // Only with an child is a real end marker. @@ -1578,7 +1592,7 @@ impl Client { // Track progress if active if self.offline_sync_metrics.active.load(Ordering::Acquire) { // Check for 'offline' attribute on relevant stanzas - if node.attrs.contains_key("offline") { + if nr.get_attr("offline").is_some() { let processed = self .offline_sync_metrics .processed_messages @@ -1607,15 +1621,15 @@ impl Client { } // --- End Tracking --- - if node.tag.as_ref() == "iq" - && let Some(sync_node) = node.get_optional_child("sync") + if nr.tag.as_ref() == "iq" + && let Some(sync_node) = nr.get_optional_child("sync") && let Some(collection_node) = sync_node.get_optional_child("collection") { let name = collection_node.attrs().optional_string("name"); let name = name.as_deref().unwrap_or(""); debug!(target: "Client/Recv", "Received app state sync response for '{name}' (hiding content)."); } else { - debug!(target: "Client/Recv","{}", DisplayableNode(&node)); + debug!(target: "Client/Recv","{}", DisplayableNodeRef(nr)); } // Prepare deferred ACK cancellation flag (sent after dispatch unless cancelled) @@ -1629,7 +1643,7 @@ impl Client { .dispatch(&Event::RawNode(Arc::clone(&node))); } - if node.tag.as_ref() == "xmlstreamend" { + if nr.tag.as_ref() == "xmlstreamend" { if self.expected_disconnect.load(Ordering::Relaxed) { debug!("Received , expected disconnect."); } else { @@ -1644,14 +1658,13 @@ impl Client { self.resolve_node_waiters(&node); } - if node.tag.as_ref() == "iq" - && let Some(id) = node.attrs.get("id").map(|v| v.as_str()) + if nr.tag.as_ref() == "iq" + && let Some(id) = nr.get_attr("id").map(|v| v.as_str()) { // Single lock acquisition: try to remove the waiter directly. let waiter = self.response_waiters.lock().await.remove(id.as_ref()); if let Some(waiter) = waiter { - let owned_node = Arc::try_unwrap(node).unwrap_or_else(|arc| (*arc).clone()); - if waiter.send(owned_node).is_err() { + if waiter.send(Arc::clone(&node)).is_err() { warn!(target: "Client/IQ", "Failed to send IQ response to waiter. Receiver was likely dropped."); } return; @@ -1667,38 +1680,38 @@ impl Client { { warn!( "Received unknown top-level node: {}", - DisplayableNode(&node) + DisplayableNodeRef(nr) ); } // Send the deferred ACK if applicable and not cancelled by handler - if self.should_ack(&node) && !cancelled { + if self.should_ack(nr) && !cancelled { self.maybe_deferred_ack(node).await; } } - /// Determine if a Node should be acknowledged with . - fn should_ack(&self, node: &Node) -> bool { + /// Determine if a node should be acknowledged with . + fn should_ack(&self, node: &wacore_binary::NodeRef<'_>) -> bool { matches!( node.tag.as_ref(), "message" | "receipt" | "notification" | "call" - ) && node.attrs.contains_key("id") - && node.attrs.contains_key("from") + ) && node.get_attr("id").is_some() + && node.get_attr("from").is_some() } /// Possibly send a deferred ack: either immediately or via spawned task. /// Handlers can cancel by setting `cancelled` to true. - /// Uses Arc to avoid cloning when spawning the async task. - async fn maybe_deferred_ack(self: &Arc, node: Arc) { + /// Uses Arc to avoid cloning when spawning the async task. + async fn maybe_deferred_ack(self: &Arc, node: Arc) { if self.synchronous_ack { - if let Err(e) = self.send_ack_for(&node).await { + if let Err(e) = self.send_ack_for(node.get()).await { warn!("Failed to send ack: {e:?}"); } } else { let this = self.clone(); self.runtime .spawn(Box::pin(async move { - if let Err(e) = this.send_ack_for(&node).await + if let Err(e) = this.send_ack_for(node.get()).await && !matches!(e, ClientError::NotConnected) { warn!("Failed to send ack: {e:?}"); @@ -1709,7 +1722,7 @@ impl Client { } /// Build and send an node corresponding to the given stanza. - async fn send_ack_for(&self, node: &Node) -> Result<(), ClientError> { + async fn send_ack_for(&self, node: &wacore_binary::NodeRef<'_>) -> Result<(), ClientError> { if self.expected_disconnect.load(Ordering::Relaxed) { return Ok(()); } @@ -1854,7 +1867,7 @@ impl Client { /// Get business profile for a WhatsApp Business account. pub async fn get_business_profile( &self, - jid: &wacore_binary::jid::Jid, + jid: &wacore_binary::Jid, ) -> Result, crate::request::IqError> { use wacore::iq::business::BusinessProfileSpec; self.execute(BusinessProfileSpec::new(jid)).await @@ -1864,7 +1877,7 @@ impl Client { pub async fn reject_call( &self, call_id: &str, - call_from: &wacore_binary::jid::Jid, + call_from: &wacore_binary::Jid, ) -> Result<(), anyhow::Error> { anyhow::ensure!(!call_id.is_empty(), "call_id cannot be empty"); let id = self.generate_request_id(); @@ -1891,7 +1904,7 @@ impl Client { self.execute(DigestKeyBundleSpec::new()).await.map(|_| ()) } - pub(crate) async fn handle_success(self: &Arc, node: &wacore_binary::node::Node) { + pub(crate) async fn handle_success(self: &Arc, node: &wacore_binary::NodeRef<'_>) { // Skip processing if an expected disconnect is pending (e.g., 515 received). // This prevents race conditions where a spawned success handler runs after // cleanup_connection_state has already reset is_logged_in. @@ -1920,7 +1933,7 @@ impl Client { self.update_server_time_offset(node); // Extract LID from the node before spawning (node isn't Send). - let lid_from_server = match node.attrs.get("lid") { + let lid_from_server = match node.get_attr("lid") { Some(lid_value) => match lid_value.to_jid() { Some(lid) => Some(lid), None => { @@ -2256,12 +2269,12 @@ impl Client { /// /// If an ack with an ID that matches a pending task in `response_waiters`, /// the task is resolved and the function returns `true`. Otherwise, returns `false`. - pub(crate) async fn handle_ack_response(&self, node: Node) -> bool { + pub(crate) async fn handle_ack_response(&self, node: &wacore_binary::NodeRef<'_>) -> bool { // Surface privacy-token nack codes for diagnosability - if let Some(error_code) = node.attrs.get("error") { + if let Some(error_code) = node.get_attr("error") { let code = error_code.as_str(); - let id = node.attrs.get("id").map(|v| v.as_str().into_owned()); - match &*code { + let id = node.get_attr("id").map(|v| v.as_str().into_owned()); + match code.as_ref() { "463" => { warn!( target: "Client/Ack", @@ -2283,19 +2296,30 @@ impl Client { } } - let id_opt = node.attrs.get("id").map(|v| v.as_str().into_owned()); + let id_opt = node.get_attr("id").map(|v| v.as_str().into_owned()); if let Some(id) = id_opt && let Some(waiter) = self.response_waiters.lock().await.remove(&id) { - if waiter.send(node).is_err() { - warn!(target: "Client/Ack", "Failed to send ACK response to waiter for ID {id}. Receiver was likely dropped."); + // ACK responses are infrequent; re-encode into OwnedNodeRef for the channel. + // marshal_ref prepends a leading 0x00 format byte; OwnedNodeRef::new expects raw + // protocol bytes without it, matching what unpack() produces from the network. + match wacore_binary::marshal::marshal_ref(node) + .and_then(|bytes| wacore_binary::OwnedNodeRef::new(bytes[1..].to_vec())) + { + Ok(onr) => { + if waiter.send(Arc::new(onr)).is_err() { + warn!(target: "Client/Ack", "Failed to send ACK response to waiter for ID {id}. Receiver was likely dropped."); + } + } + Err(e) => { + warn!(target: "Client/Ack", "Failed to re-encode ACK node for waiter: {e}"); + } } return true; } false } - #[allow(dead_code)] // Used by per-collection callers (e.g., critical sync gating) pub(crate) async fn fetch_app_state_with_retry(&self, name: WAPatchName) -> anyhow::Result<()> { // In-flight dedup: skip if this collection is already being synced. // Matches WA Web's WAWebSyncdCollectionsStateMachine which tracks in-flight syncs @@ -2316,7 +2340,6 @@ impl Client { result } - #[allow(dead_code)] async fn fetch_app_state_with_retry_inner(&self, name: WAPatchName) -> anyhow::Result<()> { let mut attempt = 0u32; loop { @@ -2456,7 +2479,7 @@ impl Client { to: server_jid().clone(), target: None, id: None, - content: Some(wacore_binary::node::NodeContent::Nodes(vec![sync_node])), + content: Some(wacore_binary::NodeContent::Nodes(vec![sync_node])), timeout: Some(Duration::from_secs(30)), }; @@ -2466,7 +2489,9 @@ impl Client { let mut pre_downloaded: std::collections::HashMap> = std::collections::HashMap::new(); - if let Ok(patch_lists) = wacore::appstate::patch_decode::parse_patch_lists(&resp) { + if let Ok(patch_lists) = + wacore::appstate::patch_decode::parse_patch_lists_ref(resp.get()) + { for pl in &patch_lists { // Download external snapshot if let Some(ext) = &pl.snapshot_ref @@ -2525,7 +2550,9 @@ impl Client { // Parse and process all collections from the response let proc = self.get_app_state_processor().await; - let results = proc.decode_multi_patch_list(&resp, &download, true).await?; + let results = proc + .decode_multi_patch_list_ref(resp.get(), &download, true) + .await?; let mut needs_refetch = Vec::new(); @@ -2664,7 +2691,7 @@ impl Client { to: server_jid().clone(), target: None, id: None, - content: Some(wacore_binary::node::NodeContent::Nodes(vec![sync_node])), + content: Some(wacore_binary::NodeContent::Nodes(vec![sync_node])), timeout: None, }; @@ -2682,7 +2709,7 @@ impl Client { let mut pre_downloaded: std::collections::HashMap> = std::collections::HashMap::new(); - if let Ok(pl) = wacore::appstate::patch_decode::parse_patch_list(&resp) { + if let Ok(pl) = wacore::appstate::patch_decode::parse_patch_list_ref(resp.get()) { debug!(target: "Client/AppState", "Parsed patch list for {:?}: has_snapshot_ref={} has_more_patches={} patches_count={}", name, pl.snapshot_ref.is_some(), pl.has_more_patches, pl.patches.len()); @@ -2740,8 +2767,9 @@ impl Client { }; let proc = self.get_app_state_processor().await; - let (mutations, new_state, list) = - proc.decode_patch_list(&resp, &download, true).await?; + let (mutations, new_state, list) = proc + .decode_patch_list_ref(resp.get(), &download, true) + .await?; let decode_elapsed = _decode_start.elapsed(); if decode_elapsed.as_millis() > 500 { debug!(target: "Client/AppState", "Patch decode for {:?} took {:?}", name, decode_elapsed); @@ -2871,7 +2899,7 @@ impl Client { to: server_jid().clone(), target: None, id: None, - content: Some(wacore_binary::node::NodeContent::Nodes(vec![sync_node])), + content: Some(wacore_binary::NodeContent::Nodes(vec![sync_node])), timeout: None, }; @@ -2973,7 +3001,7 @@ impl Client { } } - pub(crate) async fn handle_stream_error(&self, node: &wacore_binary::node::Node) { + pub(crate) async fn handle_stream_error(&self, node: &wacore_binary::NodeRef<'_>) { self.is_logged_in.store(false, Ordering::Relaxed); let mut attrs = node.attrs(); @@ -3063,12 +3091,12 @@ impl Client { info!("Got 503 service unavailable, will auto-reconnect."); } _ => { - error!("Unknown stream error: {}", DisplayableNode(node)); + error!("Unknown stream error: {}", DisplayableNodeRef(node)); self.expected_disconnect.store(true, Ordering::Relaxed); self.core.event_bus.dispatch(&Event::StreamError( crate::types::events::StreamError { code: code.to_string(), - raw: Some(node.clone()), + raw: Some(node.to_owned()), }, )); } @@ -3091,7 +3119,7 @@ impl Client { self.shutdown_notifier.notify(usize::MAX); } - pub(crate) async fn handle_connect_failure(&self, node: &wacore_binary::node::Node) { + pub(crate) async fn handle_connect_failure(&self, node: &wacore_binary::NodeRef<'_>) { self.expected_disconnect.store(true, Ordering::Relaxed); self.shutdown_notifier.notify(usize::MAX); @@ -3120,7 +3148,10 @@ impl Client { let expire_secs = attrs.optional_u64("expire").unwrap_or(0); let expire_duration = chrono::Duration::try_seconds(expire_secs as i64).unwrap_or_default(); - warn!("Temporary ban connect failure: {}", DisplayableNode(node)); + warn!( + "Temporary ban connect failure: {}", + DisplayableNodeRef(node) + ); self.core.event_bus.dispatch(&Event::TemporaryBan( crate::types::events::TemporaryBan { code: crate::types::events::TempBanReason::from(ban_code), @@ -3133,7 +3164,7 @@ impl Client { .event_bus .dispatch(&Event::ClientOutdated(crate::types::events::ClientOutdated)); } else { - warn!("Unknown connect failure: {}", DisplayableNode(node)); + warn!("Unknown connect failure: {}", DisplayableNodeRef(node)); self.core.event_bus.dispatch(&Event::ConnectFailure( crate::types::events::ConnectFailure { reason, @@ -3142,19 +3173,18 @@ impl Client { .as_deref() .unwrap_or("") .to_string(), - raw: Some(node.clone()), + raw: Some(node.to_owned()), }, )); } } - pub(crate) async fn handle_iq(self: &Arc, node: &wacore_binary::node::Node) -> bool { - if node.attrs.get("type").is_some_and(|s| s == "get") + pub(crate) async fn handle_iq(self: &Arc, node: &wacore_binary::NodeRef<'_>) -> bool { + if node.get_attr("type").is_some_and(|s| s.as_str() == "get") && (node.get_optional_child("ping").is_some() || node - .attrs - .get("xmlns") - .is_some_and(|s| s == "urn:xmpp:ping")) + .get_attr("xmlns") + .is_some_and(|s| s.as_str() == "urn:xmpp:ping")) { info!("Received ping, sending pong."); let mut parser = node.attrs(); @@ -3167,7 +3197,6 @@ impl Client { return true; } - // Pass Node directly to pair handling if pair::handle_iq(self, node).await { return true; } @@ -3203,7 +3232,7 @@ impl Client { pub fn wait_for_node( &self, filter: NodeFilter, - ) -> futures::channel::oneshot::Receiver> { + ) -> futures::channel::oneshot::Receiver> { let (tx, rx) = futures::channel::oneshot::channel(); self.node_waiter_count.fetch_add(1, Ordering::Release); let mut waiters = self @@ -3229,18 +3258,35 @@ impl Client { .sent_node_waiters .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); - waiters.push(NodeWaiter { filter, tx }); + waiters.push(SentNodeWaiter { filter, tx }); rx } /// Check pending node waiters against an incoming node. /// Only called when `node_waiter_count > 0`. - fn resolve_node_waiters(&self, node: &Arc) { + fn resolve_node_waiters(&self, node: &Arc) { resolve_waiters(&self.node_waiters, &self.node_waiter_count, node); } fn resolve_sent_node_waiters(&self, node: &Arc) { - resolve_waiters(&self.sent_node_waiters, &self.sent_node_waiter_count, node); + let nr = node.as_node_ref(); + let mut waiters = self + .sent_node_waiters + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let mut i = 0; + while i < waiters.len() { + if waiters[i].tx.is_canceled() { + waiters.swap_remove(i); + self.sent_node_waiter_count.fetch_sub(1, Ordering::Release); + } else if waiters[i].filter.matches(&nr) { + let w = waiters.swap_remove(i); + self.sent_node_waiter_count.fetch_sub(1, Ordering::Release); + let _ = w.tx.send(Arc::clone(node)); + } else { + i += 1; + } + } } fn clear_sent_node_waiters(&self) { @@ -3256,7 +3302,7 @@ impl Client { } } - pub(crate) fn update_server_time_offset(&self, node: &wacore_binary::node::Node) { + pub(crate) fn update_server_time_offset(&self, node: &wacore_binary::NodeRef<'_>) { self.unified_session.update_server_time_offset(node); } @@ -3418,7 +3464,7 @@ impl Client { pub(crate) async fn register_ack_waiter( &self, message_id: &str, - ) -> futures::channel::oneshot::Receiver { + ) -> futures::channel::oneshot::Receiver> { let (tx, rx) = futures::channel::oneshot::channel(); self.response_waiters .lock() @@ -3569,7 +3615,7 @@ impl Client { /// /// Matches WhatsApp Web (`WAWebCommsHandleStanza`): only includes `id` /// when the server ping carried one. -fn build_pong(to: String, id: Option<&str>) -> wacore_binary::node::Node { +fn build_pong(to: String, id: Option<&str>) -> wacore_binary::Node { let mut builder = NodeBuilder::new("iq").attr("to", to).attr("type", "result"); if let Some(id) = id { builder = builder.attr("id", id); @@ -3589,25 +3635,30 @@ fn build_pong(to: String, id: Option<&str>) -> wacore_binary::node::Node { /// `ackString = maybeAttrString("type")` — so `type` is only included when /// explicitly present on the incoming receipt (delivery receipts normally /// have no type attribute, meaning the ack also has no type). -fn build_ack_node(node: &Node, own_device_pn: Option<&Jid>) -> Option { - let id = node.attrs.get("id")?.clone(); - let from = node.attrs.get("from")?.clone(); - let participant = node.attrs.get("participant").cloned(); +fn build_ack_node(node: &wacore_binary::NodeRef<'_>, own_device_pn: Option<&Jid>) -> Option { + let id = NodeValue::from(node.get_attr("id")?.as_str().as_ref()); + let from = NodeValue::from(node.get_attr("from")?.as_str().as_ref()); + let participant = node + .get_attr("participant") + .map(|v| NodeValue::from(v.as_str().as_ref())); + + let tag = node.tag.as_ref(); // Whatsmeow: echo type for all stanza tags EXCEPT "message". // WA Web additionally omits type for notification type="encrypt" with child. - let typ = if node.tag != "message" && !is_encrypt_identity_notification(node) { - node.attrs.get("type").cloned() + let typ = if tag != "message" && !is_encrypt_identity_notification(node) { + node.get_attr("type") + .map(|v| NodeValue::from(v.as_str().as_ref())) } else { None }; let mut attrs = Attrs::new(); - attrs.insert("class", NodeValue::from(node.tag.as_ref())); + attrs.insert("class", NodeValue::from(tag)); attrs.insert("id", id); attrs.insert("to", from); - if node.tag == "message" + if tag == "message" && let Some(own_device_pn) = own_device_pn { attrs.insert("from", NodeValue::Jid(own_device_pn.clone())); @@ -3627,9 +3678,11 @@ fn build_ack_node(node: &Node, own_device_pn: Option<&Jid>) -> Option { } /// WA Web omits `type` when ACKing ``. -fn is_encrypt_identity_notification(node: &Node) -> bool { +fn is_encrypt_identity_notification(node: &wacore_binary::NodeRef<'_>) -> bool { node.tag == "notification" - && node.attrs.get("type").is_some_and(|v| v == "encrypt") + && node + .get_attr("type") + .is_some_and(|v| v.as_str() == "encrypt") && node.get_optional_child("identity").is_some() } @@ -3668,7 +3721,7 @@ mod tests { use crate::lid_pn_cache::LearningSource; use crate::test_utils::MockHttpClient; use futures::channel::oneshot; - use wacore_binary::jid::SERVER_JID; + use wacore_binary::SERVER_JID; #[tokio::test] async fn test_ack_behavior_for_incoming_stanzas() { @@ -3690,7 +3743,7 @@ mod tests { // --- Assertions --- // Verify that we still ack other critical stanzas (regression check). - use wacore_binary::node::{Attrs, Node, NodeContent}; + use wacore_binary::{Attrs, Node, NodeContent}; let mut receipt_attrs = Attrs::new(); receipt_attrs.insert("from".to_string(), "@s.whatsapp.net".to_string()); @@ -3711,11 +3764,11 @@ mod tests { ); assert!( - client.should_ack(&receipt_node), + client.should_ack(&receipt_node.as_node_ref()), "should_ack must still return TRUE for stanzas." ); assert!( - client.should_ack(¬ification_node), + client.should_ack(¬ification_node.as_node_ref()), "should_ack must still return TRUE for stanzas." ); @@ -3761,7 +3814,7 @@ mod tests { .build(); // 3. Handle the ack - let handled = client.handle_ack_response(ack_node).await; + let handled = client.handle_ack_response(&ack_node.as_node_ref()).await; assert!( handled, "handle_ack_response should return true when waiter exists" @@ -3772,9 +3825,9 @@ mod tests { Ok(Ok(response_node)) => { assert!( response_node - .attrs - .get("id") - .is_some_and(|v| v == test_id.as_str()), + .get() + .get_attr("id") + .is_some_and(|v| v.as_str() == test_id.as_str()), "Response node should have correct ID" ); } @@ -3817,7 +3870,7 @@ mod tests { .build(); // Should return false since there's no waiter - let handled = client.handle_ack_response(ack_node).await; + let handled = client.handle_ack_response(&ack_node.as_node_ref()).await; assert!( !handled, "handle_ack_response should return false when no waiter exists" @@ -4350,7 +4403,7 @@ mod tests { #[tokio::test] async fn test_ensure_e2e_sessions_waits_for_offline_sync() { use std::sync::atomic::Ordering; - use wacore_binary::jid::Jid; + use wacore_binary::Jid; let backend = Arc::new( crate::store::SqliteStore::new("file:memdb_ensure_e2e_waits?mode=memory&cache=shared") @@ -4435,7 +4488,7 @@ mod tests { #[tokio::test] async fn test_immediate_session_does_not_wait_for_offline_sync() { use std::sync::atomic::Ordering; - use wacore_binary::jid::Jid; + use wacore_binary::Jid; let backend = Arc::new( crate::store::SqliteStore::new("file:memdb_immediate_no_wait?mode=memory&cache=shared") @@ -4516,7 +4569,7 @@ mod tests { use wacore::libsignal::protocol::SessionRecord; use wacore::libsignal::store::SessionStore; use wacore::types::jid::JidExt; - use wacore_binary::jid::Jid; + use wacore_binary::Jid; let backend = Arc::new( crate::store::SqliteStore::new("file:memdb_skip_existing?mode=memory&cache=shared") @@ -4705,7 +4758,7 @@ mod tests { .build(); // Update the offset - client.update_server_time_offset(&node); + client.update_server_time_offset(&node.as_node_ref()); // The offset should be approximately 10 * 1000 = 10000 ms // Allow some tolerance for timing differences during the test @@ -4718,7 +4771,7 @@ mod tests { // Test with no 't' attribute - should not change offset let node_no_t = NodeBuilder::new("success").build(); - client.update_server_time_offset(&node_no_t); + client.update_server_time_offset(&node_no_t.as_node_ref()); let offset_after = client.unified_session.server_time_offset_ms(); assert!( (offset_after - offset).abs() < 100, // Should be same (or very close) @@ -4729,7 +4782,7 @@ mod tests { let node_invalid = NodeBuilder::new("success") .attr("t", "not_a_number") .build(); - client.update_server_time_offset(&node_invalid); + client.update_server_time_offset(&node_invalid.as_node_ref()); let offset_after_invalid = client.unified_session.server_time_offset_ms(); assert!( (offset_after_invalid - offset).abs() < 100, @@ -4738,7 +4791,7 @@ mod tests { // Test with negative/zero 't' - should not change offset let node_zero = NodeBuilder::new("success").attr("t", "0").build(); - client.update_server_time_offset(&node_zero); + client.update_server_time_offset(&node_zero.as_node_ref()); let offset_after_zero = client.unified_session.server_time_offset_ms(); assert!( (offset_after_zero - offset).abs() < 100, @@ -4844,6 +4897,10 @@ mod tests { info!("✅ test_unified_session_protocol_node passed"); } + fn node_to_owned_ref(node: Node) -> Arc { + crate::test_utils::node_to_owned_ref(&node) + } + /// Helper to create a test client for offline sync tests async fn create_offline_sync_test_client() -> Arc { let backend = crate::test_utils::create_test_backend().await; @@ -4877,7 +4934,7 @@ mod tests { .build()]) .build(); - client.process_node(Arc::new(node)).await; + client.process_node(node_to_owned_ref(node)).await; assert!( client.offline_sync_metrics.active.load(Ordering::Acquire), " should NOT end offline sync" @@ -4900,7 +4957,7 @@ mod tests { .build()]) .build(); - client.process_node(Arc::new(node)).await; + client.process_node(node_to_owned_ref(node)).await; assert!( client.offline_sync_metrics.active.load(Ordering::Acquire), " should NOT end offline sync" @@ -4922,7 +4979,7 @@ mod tests { .build()]) .build(); - client.process_node(Arc::new(node)).await; + client.process_node(node_to_owned_ref(node)).await; assert!( client.offline_sync_metrics.active.load(Ordering::Acquire), " should NOT end offline sync" @@ -4945,7 +5002,7 @@ mod tests { .children([NodeBuilder::new("offline").attr("count", "301").build()]) .build(); - client.process_node(Arc::new(node)).await; + client.process_node(node_to_owned_ref(node)).await; assert!( !client.offline_sync_metrics.active.load(Ordering::Acquire), " should end offline sync" @@ -4966,7 +5023,7 @@ mod tests { .build()]) .build(); - client.process_node(Arc::new(node)).await; + client.process_node(node_to_owned_ref(node)).await; assert!( client.offline_sync_metrics.active.load(Ordering::Acquire), "offline_preview with count>0 should activate sync" @@ -5000,7 +5057,7 @@ mod tests { .attr("type", "text") .build(); - client.process_node(Arc::new(node)).await; + client.process_node(node_to_owned_ref(node)).await; assert_eq!( client .offline_sync_metrics @@ -5057,7 +5114,7 @@ mod tests { .children([NodeBuilder::new("ping").build()]) .build(); - let handled = client.handle_iq(&ping_node).await; + let handled = client.handle_iq(&ping_node.as_node_ref()).await; assert!( handled, "handle_iq must recognize ping with child element" @@ -5091,7 +5148,7 @@ mod tests { .attr("xmlns", "urn:xmpp:ping") .build(); - let handled = client.handle_iq(&ping_node).await; + let handled = client.handle_iq(&ping_node.as_node_ref()).await; assert!( handled, "handle_iq must recognize ping with xmlns=\"urn:xmpp:ping\" attribute (no children)" @@ -5125,7 +5182,7 @@ mod tests { .children([NodeBuilder::new("ping").build()]) .build(); - let handled = client.handle_iq(&ping_node).await; + let handled = client.handle_iq(&ping_node.as_node_ref()).await; assert!( handled, "handle_iq must handle ping with both child and xmlns" @@ -5157,7 +5214,7 @@ mod tests { .attr("xmlns", "some:other:namespace") .build(); - let handled = client.handle_iq(&non_ping_node).await; + let handled = client.handle_iq(&non_ping_node.as_node_ref()).await; assert!( !handled, "handle_iq must NOT treat non-ping xmlns as a ping" @@ -5189,7 +5246,7 @@ mod tests { .attr("xmlns", "urn:xmpp:ping") .build(); - let handled = client.handle_iq(&result_node).await; + let handled = client.handle_iq(&result_node.as_node_ref()).await; assert!( !handled, "handle_iq must NOT respond to type=\"result\" even with ping xmlns" @@ -5229,7 +5286,7 @@ mod tests { .build(); assert!( - is_encrypt_identity_notification(&node), + is_encrypt_identity_notification(&node.as_node_ref()), "identity-change notification ACK must omit type to match WA Web" ); } @@ -5244,7 +5301,7 @@ mod tests { .build(); assert!( - !is_encrypt_identity_notification(&node), + !is_encrypt_identity_notification(&node.as_node_ref()), "device notification is not an encrypt+identity notification" ); } @@ -5263,7 +5320,7 @@ mod tests { .parse() .expect("own device PN JID should parse"); - let ack = build_ack_node(&incoming, Some(&own_device_pn)) + let ack = build_ack_node(&incoming.as_node_ref(), Some(&own_device_pn)) .expect("message ack should be buildable"); assert_eq!(ack.tag, "ack"); @@ -5303,7 +5360,7 @@ mod tests { .parse() .expect("own device PN JID should parse"); - let ack = build_ack_node(&incoming, Some(&own_device_pn)) + let ack = build_ack_node(&incoming.as_node_ref(), Some(&own_device_pn)) .expect("notification ack should be buildable"); assert!(ack.attrs.get("class").is_some_and(|v| v == "notification")); @@ -5329,7 +5386,7 @@ mod tests { .parse() .expect("own device PN JID should parse"); - let ack = build_ack_node(&incoming, Some(&own_device_pn)) + let ack = build_ack_node(&incoming.as_node_ref(), Some(&own_device_pn)) .expect("receipt ack should be buildable"); assert!(ack.attrs.get("class").is_some_and(|v| v == "receipt")); @@ -5355,7 +5412,7 @@ mod tests { .parse() .expect("own device PN JID should parse"); - let ack = build_ack_node(&incoming, Some(&own_device_pn)) + let ack = build_ack_node(&incoming.as_node_ref(), Some(&own_device_pn)) .expect("receipt ack should be buildable"); assert!(ack.attrs.get("class").is_some_and(|v| v == "receipt")); @@ -5394,7 +5451,7 @@ mod tests { .attr("xmlns", "urn:xmpp:ping") .build(); - let handled = client.handle_iq(&ping_node).await; + let handled = client.handle_iq(&ping_node.as_node_ref()).await; assert!( handled, "handle_iq must recognize ping without id attribute" @@ -5451,7 +5508,7 @@ mod tests { async fn test_stream_error_401_disables_reconnect() { let client = create_offline_sync_test_client().await; let node = NodeBuilder::new("stream:error").attr("code", "401").build(); - client.handle_stream_error(&node).await; + client.handle_stream_error(&node.as_node_ref()).await; assert!( !client.enable_auto_reconnect.load(Ordering::Relaxed), "401 should disable auto-reconnect" @@ -5462,7 +5519,7 @@ mod tests { async fn test_stream_error_409_disables_reconnect() { let client = create_offline_sync_test_client().await; let node = NodeBuilder::new("stream:error").attr("code", "409").build(); - client.handle_stream_error(&node).await; + client.handle_stream_error(&node.as_node_ref()).await; assert!( !client.enable_auto_reconnect.load(Ordering::Relaxed), "409 should disable auto-reconnect" @@ -5474,7 +5531,7 @@ mod tests { let client = create_offline_sync_test_client().await; let before = client.auto_reconnect_errors.load(Ordering::Relaxed); let node = NodeBuilder::new("stream:error").attr("code", "429").build(); - client.handle_stream_error(&node).await; + client.handle_stream_error(&node.as_node_ref()).await; assert!( client.enable_auto_reconnect.load(Ordering::Relaxed), "429 should keep auto-reconnect enabled" @@ -5491,7 +5548,7 @@ mod tests { async fn test_stream_error_503_keeps_reconnect() { let client = create_offline_sync_test_client().await; let node = NodeBuilder::new("stream:error").attr("code", "503").build(); - client.handle_stream_error(&node).await; + client.handle_stream_error(&node.as_node_ref()).await; assert!( client.enable_auto_reconnect.load(Ordering::Relaxed), "503 should keep auto-reconnect enabled" @@ -5615,7 +5672,7 @@ mod tests { .attr("participant", "236395184570386@lid") .build(); - let result = client.send_ack_for(&receipt).await; + let result = client.send_ack_for(&receipt.as_node_ref()).await; assert!( matches!(result, Err(ClientError::NotConnected)), "send_ack_for must return Err(NotConnected) when disconnected, got: {result:?}" @@ -5649,7 +5706,7 @@ mod tests { .attr("id", "TEST-RECEIPT-ID") .build(); - let result = client.send_ack_for(&receipt).await; + let result = client.send_ack_for(&receipt.as_node_ref()).await; assert!( result.is_ok(), "send_ack_for should return Ok during expected disconnect" diff --git a/src/client/context_impl.rs b/src/client/context_impl.rs index 65a74db85..4f32e96eb 100644 --- a/src/client/context_impl.rs +++ b/src/client/context_impl.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use wacore::client::context::{GroupInfo, SendContextResolver}; use wacore::iq::prekeys::PreKeyFetchReason; use wacore::libsignal::protocol::PreKeyBundle; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] diff --git a/src/client/device_registry.rs b/src/client/device_registry.rs index f8b602a14..5cf786f4f 100644 --- a/src/client/device_registry.rs +++ b/src/client/device_registry.rs @@ -5,7 +5,7 @@ use anyhow::Result; use log::{debug, info, warn}; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; use super::Client; @@ -97,7 +97,7 @@ impl Client { } /// WA Web: `isFromKnownDevice(author)` — local check only, no network. - pub(crate) async fn is_from_known_device(&self, sender: &wacore_binary::jid::Jid) -> bool { + pub(crate) async fn is_from_known_device(&self, sender: &wacore_binary::Jid) -> bool { let device_id = sender.device as u32; self.has_device(&sender.user, device_id).await } @@ -237,7 +237,7 @@ impl Client { "raw_id mismatch for user {user}: stored={stored_raw_id}, received={}. Clearing record.", decoded.raw_id ); - self.clear_device_record(user, &device.jid.server, &record) + self.clear_device_record(user, device.jid.server.as_str(), &record) .await; record.devices.clear(); } @@ -300,14 +300,11 @@ impl Client { /// `patch_device_remove`. async fn delete_sessions_for_devices(&self, user: &str, device_ids: &[u16]) { let lookup = self.resolve_lookup_keys(user).await; - let servers = [ - wacore_binary::jid::HIDDEN_USER_SERVER, - wacore_binary::jid::DEFAULT_USER_SERVER, - ]; - for &srv in &servers { + let servers = [wacore_binary::Server::Lid, wacore_binary::Server::Pn]; + for server in servers { for key in lookup.all_keys() { for &device_id in device_ids { - let mut jid = Jid::new(key, srv); + let mut jid = Jid::new(key, server); jid.device = device_id; let addr = wacore::types::jid::JidExt::to_protocol_address(&jid); self.signal_cache.delete_session(&addr).await; @@ -742,7 +739,7 @@ mod tests { wacore::stanza::devices::DeviceElement { jid: Jid { user: "15551234567".into(), - server: "s.whatsapp.net".into(), + server: wacore_binary::Server::Pn, device: device_id, ..Default::default() }, diff --git a/src/client/lid_pn.rs b/src/client/lid_pn.rs index 5333c9fc6..a59a33a65 100644 --- a/src/client/lid_pn.rs +++ b/src/client/lid_pn.rs @@ -11,7 +11,7 @@ use anyhow::Result; use log::debug; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; use super::Client; use crate::lid_pn_cache::{LearningSource, LidPnEntry}; @@ -136,18 +136,15 @@ impl Client { /// For PN JIDs, this checks if a LID mapping exists and returns the LID. /// This ensures that sending and receiving use the same session lock. pub(crate) async fn resolve_encryption_jid(&self, target: &Jid) -> Jid { - let pn_server = wacore_binary::jid::DEFAULT_USER_SERVER; - let lid_server = wacore_binary::jid::HIDDEN_USER_SERVER; - - if target.server == lid_server { + if target.is_lid() { // Already a LID - use it directly target.clone() - } else if target.server == pn_server { + } else if target.is_pn() { // PN JID - check if we have a LID mapping if let Some(lid_user) = self.lid_pn_cache.get_current_lid(&target.user).await { let lid_jid = Jid { user: lid_user.into(), - server: wacore_binary::jid::cow_server_from_str(lid_server), + server: wacore_binary::Server::Lid, device: target.device, agent: target.agent, integrator: target.integrator, @@ -259,7 +256,7 @@ mod tests { use crate::lid_pn_cache::LearningSource; use crate::test_utils::create_test_client; use std::sync::Arc; - use wacore_binary::jid::HIDDEN_USER_SERVER; + use wacore_binary::Server; #[tokio::test] async fn test_resolve_encryption_jid_pn_to_lid() { @@ -277,7 +274,7 @@ mod tests { let resolved = client.resolve_encryption_jid(&pn_jid).await; assert_eq!(resolved.user, lid); - assert_eq!(resolved.server, HIDDEN_USER_SERVER); + assert_eq!(resolved.server, Server::Lid); } #[tokio::test] diff --git a/src/client/sender_keys.rs b/src/client/sender_keys.rs index f6eabf55f..c94988588 100644 --- a/src/client/sender_keys.rs +++ b/src/client/sender_keys.rs @@ -1,7 +1,7 @@ //! Sender key tracking and message cache methods for Client. use anyhow::Result; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; use waproto::whatsapp as wa; use super::Client; diff --git a/src/client/sessions.rs b/src/client/sessions.rs index 2f440d9f2..7694d000a 100644 --- a/src/client/sessions.rs +++ b/src/client/sessions.rs @@ -5,7 +5,7 @@ use std::sync::atomic::Ordering; use std::time::Duration; use wacore::libsignal::store::SessionStore; use wacore::types::jid::JidExt; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; use super::Client; use crate::types::events::{Event, OfflineSyncCompleted}; @@ -315,7 +315,7 @@ impl Client { #[cfg(test)] mod tests { use super::*; - use wacore_binary::jid::{DEFAULT_USER_SERVER, HIDDEN_USER_SERVER, JidExt}; + use wacore_binary::{JidExt, Server}; #[test] fn test_primary_phone_jid_creation_from_pn() { @@ -323,7 +323,7 @@ mod tests { let primary_phone_jid = own_pn.with_device(0); assert_eq!(primary_phone_jid.user, "559999999999"); - assert_eq!(primary_phone_jid.server, DEFAULT_USER_SERVER); + assert_eq!(primary_phone_jid.server, Server::Pn); assert_eq!(primary_phone_jid.device, 0); assert_eq!(primary_phone_jid.agent, 0); assert_eq!(primary_phone_jid.to_string(), "559999999999@s.whatsapp.net"); @@ -336,7 +336,7 @@ mod tests { let primary_phone_jid = own_pn.with_device(0); assert_eq!(primary_phone_jid.user, "559999999999"); - assert_eq!(primary_phone_jid.server, DEFAULT_USER_SERVER); + assert_eq!(primary_phone_jid.server, Server::Pn); assert_eq!(primary_phone_jid.device, 0); } @@ -358,7 +358,7 @@ mod tests { let primary_phone_jid = own_lid.with_device(0); assert_eq!(primary_phone_jid.user, "100000000000001"); - assert_eq!(primary_phone_jid.server, HIDDEN_USER_SERVER); + assert_eq!(primary_phone_jid.server, Server::Lid); assert_eq!(primary_phone_jid.device, 0); assert!(!primary_phone_jid.is_ad()); } @@ -373,7 +373,7 @@ mod tests { let parsed: Jid = jid_string.parse().expect("JID should be parseable"); assert_eq!(parsed.user, "559999999999"); - assert_eq!(parsed.server, DEFAULT_USER_SERVER); + assert_eq!(parsed.server, Server::Pn); assert_eq!(parsed.device, 0); } @@ -591,7 +591,7 @@ mod tests { #[test] fn test_session_establishment_lookup_normalization() { use std::collections::HashMap; - use wacore_binary::jid::Jid; + use wacore_binary::Jid; // Represents the bundle map returned by fetch_pre_keys // (keys are normalized by parsing logic as verified in wacore/src/prekeys.rs) diff --git a/src/features/blocking.rs b/src/features/blocking.rs index 7e1965f87..2e4239a9f 100644 --- a/src/features/blocking.rs +++ b/src/features/blocking.rs @@ -8,7 +8,7 @@ use crate::request::IqError; use log::debug; pub use wacore::iq::blocklist::BlocklistEntry; use wacore::iq::blocklist::{GetBlocklistSpec, UpdateBlocklistSpec}; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; /// Feature handle for blocklist operations. pub struct Blocking<'a> { diff --git a/src/features/chat_actions.rs b/src/features/chat_actions.rs index c1753eca9..5dc76ff84 100644 --- a/src/features/chat_actions.rs +++ b/src/features/chat_actions.rs @@ -14,7 +14,7 @@ use wacore::types::events::{ ArchiveUpdate, ContactUpdate, DeleteChatUpdate, DeleteMessageForMeUpdate, Event, MarkChatAsReadUpdate, MuteUpdate, PinUpdate, StarUpdate, }; -use wacore_binary::jid::{Jid, JidExt}; +use wacore_binary::{Jid, JidExt}; use waproto::whatsapp as wa; /// WA Web uses `-1` for indefinite mute. diff --git a/src/features/chatstate.rs b/src/features/chatstate.rs index 46ece7cfa..ff173ca9e 100644 --- a/src/features/chatstate.rs +++ b/src/features/chatstate.rs @@ -3,8 +3,8 @@ use crate::client::Client; use log::debug; use wacore::StringEnum; +use wacore_binary::Jid; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::Jid; /// Chat state type for typing indicators. #[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)] @@ -52,7 +52,7 @@ impl<'a> Chatstate<'a> { self.send(to, ChatStateType::Paused).await } - fn build_chatstate_node(&self, to: &Jid, state: ChatStateType) -> wacore_binary::node::Node { + fn build_chatstate_node(&self, to: &Jid, state: ChatStateType) -> wacore_binary::Node { let child = match state { ChatStateType::Composing => NodeBuilder::new("composing").build(), ChatStateType::Recording => { diff --git a/src/features/community.rs b/src/features/community.rs index 4843de1c8..c06b37cab 100644 --- a/src/features/community.rs +++ b/src/features/community.rs @@ -14,7 +14,7 @@ use wacore::iq::groups::{ DeleteCommunityIq, GetLinkedGroupsParticipantsIq, GroupCreateIq, GroupCreateOptions, JoinLinkedGroupIq, LinkSubgroupsIq, QueryLinkedGroupIq, UnlinkSubgroupsIq, }; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; // Types diff --git a/src/features/contacts.rs b/src/features/contacts.rs index 24668029b..884640b23 100644 --- a/src/features/contacts.rs +++ b/src/features/contacts.rs @@ -10,7 +10,7 @@ use log::debug; use std::collections::HashMap; use wacore::iq::contacts::{ProfilePictureSpec, ProfilePictureType}; use wacore::iq::usync::{IsOnWhatsAppQueryType, IsOnWhatsAppSpec, IsOnWhatsAppUser, UserInfoSpec}; -use wacore_binary::jid::{Jid, JidExt}; +use wacore_binary::{Jid, JidExt}; // Re-export types from wacore pub use wacore::iq::contacts::ProfilePicture; diff --git a/src/features/groups.rs b/src/features/groups.rs index aef0d2852..73d53cb07 100644 --- a/src/features/groups.rs +++ b/src/features/groups.rs @@ -10,7 +10,7 @@ use wacore::iq::groups::{ SetGroupSubjectIq, SetMemberAddModeIq, normalize_participants, }; use wacore::types::message::AddressingMode; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; pub use wacore::iq::groups::{ GroupCreateOptions, GroupDescription, GroupParticipantOptions, GroupSubject, JoinGroupResult, diff --git a/src/features/media_reupload.rs b/src/features/media_reupload.rs index c395f1847..bfd8bc0b8 100644 --- a/src/features/media_reupload.rs +++ b/src/features/media_reupload.rs @@ -14,7 +14,7 @@ pub use wacore::media_retry::MediaRetryResult; use wacore::media_retry::{ build_media_retry_receipt, encrypt_media_retry_receipt, parse_media_retry_notification, }; -use wacore_binary::jid::{Jid, JidExt as _}; +use wacore_binary::{Jid, JidExt as _}; const MEDIA_RETRY_TIMEOUT: Duration = Duration::from_secs(30); @@ -108,7 +108,7 @@ impl<'a> MediaReupload<'a> { ); // Parse and decrypt the response - parse_media_retry_notification(¬ification_node, req.media_key) + parse_media_retry_notification(notification_node.get(), req.media_key) } } diff --git a/src/features/newsletter.rs b/src/features/newsletter.rs index e73d03bee..e78a84c39 100644 --- a/src/features/newsletter.rs +++ b/src/features/newsletter.rs @@ -12,9 +12,9 @@ use prost::Message as ProtoMessage; use serde_json::json; use wacore::iq::newsletter::NEWSLETTER_XMLNS; use wacore::request::InfoQuery; +use wacore_binary::Jid; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::Jid; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{NodeContent, NodeContentRef, NodeRef}; use waproto::whatsapp as wa; // Types @@ -353,9 +353,10 @@ impl<'a> Newsletter<'a> { ); let response = self.client.send_iq(iq).await?; - let duration = response + let nr = response.get(); + let duration = nr .get_optional_child("live_updates") - .and_then(|n| n.attrs.get("duration")) + .and_then(|n| n.get_attr("duration")) .map(|v| v.as_str()) .and_then(|s| s.parse::().ok()) .unwrap_or(300); @@ -409,7 +410,7 @@ impl<'a> Newsletter<'a> { ); let response = self.client.send_iq(iq).await?; - parse_newsletter_messages_response(&response) + parse_newsletter_messages_response(response.get()) } } @@ -496,23 +497,22 @@ fn parse_newsletter_metadata(value: &serde_json::Value) -> Result` node. /// Used by both message history parsing and notification handling. -pub(crate) fn parse_reaction_counts(node: &Node) -> Vec { +pub(crate) fn parse_reaction_counts(node: &NodeRef<'_>) -> Vec { let mut reactions = Vec::new(); if let Some(reactions_node) = node.get_optional_child("reactions") && let Some(children) = reactions_node.children() { for r in children.iter().filter(|n| n.tag.as_ref() == "reaction") { let Some(code) = r - .attrs - .get("code") - .map(|v| v.as_str().into_owned()) + .get_attr("code") + .map(|v| v.as_str()) .filter(|s| !s.is_empty()) + .map(|s| s.into_owned()) else { continue; }; let count = r - .attrs - .get("count") + .get_attr("count") .map(|v| v.as_str()) .and_then(|s| s.parse::().ok()) .unwrap_or(0); @@ -536,7 +536,7 @@ pub(crate) fn parse_reaction_counts(node: &Node) -> Vec /// /// ``` fn parse_newsletter_messages_response( - response: &Node, + response: &NodeRef<'_>, ) -> Result, anyhow::Error> { // Response is the IQ result node; find child let messages_node = response @@ -552,8 +552,7 @@ fn parse_newsletter_messages_response( for msg_node in children.iter().filter(|n| n.tag.as_ref() == "message") { // Skip nodes without a valid server_id (required for pagination/correlation) let Some(server_id) = msg_node - .attrs - .get("server_id") + .get_attr("server_id") .map(|v| v.as_str()) .and_then(|s| s.parse::().ok()) else { @@ -561,27 +560,29 @@ fn parse_newsletter_messages_response( }; let timestamp = msg_node - .attrs - .get("t") + .get_attr("t") .map(|v| v.as_str()) .and_then(|s| s.parse::().ok()) .unwrap_or(0); let message_type = msg_node - .attrs - .get("type") - .map(|v| NewsletterMessageType::from(v.as_str().as_ref())) + .get_attr("type") + .map(|v| v.as_str()) + .map(|s| NewsletterMessageType::from(s.as_ref())) .unwrap_or(NewsletterMessageType::Text); - let is_sender = msg_node.attrs.get("is_sender").is_some_and(|v| v == "true"); + let is_sender = msg_node + .get_attr("is_sender") + .is_some_and(|v| v.as_str() == "true"); // Decode protobuf bytes - let message = msg_node - .get_optional_child("plaintext") - .and_then(|pt| match &pt.content { - Some(NodeContent::Bytes(bytes)) => wa::Message::decode(bytes.as_slice()).ok(), - _ => None, - }); + let message = + msg_node + .get_optional_child("plaintext") + .and_then(|pt| match pt.content.as_deref() { + Some(NodeContentRef::Bytes(bytes)) => wa::Message::decode(bytes.as_ref()).ok(), + _ => None, + }); let reactions = parse_reaction_counts(msg_node); @@ -614,7 +615,7 @@ mod tests { .build()]) .build(); - let msgs = parse_newsletter_messages_response(&response).unwrap(); + let msgs = parse_newsletter_messages_response(&response.as_node_ref()).unwrap(); assert_eq!(msgs.len(), 1); assert_eq!(msgs[0].message_type, NewsletterMessageType::Text); } @@ -631,7 +632,7 @@ mod tests { .build()]) .build(); - let msgs = parse_newsletter_messages_response(&response).unwrap(); + let msgs = parse_newsletter_messages_response(&response.as_node_ref()).unwrap(); assert_eq!(msgs[0].message_type, NewsletterMessageType::Media); } } diff --git a/src/features/polls.rs b/src/features/polls.rs index 3b73ef7c1..b6c97f40c 100644 --- a/src/features/polls.rs +++ b/src/features/polls.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use anyhow::{Result, anyhow}; use wacore::poll; -use wacore_binary::jid::{Jid, JidExt}; +use wacore_binary::{Jid, JidExt}; use waproto::whatsapp as wa; use crate::client::Client; diff --git a/src/features/presence.rs b/src/features/presence.rs index c203fe967..0787aa5d6 100644 --- a/src/features/presence.rs +++ b/src/features/presence.rs @@ -3,9 +3,9 @@ use log::{debug, warn}; use thiserror::Error; use wacore::StringEnum; use wacore::iq::tctoken::build_tc_token_node; +use wacore_binary::Jid; +use wacore_binary::Node; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::Jid; -use wacore_binary::node::Node; #[derive(Debug, Error)] pub enum PresenceError { diff --git a/src/features/signal.rs b/src/features/signal.rs index 89c9071c8..5f2907aed 100644 --- a/src/features/signal.rs +++ b/src/features/signal.rs @@ -12,8 +12,8 @@ use wacore::libsignal::store::sender_key_name::SenderKeyName; use wacore::message_processing::EncType; use wacore::messages::MessageUtils; use wacore::types::jid::JidExt; -use wacore_binary::jid::Jid; -use wacore_binary::node::Node; +use wacore_binary::Jid; +use wacore_binary::Node; use crate::client::Client; diff --git a/src/features/status.rs b/src/features/status.rs index c8ac7f747..30b12191f 100644 --- a/src/features/status.rs +++ b/src/features/status.rs @@ -1,5 +1,5 @@ use wacore::StringEnum; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; use waproto::whatsapp as wa; use crate::client::Client; diff --git a/src/features/tctoken.rs b/src/features/tctoken.rs index f1b76a6d3..16871cb7b 100644 --- a/src/features/tctoken.rs +++ b/src/features/tctoken.rs @@ -22,7 +22,7 @@ use crate::client::Client; use crate::request::IqError; use wacore::iq::tctoken::{IssuePrivacyTokensSpec, ReceivedTcToken}; use wacore::store::traits::TcTokenEntry; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; /// Feature handle for trusted contact token operations. pub struct TcToken<'a> { diff --git a/src/handlers/basic.rs b/src/handlers/basic.rs index d02ef629d..77d6f8664 100644 --- a/src/handlers/basic.rs +++ b/src/handlers/basic.rs @@ -2,11 +2,9 @@ use super::traits::StanzaHandler; use crate::client::Client; use async_trait::async_trait; use std::sync::Arc; -use wacore_binary::node::Node; +use wacore_binary::OwnedNodeRef; /// Handler for `<success>` stanzas. -/// -/// Processes successful authentication/connection events. #[derive(Default)] pub struct SuccessHandler; @@ -17,15 +15,18 @@ impl StanzaHandler for SuccessHandler { "success" } - async fn handle(&self, client: Arc<Client>, node: Arc<Node>, _cancelled: &mut bool) -> bool { - client.handle_success(&node).await; + async fn handle( + &self, + client: Arc<Client>, + node: Arc<OwnedNodeRef>, + _cancelled: &mut bool, + ) -> bool { + client.handle_success(node.get()).await; true } } /// Handler for `<failure>` stanzas. -/// -/// Processes connection or authentication failures. #[derive(Default)] pub struct FailureHandler; @@ -36,15 +37,18 @@ impl StanzaHandler for FailureHandler { "failure" } - async fn handle(&self, client: Arc<Client>, node: Arc<Node>, _cancelled: &mut bool) -> bool { - client.handle_connect_failure(&node).await; + async fn handle( + &self, + client: Arc<Client>, + node: Arc<OwnedNodeRef>, + _cancelled: &mut bool, + ) -> bool { + client.handle_connect_failure(node.get()).await; true } } /// Handler for `<stream:error>` stanzas. -/// -/// Processes stream-level errors that may require connection reset. #[derive(Default)] pub struct StreamErrorHandler; @@ -55,15 +59,18 @@ impl StanzaHandler for StreamErrorHandler { "stream:error" } - async fn handle(&self, client: Arc<Client>, node: Arc<Node>, _cancelled: &mut bool) -> bool { - client.handle_stream_error(&node).await; + async fn handle( + &self, + client: Arc<Client>, + node: Arc<OwnedNodeRef>, + _cancelled: &mut bool, + ) -> bool { + client.handle_stream_error(node.get()).await; true } } /// Handler for `<ack>` stanzas. -/// -/// Processes acknowledgment messages. #[derive(Default)] pub struct AckHandler; @@ -74,13 +81,13 @@ impl StanzaHandler for AckHandler { "ack" } - async fn handle(&self, client: Arc<Client>, node: Arc<Node>, _cancelled: &mut bool) -> bool { - // Delegate to the client to check if any task is waiting for this ack. - // The client will resolve pending response waiters if the ID matches. - // Try to unwrap Arc or clone Node if there are other references - let owned_node = Arc::try_unwrap(node).unwrap_or_else(|arc| (*arc).clone()); - client.handle_ack_response(owned_node).await; - // We return `true` because this handler's purpose is to consume all <ack> stanzas. + async fn handle( + &self, + client: Arc<Client>, + node: Arc<OwnedNodeRef>, + _cancelled: &mut bool, + ) -> bool { + client.handle_ack_response(node.get()).await; true } } diff --git a/src/handlers/chatstate.rs b/src/handlers/chatstate.rs index dcadb2733..13f9bff7f 100644 --- a/src/handlers/chatstate.rs +++ b/src/handlers/chatstate.rs @@ -8,8 +8,7 @@ use std::sync::Arc; use wacore::iq::chatstate::{ ChatstateParseError, ChatstateSource, ChatstateStanza, ReceivedChatState, }; -use wacore_binary::jid::Jid; -use wacore_binary::node::Node; +use wacore_binary::Jid; /// Event for incoming chatstate (`<chatstate/>`) stanzas. /// @@ -54,8 +53,13 @@ impl StanzaHandler for ChatstateHandler { "chatstate" } - async fn handle(&self, client: Arc<Client>, node: Arc<Node>, _cancelled: &mut bool) -> bool { - match ChatstateStanza::parse(&node) { + async fn handle( + &self, + client: Arc<Client>, + node: Arc<wacore_binary::OwnedNodeRef>, + _cancelled: &mut bool, + ) -> bool { + match ChatstateStanza::parse(node.get()) { Ok(stanza) => { debug!( target: "ChatstateHandler", diff --git a/src/handlers/ib.rs b/src/handlers/ib.rs index 8582b0ca0..3b98bfa8c 100644 --- a/src/handlers/ib.rs +++ b/src/handlers/ib.rs @@ -7,8 +7,6 @@ use std::sync::Arc; use wacore::appstate::patch_decode::WAPatchName; use wacore::iq::dirty::{DirtyBit, DirtyType}; -use wacore_binary::node::{Node, NodeContent}; - /// Handler for `<ib>` (information broadcast) stanzas. /// /// Processes various server notifications including: @@ -26,13 +24,18 @@ impl StanzaHandler for IbHandler { "ib" } - async fn handle(&self, client: Arc<Client>, node: Arc<Node>, _cancelled: &mut bool) -> bool { - handle_ib_impl(client, &node).await; + async fn handle( + &self, + client: Arc<Client>, + node: Arc<wacore_binary::OwnedNodeRef>, + _cancelled: &mut bool, + ) -> bool { + handle_ib_impl(client, node.get()).await; true } } -async fn handle_ib_impl(client: Arc<Client>, node: &Node) { +async fn handle_ib_impl(client: Arc<Client>, node: &wacore_binary::NodeRef<'_>) { for child in node.children().unwrap_or_default() { match child.tag.as_ref() { "dirty" => { @@ -105,35 +108,27 @@ async fn handle_ib_impl(client: Arc<Client>, node: &Node) { // Edge routing info is used for optimized reconnection to WhatsApp servers. // When present, it should be sent as a pre-intro before the Noise handshake. // Format on wire: ED (2 bytes) + length (3 bytes BE) + routing_data + WA header - if let Some(routing_info_node) = child.get_optional_child("routing_info") { - if let Some(NodeContent::Bytes(routing_bytes)) = &routing_info_node.content { - if !routing_bytes.is_empty() { - debug!( - "Received edge routing info ({} bytes), storing for reconnection", - routing_bytes.len() - ); - // Spawn to avoid blocking the read loop on Device write lock. - let routing_bytes = routing_bytes.clone(); - let client_clone = client.clone(); - client - .runtime - .spawn(Box::pin(async move { - client_clone - .persistence_manager - .modify_device(|device| { - device.edge_routing_info = Some(routing_bytes); - }) - .await; - })) - .detach(); - } else { - debug!("Received empty edge routing info, ignoring"); - } - } else { - debug!("Edge routing info node has no bytes content"); - } - } else { - debug!("Edge routing stanza has no routing_info child"); + if let Some(routing_info_node) = child.get_optional_child("routing_info") + && let Some(routing_bytes) = routing_info_node.content_bytes() + && !routing_bytes.is_empty() + { + debug!( + "Received edge routing info ({} bytes), storing for reconnection", + routing_bytes.len() + ); + let routing_bytes = routing_bytes.to_vec(); + let client_clone = client.clone(); + client + .runtime + .spawn(Box::pin(async move { + client_clone + .persistence_manager + .modify_device(|device| { + device.edge_routing_info = Some(routing_bytes); + }) + .await; + })) + .detach(); } } "offline_preview" => { diff --git a/src/handlers/iq.rs b/src/handlers/iq.rs index d8b7dc733..a33551274 100644 --- a/src/handlers/iq.rs +++ b/src/handlers/iq.rs @@ -3,8 +3,7 @@ use crate::client::Client; use async_trait::async_trait; use log::{debug, warn}; use std::sync::Arc; -use wacore::xml::DisplayableNode; -use wacore_binary::node::Node; +use wacore::xml::DisplayableNodeRef; /// Handler for `<iq>` (Info/Query) stanzas. /// @@ -23,15 +22,21 @@ impl StanzaHandler for IqHandler { "iq" } - async fn handle(&self, client: Arc<Client>, node: Arc<Node>, _cancelled: &mut bool) -> bool { - if !client.handle_iq(&node).await { - if node.attrs.get("type").is_some_and(|s| s == "result") { + async fn handle( + &self, + client: Arc<Client>, + node: Arc<wacore_binary::OwnedNodeRef>, + _cancelled: &mut bool, + ) -> bool { + let nr = node.get(); + if !client.handle_iq(nr).await { + if nr.get_attr("type").is_some_and(|s| s.as_str() == "result") { debug!( "Received late IQ response (waiter already removed): {}", - DisplayableNode(&node) + DisplayableNodeRef(nr) ); } else { - warn!("Received unhandled IQ: {}", DisplayableNode(&node)); + warn!("Received unhandled IQ: {}", DisplayableNodeRef(nr)); } } true diff --git a/src/handlers/message.rs b/src/handlers/message.rs index de39aa775..91369d9ac 100644 --- a/src/handlers/message.rs +++ b/src/handlers/message.rs @@ -3,7 +3,6 @@ use crate::client::Client; use async_trait::async_trait; use log::warn; use std::sync::Arc; -use wacore_binary::node::Node; /// WA Web: `WAWebMessageQueue` uses `promiseTimeout(r(), 2e4)` per queued handler. const MAX_MESSAGE_DELAY_MS: u64 = 20_000; @@ -29,7 +28,12 @@ impl StanzaHandler for MessageHandler { "message" } - async fn handle(&self, client: Arc<Client>, node: Arc<Node>, _cancelled: &mut bool) -> bool { + async fn handle( + &self, + client: Arc<Client>, + node: Arc<wacore_binary::OwnedNodeRef>, + _cancelled: &mut bool, + ) -> bool { // Extract the chat ID to serialize processing for this chat. // This prevents race conditions where a later message is processed before // the PreKey message that establishes the session. @@ -63,7 +67,7 @@ impl StanzaHandler for MessageHandler { .get_with_by_ref(&chat_id, async { // Unbounded so the read loop never blocks on a full channel. // WA Web uses unbounded promise chains for the same reason. - let (tx, rx) = async_channel::unbounded::<Arc<Node>>(); + let (tx, rx) = async_channel::unbounded::<Arc<wacore_binary::OwnedNodeRef>>(); let client_for_worker = client.clone(); let spawn_generation = client diff --git a/src/handlers/notification.rs b/src/handlers/notification.rs index 7abe2f222..48e042cba 100644 --- a/src/handlers/notification.rs +++ b/src/handlers/notification.rs @@ -14,8 +14,9 @@ use wacore::types::events::{ ContactUpdated, DeviceListUpdate, DeviceNotificationInfo, GroupUpdate, PictureUpdate, UserAboutUpdate, }; -use wacore_binary::jid::{Jid, JidExt}; -use wacore_binary::{jid::SERVER_JID, node::Node}; +use wacore_binary::NodeContentRef; +use wacore_binary::{Jid, JidExt}; +use wacore_binary::{NodeRef, OwnedNodeRef}; /// Handler for `<notification>` stanzas. /// @@ -34,14 +35,20 @@ impl StanzaHandler for NotificationHandler { "notification" } - async fn handle(&self, client: Arc<Client>, node: Arc<Node>, _cancelled: &mut bool) -> bool { - handle_notification_impl(&client, &node).await; + async fn handle( + &self, + client: Arc<Client>, + node: Arc<wacore_binary::OwnedNodeRef>, + _cancelled: &mut bool, + ) -> bool { + handle_notification_impl(&client, node).await; true } } -async fn handle_notification_impl(client: &Arc<Client>, node: &Node) { - let notification_type = node.attrs().optional_string("type"); +async fn handle_notification_impl(client: &Arc<Client>, node: Arc<OwnedNodeRef>) { + let nr = node.get(); + let notification_type = nr.attrs().optional_string("type"); let notification_type = notification_type.as_deref().unwrap_or_default(); match notification_type { @@ -51,16 +58,19 @@ async fn handle_notification_impl(client: &Arc<Client>, node: &Node) { // </notification> // WA Web: WAWebHandleIdentityChange — clears device record, deletes sessions, // marks sender keys for rotation, re-establishes session. - if node.get_optional_child("identity").is_some() { - handle_identity_change(client, node).await; - } else if node.attrs.get("from").is_some_and(|v| v == SERVER_JID) { + if nr.get_optional_child("identity").is_some() { + handle_identity_change(client, nr).await; + } else if nr + .get_attr("from") + .is_some_and(|v| v.as_str() == wacore_binary::SERVER_JID) + { // Server-originated encrypt notifications: // "count" → handlePreKeyLow, "digest" → handleDigestKey - let first_child_tag = node + let first_child_tag = nr .children() - .and_then(|c| c.first().map(|n| n.tag.clone())); + .and_then(|c| c.first().map(|n| n.tag.as_ref())); - match first_child_tag.as_deref() { + match first_child_tag { Some("count") => { handle_prekey_low(client).await; } @@ -81,7 +91,7 @@ async fn handle_notification_impl(client: &Arc<Client>, node: &Node) { use wacore::appstate::patch_decode::WAPatchName; let mut collections = Vec::new(); - if let Some(children) = node.children() { + if let Some(children) = nr.children() { for collection_node in children.iter().filter(|c| c.tag == "collection") { let name_cow = collection_node.attrs().optional_string("name"); let name_str = name_cow.as_deref().unwrap_or("<unknown>"); @@ -172,7 +182,7 @@ async fn handle_notification_impl(client: &Arc<Client>, node: &Node) { } "account_sync" => { // Handle push name updates - if let Some(new_push_name) = node.attrs().optional_string("pushname") { + if let Some(new_push_name) = nr.attrs().optional_string("pushname") { client .clone() .update_push_name_and_notify(new_push_name.to_string()) @@ -181,60 +191,60 @@ async fn handle_notification_impl(client: &Arc<Client>, node: &Node) { // Handle device list updates (when a new device is paired) // Matches WhatsApp Web's handleAccountSyncNotification for DEVICES type - if let Some(devices_node) = node.get_optional_child_by_tag(&["devices"]) { - handle_account_sync_devices(client, node, devices_node).await; + if let Some(devices_node) = nr.get_optional_child_by_tag(&["devices"]) { + handle_account_sync_devices(client, nr, devices_node).await; } } "devices" => { // Handle device list change notifications (WhatsApp Web: handleDevicesNotification) // These are sent when a user adds, removes, or updates a device - handle_devices_notification(client, node).await; + handle_devices_notification(client, nr).await; } "link_code_companion_reg" => { // Handle pair code notification (stage 2 of pair code authentication) // This is sent when the user enters the code on their phone - crate::pair_code::handle_pair_code_notification(client, node).await; + crate::pair_code::handle_pair_code_notification(client, nr).await; } "business" => { // Handle business notification (WhatsApp Web: handleBusinessNotification) // Notifies about business account status changes: verified name, profile, removal - handle_business_notification(client, node).await; + handle_business_notification(client, nr).await; } "picture" => { // Handle profile picture change notifications (WhatsApp Web: WAWebHandleProfilePicNotification) - handle_picture_notification(client, node); + handle_picture_notification(client, nr); } "privacy_token" => { // Handle incoming trusted contact privacy token notifications. // Matches WhatsApp Web's WAWebHandlePrivacyTokenNotification. - handle_privacy_token_notification(client, node).await; + handle_privacy_token_notification(client, nr).await; } "status" => { // Handle status/about text change notifications (WhatsApp Web: WAWebHandleAboutNotification) - handle_status_notification(client, node); + handle_status_notification(client, nr); } "contacts" => { - handle_contacts_notification(client, node).await; + handle_contacts_notification(client, nr).await; } "w:gp2" => { - handle_group_notification(client, node).await; + handle_group_notification(client, Arc::clone(&node)).await; } "disappearing_mode" => { // WA Web: WAWebHandleDisappearingModeNotification → // WAWebUpdateDisappearingModeForContact. // Parses <disappearing_mode duration="..." t="..."/> child, // updates the contact's default ephemeral setting. - handle_disappearing_mode_notification(client, node); + handle_disappearing_mode_notification(client, nr); } "newsletter" => { - handle_newsletter_notification(client, node); + handle_newsletter_notification(client, Arc::clone(&node)); } "mediaretry" => { // Handled by wait_for_node waiter in MediaReupload::request(). // Ack is sent automatically by the stanza dispatch loop. debug!( "Received mediaretry notification for msg {}", - node.attrs().optional_string("id").unwrap_or_default() + nr.attrs().optional_string("id").unwrap_or_default() ); } _ => { @@ -242,7 +252,7 @@ async fn handle_notification_impl(client: &Arc<Client>, node: &Node) { client .core .event_bus - .dispatch(&Event::Notification(node.clone())); + .dispatch(&Event::Notification(Arc::clone(&node))); } } } @@ -331,7 +341,7 @@ fn handle_digest_key(client: &Arc<Client>) { /// /// WA Web defers this when offline. We process immediately because all cleanup /// is local-only, and `ensure_e2e_sessions` self-defers via `wait_for_offline_delivery_end`. -async fn handle_identity_change(client: &Arc<Client>, node: &Node) { +async fn handle_identity_change(client: &Arc<Client>, node: &NodeRef<'_>) { let Some(from_jid) = node.attrs().optional_jid("from") else { warn!("Identity change notification missing 'from' attribute"); return; @@ -369,7 +379,7 @@ async fn handle_identity_change(client: &Arc<Client>, node: &Node) { // Deletes non-primary sessions + all sender key device tracking if let Some(record) = client.load_device_record(&from_jid.user).await { client - .clear_device_record(&from_jid.user, &from_jid.server, &record) + .clear_device_record(&from_jid.user, from_jid.server.as_str(), &record) .await; } @@ -440,8 +450,7 @@ async fn handle_identity_change(client: &Arc<Client>, node: &Node) { /// </add/remove/update> /// </notification> /// ``` -async fn handle_devices_notification(client: &Arc<Client>, node: &Node) { - // Parse using type-safe struct +async fn handle_devices_notification(client: &Arc<Client>, node: &NodeRef<'_>) { let notification = match DeviceNotification::try_parse(node) { Ok(n) => n, Err(e) => { @@ -535,7 +544,7 @@ struct AccountSyncDevice { /// <key-index-list ts="1766612162"><!-- bytes --></key-index-list> /// </devices> /// ``` -fn parse_account_sync_device_list(devices_node: &Node) -> Vec<AccountSyncDevice> { +fn parse_account_sync_device_list(devices_node: &NodeRef<'_>) -> Vec<AccountSyncDevice> { let Some(children) = devices_node.children() else { return Vec::new(); }; @@ -561,7 +570,11 @@ fn parse_account_sync_device_list(devices_node: &Node) -> Vec<AccountSyncDevice> /// 2. Parse device list from notification /// 3. Update device registry with new device list /// 4. Does NOT trigger app state sync (that's handled by server_sync) -async fn handle_account_sync_devices(client: &Arc<Client>, node: &Node, devices_node: &Node) { +async fn handle_account_sync_devices( + client: &Arc<Client>, + node: &NodeRef<'_>, + devices_node: &NodeRef<'_>, +) { // Extract the "from" JID - this is the account the notification is about let from_jid = match node.attrs().optional_jid("from") { Some(jid) => jid, @@ -675,7 +688,7 @@ async fn handle_account_sync_devices(client: &Arc<Client>, node: &Node, devices_ /// </tokens> /// </notification> /// ``` -async fn handle_privacy_token_notification(client: &Arc<Client>, node: &Node) { +async fn handle_privacy_token_notification(client: &Arc<Client>, node: &NodeRef<'_>) { use wacore::iq::tctoken::parse_privacy_token_notification; use wacore::store::traits::TcTokenEntry; @@ -810,7 +823,7 @@ async fn handle_privacy_token_notification(client: &Arc<Client>, node: &Node) { } /// Handle business notification (WhatsApp Web: `WAWebHandleBusinessNotification`). -async fn handle_business_notification(client: &Arc<Client>, node: &Node) { +async fn handle_business_notification(client: &Arc<Client>, node: &NodeRef<'_>) { let notification = match BusinessNotification::try_parse(node) { Ok(n) => n, Err(e) => { @@ -901,7 +914,7 @@ async fn handle_business_notification(client: &Arc<Client>, node: &Node) { /// <delete jid="user@s.whatsapp.net"/> /// </notification> /// ``` -fn handle_picture_notification(client: &Arc<Client>, node: &Node) { +fn handle_picture_notification(client: &Arc<Client>, node: &NodeRef<'_>) { let from = match node.attrs().optional_jid("from") { Some(jid) => jid, None => { @@ -991,7 +1004,7 @@ fn handle_picture_notification(client: &Arc<Client>, node: &Node) { /// <set>new status text</set> /// </notification> /// ``` -fn handle_status_notification(client: &Arc<Client>, node: &Node) { +fn handle_status_notification(client: &Arc<Client>, node: &NodeRef<'_>) { let from = match node.attrs().optional_jid("from") { Some(jid) => jid, None => { @@ -1003,11 +1016,9 @@ fn handle_status_notification(client: &Arc<Client>, node: &Node) { let timestamp = notification_timestamp(node); if let Some(set_node) = node.get_optional_child("set") { - let status_text = match &set_node.content { - Some(wacore_binary::node::NodeContent::String(s)) => s.to_string(), - Some(wacore_binary::node::NodeContent::Bytes(b)) => { - String::from_utf8_lossy(b).into_owned() - } + let status_text = match set_node.content.as_deref() { + Some(NodeContentRef::String(s)) => s.to_string(), + Some(NodeContentRef::Bytes(b)) => String::from_utf8_lossy(b.as_ref()).into_owned(), _ => String::new(), }; @@ -1030,7 +1041,7 @@ fn handle_status_notification(client: &Arc<Client>, node: &Node) { } } -fn notification_timestamp(node: &Node) -> chrono::DateTime<chrono::Utc> { +fn notification_timestamp(node: &NodeRef<'_>) -> chrono::DateTime<chrono::Utc> { node.attrs() .optional_u64("t") .and_then(|t| i64::try_from(t).ok()) @@ -1089,7 +1100,7 @@ async fn learn_contact_modify_mappings( /// changed phone number. Creates LID-PN mappings when LID attrs present. /// - `<sync after="..."/>` — server requests full contact re-sync. /// - `<add .../>` or `<remove .../>` — lightweight roster changes (ACK only). -async fn handle_contacts_notification(client: &Arc<Client>, node: &Node) { +async fn handle_contacts_notification(client: &Arc<Client>, node: &NodeRef<'_>) { let timestamp = notification_timestamp(node); let Some(child) = node.children().and_then(|children| children.first()) else { @@ -1198,8 +1209,8 @@ async fn handle_contacts_notification(client: &Arc<Client>, node: &Node) { /// and dispatches typed `Event::GroupUpdate` events for each. /// /// Reference: WhatsApp Web `WAWebHandleGroupNotification` (Ri7Gf1BxhsX.js:12556-12962) -async fn handle_group_notification(client: &Arc<Client>, node: &Node) { - let notification = match GroupNotification::try_from_node(node) { +async fn handle_group_notification(client: &Arc<Client>, node: Arc<OwnedNodeRef>) { + let notification = match GroupNotification::try_from_node_ref(node.get()) { Some(n) => n, None => { warn!(target: "Client/Group", "w:gp2 notification missing 'from' attribute"); @@ -1277,7 +1288,7 @@ async fn handle_group_notification(client: &Arc<Client>, node: &Node) { client .core .event_bus - .dispatch(&Event::Notification(node.clone())); + .dispatch(&Event::Notification(Arc::clone(&node))); } /// Handle `<notification type="newsletter">` — live updates with reaction counts. @@ -1294,17 +1305,19 @@ async fn handle_group_notification(client: &Arc<Client>, node: &Node) { /// </live_updates> /// </notification> /// ``` -fn handle_newsletter_notification(client: &Arc<Client>, node: &Node) { +fn handle_newsletter_notification(client: &Arc<Client>, node: Arc<OwnedNodeRef>) { use crate::features::newsletter::parse_reaction_counts; use wacore::types::events::{ NewsletterLiveUpdate, NewsletterLiveUpdateMessage, NewsletterLiveUpdateReaction, }; - let Some(newsletter_jid) = node.attrs().optional_jid("from") else { + let nr = node.get(); + + let Some(newsletter_jid) = nr.attrs().optional_jid("from") else { return; }; - if let Some(live_updates) = node.get_optional_child("live_updates") + if let Some(live_updates) = nr.get_optional_child("live_updates") && let Some(messages_node) = live_updates.get_optional_child("messages") && let Some(children) = messages_node.children() { @@ -1313,8 +1326,7 @@ fn handle_newsletter_notification(client: &Arc<Client>, node: &Node) { .filter(|n| n.tag.as_ref() == "message") .filter_map(|msg_node| { let server_id = msg_node - .attrs - .get("server_id") + .get_attr("server_id") .map(|v| v.as_str()) .and_then(|s| s.parse::<u64>().ok())?; @@ -1348,7 +1360,7 @@ fn handle_newsletter_notification(client: &Arc<Client>, node: &Node) { client .core .event_bus - .dispatch(&Event::Notification(node.clone())); + .dispatch(&Event::Notification(Arc::clone(&node))); } /// Handle `<notification type="disappearing_mode">` — a contact changed @@ -1361,14 +1373,14 @@ fn handle_newsletter_notification(client: &Arc<Client>, node: &Node) { /// /// We dispatch `Event::DisappearingModeChanged` and let consumers decide /// how to persist/apply it. -fn handle_disappearing_mode_notification(client: &Arc<Client>, node: &Node) { +fn handle_disappearing_mode_notification(client: &Arc<Client>, node: &NodeRef<'_>) { let mut attrs = node.attrs(); let from = attrs.jid("from").to_non_ad(); let Some(dm_node) = node.get_optional_child("disappearing_mode") else { warn!( "disappearing_mode notification missing <disappearing_mode> child: {}", - wacore::xml::DisplayableNode(node) + wacore::xml::DisplayableNodeRef(node) ); return; }; @@ -1388,7 +1400,7 @@ fn handle_disappearing_mode_notification(client: &Arc<Client>, node: &Node) { else { warn!( "disappearing_mode notification missing or invalid 't' attribute: {}", - wacore::xml::DisplayableNode(node) + wacore::xml::DisplayableNodeRef(node) ); return; }; @@ -1417,8 +1429,13 @@ mod tests { use std::sync::{Arc, Mutex}; use wacore::stanza::devices::DeviceNotificationType; use wacore::types::events::{DeviceListUpdateType, EventHandler}; + use wacore_binary::Node; use wacore_binary::builder::NodeBuilder; + fn node_to_arc(node: Node) -> Arc<OwnedNodeRef> { + crate::test_utils::node_to_owned_ref(&node) + } + #[derive(Default)] struct TestEventCollector { events: Mutex<Vec<Event>>, @@ -1461,7 +1478,7 @@ mod tests { .build()]) .build(); - let parsed = DeviceNotification::try_parse(&node).unwrap(); + let parsed = DeviceNotification::try_parse(&node.as_node_ref()).unwrap(); assert_eq!(parsed.operation.operation_type, DeviceNotificationType::Add); assert_eq!(parsed.operation.device_ids(), vec![1]); // Verify key index info @@ -1486,7 +1503,7 @@ mod tests { .build()]) .build(); - let parsed = DeviceNotification::try_parse(&node).unwrap(); + let parsed = DeviceNotification::try_parse(&node.as_node_ref()).unwrap(); assert_eq!( parsed.operation.operation_type, DeviceNotificationType::Remove @@ -1504,7 +1521,7 @@ mod tests { .build()]) .build(); - let parsed = DeviceNotification::try_parse(&node).unwrap(); + let parsed = DeviceNotification::try_parse(&node.as_node_ref()).unwrap(); assert_eq!( parsed.operation.operation_type, DeviceNotificationType::Update @@ -1525,7 +1542,7 @@ mod tests { .attr("from", "1234567890@s.whatsapp.net") .build(); - let result = DeviceNotification::try_parse(&node); + let result = DeviceNotification::try_parse(&node.as_node_ref()); assert!(result.is_err()); assert!( result @@ -1566,7 +1583,7 @@ mod tests { ]) .build(); - let parsed = DeviceNotification::try_parse(&node).unwrap(); + let parsed = DeviceNotification::try_parse(&node.as_node_ref()).unwrap(); // Should process remove, not add (priority: remove > add > update) assert_eq!( parsed.operation.operation_type, @@ -1608,7 +1625,7 @@ mod tests { ]) .build(); - let devices = parse_account_sync_device_list(&devices_node); + let devices = parse_account_sync_device_list(&devices_node.as_node_ref()); assert_eq!(devices.len(), 2); // Primary device (device 0) @@ -1642,7 +1659,7 @@ mod tests { ]) .build(); - let devices = parse_account_sync_device_list(&devices_node); + let devices = parse_account_sync_device_list(&devices_node.as_node_ref()); // Should only parse <device> tags, not <key-index-list> assert_eq!(devices.len(), 2); assert_eq!(devices[0].jid.device, 0); @@ -1656,7 +1673,7 @@ mod tests { .attr("dhash", "2:FnEWjS13") .build(); - let devices = parse_account_sync_device_list(&devices_node); + let devices = parse_account_sync_device_list(&devices_node.as_node_ref()); assert!(devices.is_empty()); } @@ -1683,7 +1700,7 @@ mod tests { ]) .build(); - let devices = parse_account_sync_device_list(&devices_node); + let devices = parse_account_sync_device_list(&devices_node.as_node_ref()); assert_eq!(devices.len(), 4); // Verify device IDs are correctly parsed @@ -1809,7 +1826,7 @@ mod tests { .build()]) .build(); - handle_notification_impl(&client, &node).await; + handle_notification_impl(&client, node_to_arc(node)).await; let events = collector.events(); assert!(matches!( @@ -1839,7 +1856,7 @@ mod tests { .build()]) .build(); - handle_notification_impl(&client, &node).await; + handle_notification_impl(&client, node_to_arc(node)).await; // Both LID-PN mappings should be created assert_eq!( @@ -1888,7 +1905,7 @@ mod tests { .build()]) .build(); - handle_notification_impl(&client, &node).await; + handle_notification_impl(&client, node_to_arc(node)).await; // Event should still be dispatched, just without LID info assert_eq!(collector.events().len(), 1); @@ -1907,7 +1924,7 @@ mod tests { .children([NodeBuilder::new("sync").attr("after", "1773519041").build()]) .build(); - handle_notification_impl(&client, &node).await; + handle_notification_impl(&client, node_to_arc(node)).await; let events = collector.events(); assert!(matches!( @@ -1930,7 +1947,7 @@ mod tests { .attr("id", format!("contacts-{tag}-1")) .children([NodeBuilder::new(tag).build()]) .build(); - handle_notification_impl(&client, &node).await; + handle_notification_impl(&client, node_to_arc(node)).await; } assert!( @@ -1951,7 +1968,7 @@ mod tests { .attr("from", "s.whatsapp.net") .attr("id", "contacts-empty-1") .build(); - handle_notification_impl(&client, &node).await; + handle_notification_impl(&client, node_to_arc(node)).await; assert!( collector.events().is_empty(), @@ -1974,7 +1991,7 @@ mod tests { .attr("t", "1773668072") .children([NodeBuilder::new("update").attr("hash", "Quvc").build()]) .build(); - handle_notification_impl(&client, &node).await; + handle_notification_impl(&client, node_to_arc(node)).await; assert!( collector.events().is_empty(), @@ -2011,7 +2028,7 @@ mod tests { .attr("id", "identity-change-1") .children([NodeBuilder::new("identity").build()]) .build(); - handle_notification_impl(&client, &node).await; + handle_notification_impl(&client, node_to_arc(node)).await; // Should have dispatched IdentityChange event let events = collector.events(); @@ -2053,7 +2070,7 @@ mod tests { .attr("id", "identity-change-self") .children([NodeBuilder::new("identity").build()]) .build(); - handle_notification_impl(&client, &node).await; + handle_notification_impl(&client, node_to_arc(node)).await; assert!( collector.events().is_empty(), @@ -2073,7 +2090,7 @@ mod tests { .attr("id", "identity-change-2") .children([NodeBuilder::new("identity").build()]) .build(); - handle_notification_impl(&client, &node).await; + handle_notification_impl(&client, node_to_arc(node)).await; assert!( collector.events().is_empty(), @@ -2104,7 +2121,7 @@ mod tests { .attr("id", "identity-change-3") .children([NodeBuilder::new("identity").build()]) .build(); - handle_notification_impl(&client, &node).await; + handle_notification_impl(&client, node_to_arc(node)).await; let backend = client.persistence_manager.backend(); let has_session = client @@ -2153,7 +2170,7 @@ mod tests { .attr("id", "identity-change-4") .children([NodeBuilder::new("identity").build()]) .build(); - handle_notification_impl(&client, &node).await; + handle_notification_impl(&client, node_to_arc(node)).await; let backend = client.persistence_manager.backend(); let sk = client @@ -2181,7 +2198,7 @@ mod tests { .attr("offline", "1") .children([NodeBuilder::new("identity").build()]) .build(); - handle_notification_impl(&client, &node).await; + handle_notification_impl(&client, node_to_arc(node)).await; assert!( collector diff --git a/src/handlers/presence.rs b/src/handlers/presence.rs index cbddf6bf4..54ca5bc28 100644 --- a/src/handlers/presence.rs +++ b/src/handlers/presence.rs @@ -6,7 +6,6 @@ use async_trait::async_trait; use log::debug; use std::sync::Arc; use wacore::types::events::{Event, PresenceUpdate}; -use wacore_binary::node::Node; /// Handler for `<presence>` stanzas. /// @@ -21,8 +20,14 @@ impl StanzaHandler for PresenceHandler { "presence" } - async fn handle(&self, client: Arc<Client>, node: Arc<Node>, _cancelled: &mut bool) -> bool { - let from_jid = match node.attrs.get("from").and_then(|v| v.to_jid()) { + async fn handle( + &self, + client: Arc<Client>, + node: Arc<wacore_binary::OwnedNodeRef>, + _cancelled: &mut bool, + ) -> bool { + let nr = node.get(); + let from_jid = match nr.get_attr("from").and_then(|v| v.to_jid()) { Some(jid) => jid, None => { debug!(target: "PresenceHandler", "Presence stanza missing or invalid 'from' attribute"); @@ -30,13 +35,15 @@ impl StanzaHandler for PresenceHandler { } }; - let unavailable = node.attrs.get("type").is_some_and(|v| v == "unavailable"); + let unavailable = nr + .get_attr("type") + .is_some_and(|v| v.as_str() == "unavailable"); // Parse last_seen from 'last' attribute if present - let last_seen = node - .attrs - .get("last") - .and_then(|v| v.as_str().parse::<i64>().ok()) + let last_seen = nr + .get_attr("last") + .map(|v| v.as_str()) + .and_then(|s| s.parse::<i64>().ok()) .and_then(|ts| chrono::DateTime::from_timestamp(ts, 0)); debug!( diff --git a/src/handlers/receipt.rs b/src/handlers/receipt.rs index d3f2a7280..9a59afd2f 100644 --- a/src/handlers/receipt.rs +++ b/src/handlers/receipt.rs @@ -2,7 +2,6 @@ use super::traits::StanzaHandler; use crate::client::Client; use async_trait::async_trait; use std::sync::Arc; -use wacore_binary::node::Node; /// Handler for `<receipt>` stanzas. /// @@ -20,7 +19,12 @@ impl StanzaHandler for ReceiptHandler { "receipt" } - async fn handle(&self, client: Arc<Client>, node: Arc<Node>, _cancelled: &mut bool) -> bool { + async fn handle( + &self, + client: Arc<Client>, + node: Arc<wacore_binary::OwnedNodeRef>, + _cancelled: &mut bool, + ) -> bool { client.handle_receipt(node).await; true } diff --git a/src/handlers/router.rs b/src/handlers/router.rs index 886a9e0ad..ef85b7345 100644 --- a/src/handlers/router.rs +++ b/src/handlers/router.rs @@ -2,8 +2,6 @@ use super::traits::StanzaHandler; use crate::client::Client; use std::collections::HashMap; use std::sync::Arc; -use wacore_binary::node::Node; - /// Central router for dispatching XML stanzas to their appropriate handlers. /// /// The router maintains a registry of handlers keyed by XML tag and efficiently @@ -49,10 +47,10 @@ impl StanzaRouter { pub async fn dispatch( &self, client: Arc<Client>, - node: Arc<Node>, + node: Arc<wacore_binary::OwnedNodeRef>, cancelled: &mut bool, ) -> bool { - if let Some(handler) = self.handlers.get(node.tag.as_ref()) { + if let Some(handler) = self.handlers.get(node.tag()) { handler.handle(client, node, cancelled).await } else { false @@ -76,7 +74,8 @@ mod tests { use super::*; use crate::test_utils::MockHttpClient; use std::sync::Arc; - use wacore_binary::node::{Attrs, Node, NodeContent}; + use wacore_binary::OwnedNodeRef; + use wacore_binary::{Attrs, Node, NodeContent}; #[derive(Debug)] struct MockHandler { @@ -106,7 +105,7 @@ mod tests { async fn handle( &self, _client: Arc<crate::client::Client>, - _node: Arc<Node>, + _node: Arc<OwnedNodeRef>, _cancelled: &mut bool, ) -> bool { self.handled @@ -115,6 +114,8 @@ mod tests { } } + use crate::test_utils::node_to_owned_ref; + #[test] fn test_router_registration() { let mut router = StanzaRouter::new(); @@ -143,16 +144,11 @@ mod tests { router.register(handler); - // Create owned Node wrapped in Arc let mut attrs = Attrs::new(); attrs.insert("id".to_string(), "test-id".to_string()); - let node = Arc::new(Node::new( - "test", - attrs, - Some(NodeContent::String("test".into())), - )); + let raw_node = Node::new("test", attrs, Some(NodeContent::String("test".into()))); + let node = node_to_owned_ref(&raw_node); - // Create a minimal client for testing with an in-memory database use crate::store::persistence_manager::PersistenceManager; let backend = crate::test_utils::create_test_backend().await; @@ -181,16 +177,11 @@ mod tests { async fn test_router_dispatch_not_found() { let router = StanzaRouter::new(); - // Create owned Node wrapped in Arc let mut attrs = Attrs::new(); attrs.insert("id".to_string(), "test-id".to_string()); - let node = Arc::new(Node::new( - "unknown", - attrs, - Some(NodeContent::String("test".into())), - )); + let raw_node = Node::new("unknown", attrs, Some(NodeContent::String("test".into()))); + let node = node_to_owned_ref(&raw_node); - // Create a minimal client for testing with an in-memory database use crate::store::persistence_manager::PersistenceManager; let backend = crate::test_utils::create_test_backend().await; diff --git a/src/handlers/traits.rs b/src/handlers/traits.rs index 5c64fe64b..f147ddca1 100644 --- a/src/handlers/traits.rs +++ b/src/handlers/traits.rs @@ -1,13 +1,11 @@ use crate::client::Client; use async_trait::async_trait; use std::sync::Arc; -use wacore_binary::node::Node; +use wacore_binary::OwnedNodeRef; /// Trait for handling specific types of XML stanzas received from the WhatsApp server. /// /// Each handler is responsible for processing a specific top-level XML tag (e.g., "message", "iq", "receipt"). -/// This pattern allows for better separation of concerns and makes it easier to add new stanza types -/// without modifying the core client dispatch logic. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait StanzaHandler: Send + Sync { @@ -18,11 +16,16 @@ pub trait StanzaHandler: Send + Sync { /// /// # Arguments /// * `client` - Arc reference to the client instance - /// * `node` - Arc-wrapped owned Node (avoids cloning - handlers can share or store cheaply) + /// * `node` - Arc-wrapped OwnedNodeRef (zero-copy, handlers share cheaply via Arc) /// * `cancelled` - If set to `true`, prevents the deferred ack from being sent /// /// # Returns /// Returns `true` if the node was successfully handled, `false` if it should be /// processed by other handlers or logged as unhandled. - async fn handle(&self, client: Arc<Client>, node: Arc<Node>, cancelled: &mut bool) -> bool; + async fn handle( + &self, + client: Arc<Client>, + node: Arc<OwnedNodeRef>, + cancelled: &mut bool, + ) -> bool; } diff --git a/src/handlers/unimplemented.rs b/src/handlers/unimplemented.rs index 6e4c03aba..213bc4e11 100644 --- a/src/handlers/unimplemented.rs +++ b/src/handlers/unimplemented.rs @@ -2,7 +2,6 @@ use super::traits::StanzaHandler; use crate::client::Client; use async_trait::async_trait; use std::sync::Arc; -use wacore_binary::node::Node; /// Handler for stanza types that are not yet fully implemented. /// @@ -43,8 +42,13 @@ impl StanzaHandler for UnimplementedHandler { } } - async fn handle(&self, client: Arc<Client>, node: Arc<Node>, _cancelled: &mut bool) -> bool { - client.handle_unimplemented(&node.tag).await; + async fn handle( + &self, + client: Arc<Client>, + node: Arc<wacore_binary::OwnedNodeRef>, + _cancelled: &mut bool, + ) -> bool { + client.handle_unimplemented(node.tag()).await; true } } diff --git a/src/history_sync.rs b/src/history_sync.rs index 659ca6527..164ff78dc 100644 --- a/src/history_sync.rs +++ b/src/history_sync.rs @@ -3,7 +3,7 @@ use bytes::Bytes; use std::sync::Arc; use wacore::history_sync::process_history_sync; use wacore::store::traits::TcTokenEntry; -use wacore_binary::jid::JidExt; +use wacore_binary::JidExt; use waproto::whatsapp::message::HistorySyncNotification; use crate::client::Client; @@ -338,7 +338,7 @@ impl Client { }; // Resolve to LID for storage key consistency with notification handler - let jid: wacore_binary::jid::Jid = match conv.id.parse() { + let jid: wacore_binary::Jid = match conv.id.parse() { Ok(j) => j, Err(_) => return, }; diff --git a/src/jid_utils.rs b/src/jid_utils.rs index af1eb9118..54904763a 100644 --- a/src/jid_utils.rs +++ b/src/jid_utils.rs @@ -1,5 +1,5 @@ use std::sync::OnceLock; -use wacore_binary::jid::{Jid, SERVER_JID}; +use wacore_binary::{Jid, SERVER_JID}; static SERVER_JID_CACHE: OnceLock<Jid> = OnceLock::new(); diff --git a/src/keepalive.rs b/src/keepalive.rs index 575f6a7cc..944e5d58f 100644 --- a/src/keepalive.rs +++ b/src/keepalive.rs @@ -72,7 +72,7 @@ impl Client { debug!(target: "Client/Keepalive", "Received keepalive pong (RTT: {rtt_ms}ms)"); // WA Web: onClockSkewUpdate — Math.round((startTime + rtt/2) / 1000 - serverTime) self.unified_session.update_server_time_offset_with_rtt( - &response_node, + response_node.get(), start_ms, rtt_ms, ); diff --git a/src/lib.rs b/src/lib.rs index c3578787d..d7a49ac24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,8 +2,9 @@ pub use wacore::{ iq::privacy as privacy_settings, proto_helpers, sticker_pack, store::traits, webp, }; pub use wacore_binary::CompactString; +pub use wacore_binary::OwnedNodeRef; pub use wacore_binary::builder::NodeBuilder; -pub use wacore_binary::jid::Jid; +pub use wacore_binary::{Jid, Server}; pub use waproto; pub mod cache; diff --git a/src/message.rs b/src/message.rs index 738ccdc79..09431a517 100644 --- a/src/message.rs +++ b/src/message.rs @@ -18,9 +18,9 @@ use wacore::libsignal::protocol::{ use wacore::libsignal::store::sender_key_name::SenderKeyName; use wacore::message_processing::EncType; use wacore::types::jid::JidExt; -use wacore_binary::jid::Jid; -use wacore_binary::jid::JidExt as _; -use wacore_binary::node::Node; +use wacore_binary::Jid; +use wacore_binary::JidExt as _; +use wacore_binary::{NodeRef, OwnedNodeRef}; use waproto::whatsapp::{self as wa}; /// Maximum retry attempts per message (matches WhatsApp Web's MAX_RETRY = 5). @@ -59,7 +59,7 @@ impl Client { /// Handles a newsletter plaintext message. /// Newsletters are not E2E encrypted and use the <plaintext> tag directly. - async fn handle_newsletter_message(self: &Arc<Self>, node: &Node, info: &MessageInfo) { + async fn handle_newsletter_message(self: &Arc<Self>, node: &NodeRef<'_>, info: &MessageInfo) { let Some(plaintext_node) = node.get_optional_child_by_tag(&["plaintext"]) else { log::warn!( "[msg:{}] Received newsletter message without <plaintext> child: {}", @@ -69,8 +69,8 @@ impl Client { return; }; - if let Some(wacore_binary::node::NodeContent::Bytes(bytes)) = &plaintext_node.content { - match wa::Message::decode(bytes.as_slice()) { + if let Some(bytes) = plaintext_node.content_bytes() { + match wa::Message::decode(bytes) { Ok(msg) => { log::info!( "[msg:{}] Received newsletter plaintext message from {}", @@ -252,12 +252,13 @@ impl Client { })).detach(); } - pub(crate) async fn handle_incoming_message(self: Arc<Self>, node: Arc<Node>) { - let info = match self.parse_message_info(&node).await { + pub(crate) async fn handle_incoming_message(self: Arc<Self>, node: Arc<OwnedNodeRef>) { + let nr = node.get(); + let info = match self.parse_message_info(nr).await { Ok(info) => Arc::new(info), Err(e) => { - let id = node.attrs.get("id").map(|v| v.as_str()); - let from = node.attrs.get("from").map(|v| v.as_str()); + let id = nr.get_attr("id").map(|v| v.as_str()); + let from = nr.get_attr("from").map(|v| v.as_str()); log::warn!("Failed to parse message info (id={id:?}, from={from:?}): {e:?}"); return; } @@ -265,7 +266,7 @@ impl Client { // Newsletters use <plaintext> instead of <enc> because they are not E2E encrypted. if info.source.chat.is_newsletter() { - self.handle_newsletter_message(&node, &info).await; + self.handle_newsletter_message(nr, &info).await; return; } @@ -274,14 +275,14 @@ impl Client { .await; let sender_encryption_jid = self.resolve_encryption_jid(&info.source.sender).await; - let unavailable_node = node.get_optional_child("unavailable"); + let unavailable_node = nr.get_optional_child("unavailable"); - let mut all_enc_nodes = Vec::new(); + let mut all_enc_nodes: Vec<&NodeRef<'_>> = Vec::new(); - let direct_enc_nodes = node.get_children_by_tag("enc"); + let direct_enc_nodes = nr.get_children_by_tag("enc"); all_enc_nodes.extend(direct_enc_nodes); - let participants = node.get_optional_child_by_tag(&["participants"]); + let participants = nr.get_optional_child_by_tag(&["participants"]); if let Some(participants_node) = participants { let own_jid = self.get_pn().await; let to_nodes = participants_node.get_children_by_tag("to"); @@ -301,17 +302,17 @@ impl Client { log::warn!( "[msg:{}] Received non-newsletter message without <enc> child: {}", info.id, - node.tag + nr.tag ); return; } if let Some(unavailable) = unavailable_node { - let unavailable_type = - match unavailable.attrs.get("type").map(|v| v.as_str()).as_deref() { - Some("view_once") => crate::types::events::UnavailableType::ViewOnce, - _ => crate::types::events::UnavailableType::Unknown, - }; + let unavailable_type = match unavailable.get_attr("type").map(|v| v.as_str()).as_deref() + { + Some("view_once") => crate::types::events::UnavailableType::ViewOnce, + _ => crate::types::events::UnavailableType::Unknown, + }; log::warn!( "[msg:{}] Message has <unavailable> child (type: {:?}), requesting from phone via PDO", info.id, @@ -335,7 +336,7 @@ impl Client { let mut max_sender_retry_count: u8 = 0; let mut has_hide_fail = false; - for &enc_node in &all_enc_nodes { + for enc_node in &all_enc_nodes { // Parse sender retry count (WA Web: e.maybeAttrInt("count") ?? 0) // Clamp to MAX_DECRYPT_RETRIES to prevent u64→u8 truncation on unexpected values. let sender_count = enc_node @@ -347,9 +348,9 @@ impl Client { // Parse decrypt-fail attribute (WA Web: e.maybeAttrString("decrypt-fail") === "hide") if enc_node - .attrs - .get("decrypt-fail") - .is_some_and(|v| v == "hide") + .get_attr("decrypt-fail") + .map(|v| v.as_str()) + .is_some_and(|s| s == "hide") { has_hide_fail = true; } @@ -372,13 +373,14 @@ impl Client { let handler_clone = handler; let client_clone = self.clone(); let info_arc = Arc::clone(&info); - let enc_node_clone = Arc::new(enc_node.clone()); + // Custom enc handlers take &Node (public API); convert from NodeRef here. + let enc_node_owned = (*enc_node).to_owned(); let enc_type_owned = enc_type.to_string(); self.runtime .spawn(Box::pin(async move { if let Err(e) = handler_clone - .handle(client_clone, &enc_node_clone, &info_arc) + .handle(client_clone, &enc_node_owned, &info_arc) .await { log::warn!( @@ -393,8 +395,8 @@ impl Client { // Fall back to built-in handlers match EncType::from_wire(enc_type.as_ref()) { - Some(et) if et.is_session() => session_enc_nodes.push(enc_node), - Some(EncType::SenderKey) => group_content_enc_nodes.push(enc_node), + Some(et) if et.is_session() => session_enc_nodes.push(*enc_node), + Some(EncType::SenderKey) => group_content_enc_nodes.push(*enc_node), _ => log::warn!("Unknown enc type: {enc_type}"), } } @@ -405,9 +407,9 @@ impl Client { if !session_enc_nodes.is_empty() && !group_content_enc_nodes.is_empty() && all_enc_nodes.first().is_some_and(|n| { - n.attrs - .get("type") - .is_some_and(|v| v == EncType::SenderKey.as_wire_str()) + n.get_attr("type") + .map(|v| v.as_str()) + .is_some_and(|s| s == EncType::SenderKey.as_wire_str()) }) { log::error!( @@ -598,9 +600,9 @@ impl Client { .await; } - async fn process_session_enc_batch( + async fn process_session_enc_batch<'n>( self: Arc<Self>, - enc_nodes: &[&wacore_binary::node::Node], + enc_nodes: &[&'n NodeRef<'n>], info: &MessageInfo, sender_encryption_jid: &Jid, decrypt_fail_mode: crate::types::events::DecryptFailMode, @@ -627,9 +629,9 @@ impl Client { let mut dispatched_undecryptable = false; for enc_node in enc_nodes { - let ciphertext: &[u8] = match &enc_node.content { - Some(wacore_binary::node::NodeContent::Bytes(b)) => b, - _ => { + let ciphertext: &[u8] = match enc_node.content_bytes() { + Some(b) => b, + None => { log::warn!("Enc node has no byte content (batch session)"); continue; } @@ -988,9 +990,9 @@ impl Client { (any_success, any_duplicate, dispatched_undecryptable) } - async fn process_group_enc_batch( + async fn process_group_enc_batch<'n>( self: Arc<Self>, - enc_nodes: &[&wacore_binary::node::Node], + enc_nodes: &[&'n NodeRef<'n>], info: &MessageInfo, _sender_encryption_jid: &Jid, decrypt_fail_mode: crate::types::events::DecryptFailMode, @@ -1003,9 +1005,9 @@ impl Client { let mut adapter = self.signal_adapter().await; for enc_node in enc_nodes { - let ciphertext: &[u8] = match &enc_node.content { - Some(wacore_binary::node::NodeContent::Bytes(b)) => b, - _ => { + let ciphertext: &[u8] = match enc_node.content_bytes() { + Some(b) => b, + None => { log::warn!("Enc node has no byte content (batch group)"); continue; } @@ -1304,12 +1306,9 @@ impl Client { /// Cache LID-PN mapping from message attributes (before resolve_encryption_jid). async fn cache_lid_pn_from_message(&self, sender: &Jid, alt: Option<&Jid>) { - let pn_server = wacore_binary::jid::DEFAULT_USER_SERVER; - let lid_server = wacore_binary::jid::HIDDEN_USER_SERVER; - - let (lid_user, pn_user, source) = if sender.server == lid_server { + let (lid_user, pn_user, source) = if sender.is_lid() { if let Some(alt_jid) = alt - && alt_jid.server == pn_server + && alt_jid.is_pn() { ( &sender.user, @@ -1319,9 +1318,9 @@ impl Client { } else { return; } - } else if sender.server == pn_server { + } else if sender.is_pn() { if let Some(alt_jid) = alt - && alt_jid.server == lid_server + && alt_jid.is_lid() { ( &alt_jid.user, @@ -1345,7 +1344,7 @@ impl Client { pub(crate) async fn parse_message_info( &self, - node: &Node, + node: &wacore_binary::NodeRef<'_>, ) -> Result<MessageInfo, anyhow::Error> { let device_snapshot = self.persistence_manager.get_device_snapshot().await; let default_jid = Jid::default(); @@ -1569,7 +1568,11 @@ mod tests { use crate::types::message::EditAttribute; use std::sync::Arc; use wacore_binary::builder::NodeBuilder; - use wacore_binary::jid::{Jid, SERVER_JID}; + + fn node_to_arc(node: wacore_binary::Node) -> Arc<OwnedNodeRef> { + crate::test_utils::node_to_owned_ref(&node) + } + use wacore_binary::{Jid, SERVER_JID}; fn mock_transport() -> Arc<dyn crate::transport::TransportFactory> { Arc::new(crate::transport::mock::MockTransportFactory::new()) @@ -1612,7 +1615,7 @@ mod tests { .build(); let info = client - .parse_message_info(&node) + .parse_message_info(&node.as_node_ref()) .await .expect("parse_message_info should not fail"); @@ -1696,7 +1699,8 @@ mod tests { .attr("type", "msg") .bytes(signal_message.serialized().to_vec()) .build(); - let enc_nodes = vec![&enc_node]; + let enc_node_ref = enc_node.as_node_ref(); + let enc_nodes = vec![&enc_node_ref]; // With SessionNotFound, should return (false, false, true) - no success, no dupe, dispatched event let (success, had_duplicates, dispatched) = client @@ -1784,7 +1788,8 @@ mod tests { .attr("type", "msg") .bytes(signal_message.serialized().to_vec()) .build(); - let enc_nodes = vec![&enc_node]; + let enc_node_ref = enc_node.as_node_ref(); + let enc_nodes = vec![&enc_node_ref]; let (success, had_duplicates, dispatched) = client .clone() @@ -1884,7 +1889,7 @@ mod tests { .bytes(vec![4, 5, 6]) .build(); - let message_node = Arc::new( + let message_node = node_to_arc( NodeBuilder::new("message") .attr("from", group_jid) .attr("participant", sender_jid) @@ -2129,7 +2134,7 @@ mod tests { /// - LID without device numbers #[test] fn test_lid_jid_parsing_edge_cases() { - use wacore_binary::jid::Jid; + use wacore_binary::Jid; // Single dot in user portion let lid1: Jid = "100000000000001.1:75@lid" @@ -2172,7 +2177,7 @@ mod tests { #[test] fn test_lid_protocol_address_consistency() { use wacore::types::jid::JidExt as CoreJidExt; - use wacore_binary::jid::Jid; + use wacore_binary::Jid; // Format: (jid_str, expected_name, expected_device_id, expected_to_string) let test_cases = vec![ @@ -2283,7 +2288,7 @@ mod tests { .build(); let info1 = client - .parse_message_info(&lid_group_node) + .parse_message_info(&lid_group_node.as_node_ref()) .await .expect("parse_message_info should succeed"); assert_eq!(info1.source.sender.user, "987654321000000.2"); @@ -2309,7 +2314,7 @@ mod tests { .build(); let info2 = client - .parse_message_info(&self_lid_node) + .parse_message_info(&self_lid_node.as_node_ref()) .await .expect("parse_message_info should succeed"); assert!( @@ -2338,7 +2343,7 @@ mod tests { use std::collections::HashMap; use wacore::client::context::GroupInfo; use wacore::types::message::AddressingMode; - use wacore_binary::jid::Jid; + use wacore_binary::Jid; // Simulate a LID group with phone number mappings let mut lid_to_pn_map = HashMap::new(); @@ -2406,7 +2411,7 @@ mod tests { use std::collections::HashMap; use wacore::client::context::GroupInfo; use wacore::types::message::AddressingMode; - use wacore_binary::jid::Jid; + use wacore_binary::Jid; let mut lid_to_pn_map = HashMap::new(); lid_to_pn_map.insert( @@ -2457,7 +2462,7 @@ mod tests { #[test] fn test_own_jid_check_in_lid_mode() { use std::collections::HashMap; - use wacore_binary::jid::Jid; + use wacore_binary::Jid; let own_lid: Jid = "100000000000001.1@lid" .parse() @@ -2656,7 +2661,7 @@ mod tests { .bytes(skmsg_ciphertext) .build(); - let message_node = Arc::new( + let message_node = node_to_arc( NodeBuilder::new("message") .attr("from", group_jid) .attr("participant", sender_jid) @@ -2738,7 +2743,8 @@ mod tests { .bytes(vec![0xFF; 100]) // Invalid encrypted payload .build(); - let enc_nodes = vec![&enc_node]; + let enc_node_ref = enc_node.as_node_ref(); + let enc_nodes = vec![&enc_node_ref]; // Call process_session_enc_batch // This should handle any errors gracefully without panicking @@ -2824,7 +2830,8 @@ mod tests { log::info!("Test: Created batch of 2 messages with invalid data"); - let enc_node_refs: Vec<&wacore_binary::node::Node> = enc_nodes.iter().collect(); + let enc_node_refs_owned: Vec<_> = enc_nodes.iter().map(|n| n.as_node_ref()).collect(); + let enc_node_refs: Vec<&NodeRef<'_>> = enc_node_refs_owned.iter().collect(); // Process the batch // Should handle all errors gracefully without stopping at first error @@ -2895,7 +2902,8 @@ mod tests { .bytes(vec![0xFF; 100]) .build(); - let enc_nodes = vec![&enc_node]; + let enc_node_ref = enc_node.as_node_ref(); + let enc_nodes = vec![&enc_node_ref]; // Process the message // Should handle errors gracefully in group context @@ -2978,7 +2986,7 @@ mod tests { .build(); let info = client - .parse_message_info(&self_dm_node) + .parse_message_info(&self_dm_node.as_node_ref()) .await .expect("parse_message_info should succeed"); @@ -3075,7 +3083,7 @@ mod tests { .build(); let info = client - .parse_message_info(&other_dm_node) + .parse_message_info(&other_dm_node.as_node_ref()) .await .expect("parse_message_info should succeed"); @@ -3171,7 +3179,7 @@ mod tests { .build(); let info = client - .parse_message_info(&self_chat_node) + .parse_message_info(&self_chat_node.as_node_ref()) .await .expect("parse_message_info should succeed"); @@ -3262,7 +3270,7 @@ mod tests { // but it should still populate the cache before attempting decryption client .clone() - .handle_incoming_message(Arc::new(dm_node)) + .handle_incoming_message(node_to_arc(dm_node)) .await; // Verify the cache was populated @@ -3322,7 +3330,7 @@ mod tests { // Call handle_incoming_message client .clone() - .handle_incoming_message(Arc::new(dm_node)) + .handle_incoming_message(node_to_arc(dm_node)) .await; assert!( @@ -3384,7 +3392,7 @@ mod tests { // Call handle_incoming_message client .clone() - .handle_incoming_message(Arc::new(group_node)) + .handle_incoming_message(node_to_arc(group_node)) .await; // Verify the cache WAS populated (bidirectional cache) @@ -3454,7 +3462,7 @@ mod tests { client .clone() - .handle_incoming_message(Arc::new(dm_node)) + .handle_incoming_message(node_to_arc(dm_node)) .await; } @@ -3540,13 +3548,13 @@ mod tests { .build(); let info = client - .parse_message_info(&dm_node_with_sender_lid) + .parse_message_info(&dm_node_with_sender_lid.as_node_ref()) .await .expect("parse_message_info should succeed"); // Verify sender is PN but sender_alt is LID assert_eq!(info.source.sender.user, phone); - assert_eq!(info.source.sender.server, "s.whatsapp.net"); + assert_eq!(info.source.sender.server, wacore_binary::Server::Pn); assert!(info.source.sender_alt.is_some()); assert_eq!( info.source @@ -3562,27 +3570,24 @@ mod tests { .as_ref() .expect("sender_alt should be present") .server, - "lid" + wacore_binary::Server::Lid ); // Now simulate what handle_incoming_message does: determine encryption JID // We can't easily call handle_incoming_message, so we'll test the logic directly let sender = &info.source.sender; let alt = info.source.sender_alt.as_ref(); - let pn_server = wacore_binary::jid::DEFAULT_USER_SERVER; - let lid_server = wacore_binary::jid::HIDDEN_USER_SERVER; - // Apply the same logic as in handle_incoming_message - let sender_encryption_jid = if sender.server == lid_server { + let sender_encryption_jid = if sender.is_lid() { sender.clone() - } else if sender.server == pn_server { + } else if sender.is_pn() { if let Some(alt_jid) = alt - && alt_jid.server == lid_server + && alt_jid.is_lid() { // Use the LID from the message attribute Jid { user: alt_jid.user.clone(), - server: wacore_binary::jid::cow_server_from_str(lid_server), + server: wacore_binary::Server::Lid, device: sender.device, agent: sender.agent, integrator: sender.integrator, @@ -3591,7 +3596,7 @@ mod tests { // Use the cached LID Jid { user: lid_user.into(), - server: wacore_binary::jid::cow_server_from_str(lid_server), + server: wacore_binary::Server::Lid, device: sender.device, agent: sender.agent, integrator: sender.integrator, @@ -3609,7 +3614,8 @@ mod tests { "Encryption JID should use LID user" ); assert_eq!( - sender_encryption_jid.server, "lid", + sender_encryption_jid.server, + wacore_binary::Server::Lid, "Encryption JID should use LID server" ); @@ -3673,13 +3679,13 @@ mod tests { .build(); let info = client - .parse_message_info(&dm_node_without_sender_lid) + .parse_message_info(&dm_node_without_sender_lid.as_node_ref()) .await .expect("parse_message_info should succeed"); // Verify sender is PN and NO sender_alt (since there's no sender_lid attribute) assert_eq!(info.source.sender.user, phone); - assert_eq!(info.source.sender.server, "s.whatsapp.net"); + assert_eq!(info.source.sender.server, wacore_binary::Server::Pn); assert!( info.source.sender_alt.is_none(), "Should have no sender_alt without sender_lid attribute" @@ -3688,18 +3694,15 @@ mod tests { // Apply the encryption JID logic (fallback to cached LID) let sender = &info.source.sender; let alt = info.source.sender_alt.as_ref(); - let pn_server = wacore_binary::jid::DEFAULT_USER_SERVER; - let lid_server = wacore_binary::jid::HIDDEN_USER_SERVER; - - let sender_encryption_jid = if sender.server == lid_server { + let sender_encryption_jid = if sender.is_lid() { sender.clone() - } else if sender.server == pn_server { + } else if sender.is_pn() { if let Some(alt_jid) = alt - && alt_jid.server == lid_server + && alt_jid.is_lid() { Jid { user: alt_jid.user.clone(), - server: wacore_binary::jid::cow_server_from_str(lid_server), + server: wacore_binary::Server::Lid, device: sender.device, agent: sender.agent, integrator: sender.integrator, @@ -3708,7 +3711,7 @@ mod tests { // This is the path we're testing - fallback to cached LID Jid { user: lid_user.into(), - server: wacore_binary::jid::cow_server_from_str(lid_server), + server: wacore_binary::Server::Lid, device: sender.device, agent: sender.agent, integrator: sender.integrator, @@ -3726,7 +3729,8 @@ mod tests { "Encryption JID should use cached LID user" ); assert_eq!( - sender_encryption_jid.server, "lid", + sender_encryption_jid.server, + wacore_binary::Server::Lid, "Encryption JID should use LID server" ); @@ -3780,7 +3784,7 @@ mod tests { .build(); let info = client - .parse_message_info(&dm_node) + .parse_message_info(&dm_node.as_node_ref()) .await .expect("parse_message_info should succeed"); @@ -3791,18 +3795,16 @@ mod tests { // Apply the encryption JID logic let sender = &info.source.sender; let alt = info.source.sender_alt.as_ref(); - let pn_server = wacore_binary::jid::DEFAULT_USER_SERVER; - let lid_server = wacore_binary::jid::HIDDEN_USER_SERVER; - let sender_encryption_jid = if sender.server == lid_server { + let sender_encryption_jid = if sender.is_lid() { sender.clone() - } else if sender.server == pn_server { + } else if sender.is_pn() { if let Some(alt_jid) = alt - && alt_jid.server == lid_server + && alt_jid.is_lid() { Jid { user: alt_jid.user.clone(), - server: wacore_binary::jid::cow_server_from_str(lid_server), + server: wacore_binary::Server::Lid, device: sender.device, agent: sender.agent, integrator: sender.integrator, @@ -3810,7 +3812,7 @@ mod tests { } else if let Some(lid_user) = client.lid_pn_cache.get_current_lid(&sender.user).await { Jid { user: lid_user.into(), - server: wacore_binary::jid::cow_server_from_str(lid_server), + server: wacore_binary::Server::Lid, device: sender.device, agent: sender.agent, integrator: sender.integrator, @@ -3829,7 +3831,8 @@ mod tests { "Encryption JID should use PN user when no LID mapping" ); assert_eq!( - sender_encryption_jid.server, "s.whatsapp.net", + sender_encryption_jid.server, + wacore_binary::Server::Pn, "Encryption JID should use PN server when no LID mapping" ); @@ -4277,7 +4280,7 @@ mod tests { /// Test: Verify JID type detection for status broadcasts, broadcast lists, groups, and users. #[test] fn test_status_broadcast_jid_detection() { - use wacore_binary::jid::{Jid, JidExt}; + use wacore_binary::{Jid, JidExt}; let status_jid: Jid = "status@broadcast".parse().expect("status JID should parse"); assert!(status_jid.is_status_broadcast()); @@ -4374,7 +4377,7 @@ mod tests { .attr("type", "text") .build(); - let result = client.parse_message_info(&node).await; + let result = client.parse_message_info(&node.as_node_ref()).await; assert!( result.is_err(), @@ -4396,8 +4399,8 @@ mod tests { use crate::store::SqliteStore; use crate::store::persistence_manager::PersistenceManager; + use wacore_binary::NodeContent; use wacore_binary::builder::NodeBuilder; - use wacore_binary::node::NodeContent; let backend = Arc::new( SqliteStore::new("file:memdb_retry_immediate?mode=memory&cache=shared") @@ -4441,7 +4444,10 @@ mod tests { }]) .build(); - client.clone().handle_incoming_message(Arc::new(node)).await; + client + .clone() + .handle_incoming_message(node_to_arc(node)) + .await; // spawn_retry_receipt runs in a spawned task, wait for it let retry_key = client @@ -4656,7 +4662,7 @@ mod tests { .build(); let info = client - .parse_message_info(&node) + .parse_message_info(&node.as_node_ref()) .await .expect("parse_message_info should succeed"); @@ -4681,7 +4687,7 @@ mod tests { .build(); let info = client - .parse_message_info(&node) + .parse_message_info(&node.as_node_ref()) .await .expect("parse_message_info should succeed"); @@ -4705,7 +4711,7 @@ mod tests { .build(); let info = client - .parse_message_info(&node) + .parse_message_info(&node.as_node_ref()) .await .expect("parse_message_info should succeed"); @@ -4728,7 +4734,7 @@ mod tests { .build(); let info = client - .parse_message_info(&node) + .parse_message_info(&node.as_node_ref()) .await .expect("parse_message_info should succeed"); diff --git a/src/pair.rs b/src/pair.rs index 25296a76e..a2e0488e6 100644 --- a/src/pair.rs +++ b/src/pair.rs @@ -7,8 +7,8 @@ use prost::Message; use std::sync::Arc; use std::sync::atomic::Ordering; use wacore::libsignal::protocol::KeyPair; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::NodeRef; +use wacore_binary::{Jid, SERVER_JID}; use waproto::whatsapp as wa; pub use wacore::pair::{DeviceState, PairCryptoError, PairUtils}; @@ -22,13 +22,11 @@ pub fn make_qr_data(store: &crate::store::Device, ref_str: String) -> String { PairUtils::make_qr_data(&device_state, ref_str) } -pub async fn handle_iq(client: &Arc<Client>, node: &Node) -> bool { +pub async fn handle_iq(client: &Arc<Client>, node: &NodeRef<'_>) -> bool { // Server JID is "s.whatsapp.net" (no @ prefix for server-only JIDs) - if !node - .attrs - .get("from") - .map(|from| from == SERVER_JID) - .unwrap_or(false) + if node + .get_attr("from") + .is_none_or(|v| v.as_str() != SERVER_JID) { return false; } @@ -37,7 +35,7 @@ pub async fn handle_iq(client: &Arc<Client>, node: &Node) -> bool { for child in children { let handled = match child.tag.as_ref() { "pair-device" => { - if let Some(ack_node) = PairUtils::build_ack_node(node) + if let Some(ack_node) = PairUtils::build_ack_node_ref(node) && let Err(e) = client.send_node(ack_node).await { warn!("Failed to send acknowledgement: {e:?}"); @@ -53,10 +51,10 @@ pub async fn handle_iq(client: &Arc<Client>, node: &Node) -> bool { }; for grandchild in child.get_children_by_tag("ref") { - if let Some(NodeContent::Bytes(bytes)) = &grandchild.content - && let Ok(r) = String::from_utf8(bytes.clone()) + if let Some(bytes) = grandchild.content_bytes() + && let Ok(r) = std::str::from_utf8(bytes) { - codes.push(PairUtils::make_qr_data(&device_state, r)); + codes.push(PairUtils::make_qr_data(&device_state, r.to_string())); } } @@ -137,7 +135,11 @@ pub async fn handle_iq(client: &Arc<Client>, node: &Node) -> bool { false } -async fn handle_pair_success(client: &Arc<Client>, request_node: &Node, success_node: &Node) { +async fn handle_pair_success<'a>( + client: &Arc<Client>, + request_node: &NodeRef<'a>, + success_node: &NodeRef<'a>, +) { if let Some(tx) = client.pairing_cancellation_tx.lock().await.take() { let _ = tx.try_send(()); debug!("Sent QR rotation stop signal"); @@ -150,20 +152,18 @@ async fn handle_pair_success(client: &Arc<Client>, request_node: &Node, success_ client.update_server_time_offset(request_node); - let req_id = match request_node.attrs.get("id") { - Some(id) => id.to_string(), + let req_id = match request_node.get_attr("id").map(|v| v.as_str()) { + Some(id) => id.into_owned(), None => { error!("Received pair-success without request ID"); return; } }; - let device_identity_bytes = match success_node - .get_optional_child_by_tag(&["device-identity"]) - .and_then(|n| n.content.as_ref()) - { - Some(NodeContent::Bytes(b)) => b.clone(), - _ => { + let device_identity_node = success_node.get_optional_child_by_tag(&["device-identity"]); + let device_identity_bytes = match device_identity_node.and_then(|n| n.content_bytes()) { + Some(b) => b, + None => { let error_node = PairUtils::build_pair_error_node(&req_id, 500, "internal-error"); if let Err(e) = client.send_node(error_node).await { error!("Failed to send pair error node: {e}"); @@ -176,22 +176,18 @@ async fn handle_pair_success(client: &Arc<Client>, request_node: &Node, success_ let business_name = success_node .get_optional_child_by_tag(&["biz"]) .map(|n| { - n.attrs() - .optional_string("name") - .as_deref() - .unwrap_or("") - .to_string() + n.get_attr("name") + .map(|v| v.as_str().into_owned()) + .unwrap_or_default() }) .unwrap_or_default(); let platform = success_node .get_optional_child_by_tag(&["platform"]) .map(|n| { - n.attrs() - .optional_string("name") - .as_deref() - .unwrap_or("") - .to_string() + n.get_attr("name") + .map(|v| v.as_str().into_owned()) + .unwrap_or_default() }) .unwrap_or_default(); @@ -201,13 +197,7 @@ async fn handle_pair_success(client: &Arc<Client>, request_node: &Node, success_ let mut parser = device_node.attrs(); let parsed_jid = parser.optional_jid("jid").unwrap_or_default(); let parsed_lid = parser.optional_jid("lid").unwrap_or_default(); - - if let Err(e) = parser.finish() { - warn!(target: "Client/Pair", "Error parsing device node attributes: {e:?}"); - (Jid::default(), Jid::default()) - } else { - (parsed_jid, parsed_lid) - } + (parsed_jid, parsed_lid) } else { (Jid::default(), Jid::default()) }; @@ -219,7 +209,7 @@ async fn handle_pair_success(client: &Arc<Client>, request_node: &Node, success_ adv_secret_key: device_snapshot.adv_secret_key, }; - let result = PairUtils::do_pair_crypto(&device_state, &device_identity_bytes); + let result = PairUtils::do_pair_crypto(&device_state, device_identity_bytes); match result { Ok((self_signed_identity_bytes, key_index)) => { diff --git a/src/pair_code.rs b/src/pair_code.rs index d227abb90..53b48574d 100644 --- a/src/pair_code.rs +++ b/src/pair_code.rs @@ -53,8 +53,8 @@ use log::{error, info, warn}; use std::sync::Arc; use wacore::libsignal::protocol::KeyPair; use wacore::pair_code::{PairCodeError, PairCodeState, PairCodeUtils}; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::Jid; +use wacore_binary::{NodeContent, NodeContentRef, NodeRef}; // Re-export types for user convenience pub use wacore::pair_code::{PairCodeOptions, PlatformId}; @@ -176,7 +176,7 @@ impl Client { let query = InfoQuery { query_type: InfoQueryType::Set, namespace: "md", - to: Jid::new("", SERVER_JID), + to: Jid::new("", wacore_binary::Server::Pn), target: None, content: Some(NodeContent::Nodes( iq_content @@ -194,7 +194,7 @@ impl Client { .map_err(|e: IqError| PairCodeError::RequestFailed(e.to_string()))?; // Extract pairing ref from response - let pairing_ref = PairCodeUtils::parse_companion_hello_response(&response) + let pairing_ref = PairCodeUtils::parse_companion_hello_response(response.get()) .ok_or(PairCodeError::MissingPairingRef)?; info!( @@ -225,7 +225,10 @@ impl Client { /// /// This is called when the user enters the code on their phone. The notification /// contains the primary device's encrypted ephemeral public key and identity public key. -pub(crate) async fn handle_pair_code_notification(client: &Arc<Client>, node: &Node) -> bool { +pub(crate) async fn handle_pair_code_notification( + client: &Arc<Client>, + node: &NodeRef<'_>, +) -> bool { // Check if this is a link_code_companion_reg notification let Some(reg_node) = node.get_optional_child_by_tag(&["link_code_companion_reg"]) else { return false; @@ -234,10 +237,12 @@ pub(crate) async fn handle_pair_code_notification(client: &Arc<Client>, node: &N // Extract primary's wrapped ephemeral public key (80 bytes: salt + iv + encrypted key) let primary_wrapped_ephemeral = match reg_node .get_optional_child_by_tag(&["link_code_pairing_wrapped_primary_ephemeral_pub"]) - .and_then(|n| n.content.as_ref()) - { - Some(NodeContent::Bytes(b)) if b.len() == 80 => b.clone(), - _ => { + .and_then(|n| match n.content.as_deref() { + Some(NodeContentRef::Bytes(b)) if b.len() == 80 => Some(b.to_vec()), + _ => None, + }) { + Some(b) => b, + None => { warn!( target: "Client/PairCode", "Missing or invalid primary wrapped ephemeral pub in notification" @@ -249,19 +254,12 @@ pub(crate) async fn handle_pair_code_notification(client: &Arc<Client>, node: &N // Extract primary's identity public key (32 bytes, unencrypted) let primary_identity_pub: [u8; 32] = match reg_node .get_optional_child_by_tag(&["primary_identity_pub"]) - .and_then(|n| n.content.as_ref()) - { - Some(NodeContent::Bytes(b)) if b.len() == 32 => match b.as_slice().try_into() { - Ok(arr) => arr, - Err(_) => { - warn!( - target: "Client/PairCode", - "Failed to convert primary identity pub to array" - ); - return false; - } - }, - _ => { + .and_then(|n| match n.content.as_deref() { + Some(NodeContentRef::Bytes(b)) if b.len() == 32 => b.as_ref().try_into().ok(), + _ => None, + }) { + Some(arr) => arr, + None => { warn!( target: "Client/PairCode", "Missing or invalid primary identity pub in notification" diff --git a/src/pdo.rs b/src/pdo.rs index ba8e78196..88ab37b75 100644 --- a/src/pdo.rs +++ b/src/pdo.rs @@ -24,7 +24,7 @@ use std::time::Duration; use wacore::types::message::{ ChatMessageId, EditAttribute, MessageCategory, MessageSource, MsgMetaInfo, }; -use wacore_binary::jid::{Jid, JidExt}; +use wacore_binary::{Jid, JidExt}; use waproto::whatsapp as wa; #[derive(Clone, Debug)] @@ -463,7 +463,7 @@ impl Client { if info.source.is_from_me { return; } - if info.source.chat.server == wacore_binary::jid::BROADCAST_SERVER { + if info.source.chat.server == wacore_binary::Server::Broadcast { return; } @@ -500,7 +500,7 @@ impl Client { #[cfg(test)] mod tests { - use wacore_binary::jid::{DEFAULT_USER_SERVER, Jid, JidExt}; + use wacore_binary::{Jid, JidExt, Server}; #[test] fn test_pdo_peer_target_is_device_0() { @@ -517,7 +517,7 @@ mod tests { let peer_target = own_pn.to_non_ad(); assert_eq!(peer_target.user, "559999999999"); - assert_eq!(peer_target.server, DEFAULT_USER_SERVER); + assert_eq!(peer_target.server, Server::Pn); } #[test] diff --git a/src/pending_device_sync.rs b/src/pending_device_sync.rs index 6759b30e4..9ff3ff633 100644 --- a/src/pending_device_sync.rs +++ b/src/pending_device_sync.rs @@ -2,7 +2,7 @@ //! WA Web: `OfflinePendingDeviceCache` + `doPendingDeviceSync()`. use std::collections::HashSet; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; pub(crate) struct PendingDeviceSync { pending: async_lock::Mutex<HashSet<Jid>>, diff --git a/src/prekeys.rs b/src/prekeys.rs index ac34e195e..71633e8e9 100644 --- a/src/prekeys.rs +++ b/src/prekeys.rs @@ -15,7 +15,7 @@ use wacore::iq::prekeys::{ use wacore::libsignal::protocol::{KeyPair, PreKeyBundle, PublicKey}; use wacore::libsignal::store::record_helpers::new_pre_key_record; use wacore::store::commands::DeviceCommand; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; pub use wacore::prekeys::PreKeyUtils; diff --git a/src/receipt.rs b/src/receipt.rs index b38855927..6b76f47bb 100644 --- a/src/receipt.rs +++ b/src/receipt.rs @@ -5,13 +5,13 @@ use log::debug; use std::sync::Arc; use wacore::types::message::MessageCategory; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, JidExt as _}; +use wacore_binary::{Jid, JidExt as _}; -use wacore_binary::node::Node; +use wacore_binary::OwnedNodeRef; impl Client { fn should_send_delivery_receipt(info: &crate::types::message::MessageInfo) -> bool { - use wacore_binary::jid::STATUS_BROADCAST_USER; + use wacore_binary::STATUS_BROADCAST_USER; if info.id.is_empty() || info.source.chat.user == STATUS_BROADCAST_USER @@ -27,8 +27,9 @@ impl Client { info.category == MessageCategory::Peer || !info.source.is_from_me } - pub(crate) async fn handle_receipt(self: &Arc<Self>, node: Arc<Node>) { - let mut attrs = node.attrs(); + pub(crate) async fn handle_receipt(self: &Arc<Self>, node: Arc<OwnedNodeRef>) { + let nr = node.get(); + let mut attrs = nr.attrs(); let from = attrs.jid("from"); let id = match attrs.optional_string("id") { Some(id) => id.to_string(), @@ -65,7 +66,6 @@ impl Client { if receipt_type == ReceiptType::Retry { let client_clone = Arc::clone(self); - // Arc clone is cheap - just reference count increment let node_clone = Arc::clone(&node); self.runtime .spawn(Box::pin(async move { @@ -88,21 +88,21 @@ impl Client { // Since we don't have a VoIP stack yet, log and dispatch as a // Receipt event so consumers can observe it. When VoIP is // implemented (#345), this will route to the VoIP re-key handler. - if let Some(child) = node.get_optional_child("enc_rekey") { - let mut attrs = child.attrs(); + if let Some(child) = nr.get_optional_child("enc_rekey") { + let mut child_attrs = child.attrs(); log::debug!( "Received enc_rekey_retry receipt for call-id={} from {} \ (call-creator={}, count={}). VoIP not implemented, forwarding as event.", - attrs + child_attrs .optional_string("call-id") .as_deref() .unwrap_or_default(), from, - attrs + child_attrs .optional_string("call-creator") .as_deref() .unwrap_or_default(), - attrs + child_attrs .optional_string("count") .and_then(|s| s.parse::<u8>().ok()) .unwrap_or(1), @@ -183,7 +183,7 @@ impl Client { // Additional message IDs go into <list><item id="..."/></list> if message_ids.len() > 1 { - let items: Vec<wacore_binary::node::Node> = message_ids[1..] + let items: Vec<wacore_binary::Node> = message_ids[1..] .iter() .map(|id| NodeBuilder::new("item").attr("id", id).build()) .collect(); @@ -209,6 +209,10 @@ mod tests { use std::sync::Mutex; use wacore::types::events::EventHandler; + fn node_to_arc(node: wacore_binary::Node) -> Arc<OwnedNodeRef> { + crate::test_utils::node_to_owned_ref(&node) + } + #[derive(Default)] struct TestEventCollector { events: Mutex<Vec<Event>>, @@ -506,7 +510,7 @@ mod tests { let (client, collector) = setup_client_with_collector().await; // Build an enc_rekey_retry receipt node matching WA Web structure - let node = Arc::new( + let node = node_to_arc( NodeBuilder::new("receipt") .attr("from", "5511999999999@s.whatsapp.net") .attr("id", "3EB0AABBCCDD") @@ -555,7 +559,7 @@ mod tests { let (client, collector) = setup_client_with_collector().await; // Malformed: no <enc_rekey> child - let node = Arc::new( + let node = node_to_arc( NodeBuilder::new("receipt") .attr("from", "5511999999999@s.whatsapp.net") .attr("id", "3EB0AABBCCDD") @@ -611,7 +615,7 @@ mod tests { /// ensuring the NodeValue::Jid optimization is not accidentally regressed to to_string. #[test] fn test_receipt_node_uses_jid_attrs() { - use wacore_binary::node::NodeValue; + use wacore_binary::NodeValue; let chat_jid: Jid = "120363021033254949@g.us" .parse() diff --git a/src/request.rs b/src/request.rs index 1820e0bc6..15c337533 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,11 +1,12 @@ use crate::client::Client; use crate::socket::error::SocketError; use futures::FutureExt; +use std::sync::Arc; use std::sync::atomic::Ordering; use std::time::Duration; use thiserror::Error; use wacore::runtime::timeout as rt_timeout; -use wacore_binary::node::Node; +use wacore_binary::Node; pub use wacore::request::{InfoQuery, InfoQueryType, RequestUtils}; @@ -91,7 +92,7 @@ impl Client { /// /// # Returns /// - /// * `Ok(Node)` - The response node from the server + /// * `Ok(Arc<OwnedNodeRef>)` - The response node from the server (zero-copy, borrowed from decode buffer) /// * `Err(IqError)` - Various error conditions including timeout, connection issues, or server errors /// /// # Example @@ -99,8 +100,8 @@ impl Client { /// ```rust,no_run /// use wacore::request::{InfoQuery, InfoQueryType}; /// use wacore_binary::builder::NodeBuilder; - /// use wacore_binary::node::NodeContent; - /// use wacore_binary::jid::{Jid, SERVER_JID}; + /// use wacore_binary::NodeContent; + /// use wacore_binary::{Jid, Server}; /// /// // This is a simplified example - real usage requires proper setup /// # async fn example(client: &whatsapp_rust::Client) -> Result<(), Box<dyn std::error::Error>> { @@ -108,7 +109,7 @@ impl Client { /// .attr("type", "available") /// .build(); /// - /// let server_jid = Jid::new("", SERVER_JID); + /// let server_jid = Jid::new("", Server::Pn); /// /// let query = InfoQuery { /// query_type: InfoQueryType::Set, @@ -121,10 +122,14 @@ impl Client { /// }; /// /// let response = client.send_iq(query).await?; + /// // Access the node via response.get() /// # Ok(()) /// # } /// ``` - pub async fn send_iq(&self, query: InfoQuery<'_>) -> Result<Node, IqError> { + pub async fn send_iq( + &self, + query: InfoQuery<'_>, + ) -> Result<Arc<wacore_binary::OwnedNodeRef>, IqError> { // Fail fast if the client is shutting down if !self.is_running.load(Ordering::Relaxed) { return Err(IqError::NotConnected); @@ -172,7 +177,7 @@ impl Client { futures::select! { result = rt_timeout(&*self.runtime, iq_timeout, rx).fuse() => { match result { - Ok(Ok(response_node)) => match *request_utils.parse_iq_response(&response_node) { + Ok(Ok(response_node)) => match request_utils.parse_iq_response(response_node.get()) { Ok(()) => Ok(response_node), Err(e) => Err(e.into()), }, @@ -209,6 +214,7 @@ impl Client { { let iq = spec.build_iq(); let response = self.send_iq(iq).await?; - spec.parse_response(&response).map_err(IqError::ParseError) + spec.parse_response(response.get()) + .map_err(IqError::ParseError) } } diff --git a/src/retry.rs b/src/retry.rs index 318d76bf1..2af97c8cd 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -14,11 +14,15 @@ use wacore::libsignal::protocol::{ use wacore::libsignal::store::PreKeyStore; use wacore::protocol::ProtocolNode; use wacore::types::jid::JidExt; +use wacore_binary::JidExt as _; +use wacore_binary::OwnedNodeRef; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::JidExt as _; -use wacore_binary::node::{Node, NodeContent}; +#[cfg(test)] +use wacore_binary::{Node, NodeContent}; +use wacore_binary::{NodeContentRef, NodeRef}; -/// Helper to extract bytes content from a Node. +/// Helper to extract bytes content from a Node (used in tests). +#[cfg(test)] fn get_bytes_content(node: &Node) -> Option<&[u8]> { match &node.content { Some(NodeContent::Bytes(b)) => Some(b.as_slice()), @@ -26,11 +30,37 @@ fn get_bytes_content(node: &Node) -> Option<&[u8]> { } } -/// Helper to extract registration ID from a node (4 bytes big-endian). +/// Helper to extract bytes content from a NodeRef. +fn get_bytes_content_ref<'a>(node: &'a NodeRef<'_>) -> Option<&'a [u8]> { + match node.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => Some(b.as_ref()), + _ => None, + } +} + +/// Helper to extract registration ID from a Node (used in tests). +#[cfg(test)] fn extract_registration_id_from_node(node: &Node) -> Option<u32> { let registration_node = node.get_optional_child("registration")?; let bytes = get_bytes_content(registration_node)?; + if bytes.len() >= 4 { + Some(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) + } else if !bytes.is_empty() { + let mut arr = [0u8; 4]; + let start = 4 - bytes.len(); + arr[start..].copy_from_slice(bytes); + Some(u32::from_be_bytes(arr)) + } else { + None + } +} + +/// Helper to extract registration ID from a NodeRef (4 bytes big-endian). +fn extract_registration_id_from_node_ref(node: &NodeRef<'_>) -> Option<u32> { + let registration_node = node.get_optional_child("registration")?; + let bytes = get_bytes_content_ref(registration_node)?; + if bytes.len() >= 4 { Some(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) } else if !bytes.is_empty() { @@ -56,20 +86,21 @@ impl Client { pub(crate) async fn handle_retry_receipt( self: &Arc<Self>, receipt: &Receipt, - node: &Node, + node: &Arc<OwnedNodeRef>, ) -> Result<(), anyhow::Error> { - let retry_child = node + let nr = node.get(); + let retry_child = nr .get_optional_child("retry") .ok_or_else(|| anyhow::anyhow!("<retry> child missing from receipt"))?; let message_id = retry_child - .attrs() - .optional_string("id") + .get_attr("id") + .map(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("<retry> missing 'id' attribute"))? - .to_string(); + .into_owned(); let retry_count: u8 = retry_child - .attrs() - .optional_string("count") + .get_attr("count") + .map(|v| v.as_str()) .and_then(|s| s.parse().ok()) .unwrap_or(1); @@ -89,13 +120,12 @@ impl Client { // For groups/status broadcasts, the actual participant is in the // `participant` attribute of the receipt node, NOT receipt.source.sender // (which may be the group/broadcast JID for non-group servers). - let participant_str = if is_group_or_status { - node.attrs() - .optional_string("participant") - .map(|s| s.to_string()) - .unwrap_or_else(|| receipt.source.sender.to_string()) + let participant_jid = if is_group_or_status { + nr.attrs() + .optional_jid("participant") + .unwrap_or_else(|| receipt.source.sender.clone()) } else { - receipt.source.sender.to_string() + receipt.source.sender.clone() }; // Deduplicate retry receipts to prevent processing the same retry multiple times. @@ -103,7 +133,7 @@ impl Client { // For DMs: key is (chat, msg_id) since there's only one sender. // Uses atomic entry API to avoid race conditions between check and insert. let dedupe_key = if is_group_or_status { - format!("{}:{}:{}", receipt.source.chat, message_id, participant_str) + format!("{}:{}:{}", receipt.source.chat, message_id, participant_jid) } else { format!("{}:{}", receipt.source.chat, message_id) }; @@ -160,12 +190,6 @@ impl Client { .await; } - // Reuse the participant string extracted earlier (same source: node's - // `participant` attribute for groups/status, receipt.source.sender for DMs). - let participant_jid = participant_str - .parse::<wacore_binary::jid::Jid>() - .unwrap_or_else(|_| receipt.source.sender.clone()); - // Resolved JID for session operations; keep original for stanza addressing let resolved_jid = self.resolve_encryption_jid(&participant_jid).await; @@ -196,7 +220,7 @@ impl Client { if !receipt.source.chat.is_status_broadcast() { // Try to process key bundle if present let key_bundle_result = self - .process_retry_key_bundle(node, &resolved_jid, is_peer) + .process_retry_key_bundle(nr, &resolved_jid, is_peer) .await; if let Err(e) = &key_bundle_result { @@ -208,7 +232,7 @@ impl Client { // WhatsApp Web behavior: If no key bundle but registration ID differs from stored // session, delete the session to force re-establishment. // This handles the case where the requester reinstalled but didn't include keys. - if let Some(received_reg_id) = extract_registration_id_from_node(node) { + if let Some(received_reg_id) = extract_registration_id_from_node_ref(nr) { let signal_address = resolved_jid.to_protocol_address(); let device_store = self.persistence_manager.get_device_arc().await; let device_guard = device_store.read().await; @@ -280,7 +304,7 @@ impl Client { log::warn!( "Unknown device {} in group {} — forcing full sender key rotation \ (matches WA Web's rotateKey behavior)", - participant_str, + participant_jid, group_jid ); @@ -507,8 +531,8 @@ impl Client { /// * `is_peer` - Whether this is a peer device (our own device) async fn process_retry_key_bundle( &self, - node: &Node, - requester_jid: &wacore_binary::jid::Jid, + node: &NodeRef<'_>, + requester_jid: &wacore_binary::Jid, is_peer: bool, ) -> Result<(), anyhow::Error> { let keys_node = node @@ -519,7 +543,7 @@ impl Client { // Extract registration ID (4 bytes big-endian). let registration_id = registration_node - .and_then(get_bytes_content) + .and_then(get_bytes_content_ref) .map(|bytes| { if bytes.len() >= 4 { u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) @@ -580,16 +604,13 @@ impl Client { // Extract identity key. let identity_bytes = keys_node .get_optional_child("identity") - .and_then(get_bytes_content) + .and_then(get_bytes_content_ref) .ok_or_else(|| anyhow::anyhow!("Missing identity key in retry receipt"))?; let identity_key = PublicKey::from_djb_public_key_bytes(identity_bytes)?; // Extract prekey (optional in some cases). - let prekey_node = keys_node - .get_optional_child("key") - .map(OneTimePreKeyNode::try_from_node) - .transpose()?; - let prekey_data = if let Some(prekey_node) = prekey_node { + let prekey_data = if let Some(key_ref) = keys_node.get_optional_child("key") { + let prekey_node = OneTimePreKeyNode::try_from_node_ref(key_ref)?; let prekey_public = PublicKey::from_djb_public_key_bytes(&prekey_node.public_bytes)?; Some((prekey_node.id.into(), prekey_public)) } else { @@ -597,11 +618,11 @@ impl Client { }; // Extract signed prekey. - let skey_node = keys_node + let skey_ref = keys_node .get_optional_child("skey") .ok_or_else(|| anyhow::anyhow!("Missing signed prekey in retry receipt"))?; - let signed_prekey = SignedPreKeyNode::try_from_node(skey_node)?; + let signed_prekey = SignedPreKeyNode::try_from_node_ref(skey_ref)?; let skey_public = PublicKey::from_djb_public_key_bytes(&signed_prekey.public_bytes)?; let skey_signature: [u8; 64] = signed_prekey .signature @@ -841,9 +862,9 @@ impl Client { pub(crate) async fn send_enc_rekey_retry_receipt( &self, stanza_id: &str, - peer_jid: &wacore_binary::jid::Jid, + peer_jid: &wacore_binary::Jid, call_id: &str, - call_creator: &wacore_binary::jid::Jid, + call_creator: &wacore_binary::Jid, retry_count: u8, ) -> Result<(), anyhow::Error> { let device_snapshot = self.persistence_manager.get_device_snapshot().await; @@ -884,7 +905,7 @@ mod tests { use crate::store::persistence_manager::PersistenceManager; use crate::test_utils::MockHttpClient; use std::borrow::Cow; - use wacore_binary::jid::{Jid, JidExt}; + use wacore_binary::{Jid, JidExt}; use waproto::whatsapp as wa; #[tokio::test] @@ -940,7 +961,7 @@ mod tests { #[test] fn get_bytes_content_extracts_bytes() { - use wacore_binary::node::{Attrs, Node}; + use wacore_binary::{Attrs, Node}; // Test with bytes content let node = Node { @@ -992,7 +1013,7 @@ mod tests { info: &MessageInfo, our_pn: &Jid, our_lid: &Jid, - ) -> wacore_binary::node::Node { + ) -> wacore_binary::Node { // Mirror production routing: groups → chat JID, DMs → sender JID let receipt_to = if info.source.is_group { &info.source.chat @@ -1221,7 +1242,7 @@ mod tests { .get_optional_child("registration") .expect("<registration> child must exist"); let reg_bytes = match &registration.content { - Some(wacore_binary::node::NodeContent::Bytes(b)) => b.clone(), + Some(wacore_binary::NodeContent::Bytes(b)) => b.clone(), _ => panic!("registration must contain bytes"), }; assert_eq!( @@ -1401,7 +1422,7 @@ mod tests { #[test] fn bot_jid_detection() { // Test bot JID detection for bot message filtering - use wacore_binary::jid::JidExt as _; + use wacore_binary::JidExt as _; // Regular user JID - not a bot let regular_user: Jid = "1234567890@s.whatsapp.net".parse().unwrap(); @@ -1426,7 +1447,7 @@ mod tests { #[test] fn extract_registration_id_from_node_test() { - use wacore_binary::node::{Attrs, Node}; + use wacore_binary::{Attrs, Node}; // Test with 4-byte registration ID let reg_bytes = vec![0x00, 0x01, 0x02, 0x03]; // = 66051 @@ -1484,7 +1505,7 @@ mod tests { #[test] fn group_or_status_detection_for_sender_key_handling() { // Test that both groups and status broadcasts trigger sender key handling - use wacore_binary::jid::JidExt as _; + use wacore_binary::JidExt as _; let group: Jid = "120363021033254949@g.us".parse().unwrap(); let status: Jid = "status@broadcast".parse().unwrap(); @@ -1717,19 +1738,15 @@ mod tests { let is_group_or_status = true; let fallback_sender: Jid = "status@broadcast".parse().unwrap(); - let participant_str = if is_group_or_status { + let participant_jid = if is_group_or_status { node.attrs() - .optional_string("participant") - .map(|s| s.to_string()) - .unwrap_or_else(|| fallback_sender.to_string()) + .optional_jid("participant") + .unwrap_or_else(|| fallback_sender.clone()) } else { - fallback_sender.to_string() + fallback_sender.clone() }; // Should extract the actual participant, not status@broadcast - assert_eq!(participant_str, "236395184570386@lid"); - - let participant_jid: Jid = participant_str.parse().unwrap(); assert!(participant_jid.is_lid()); assert_eq!(participant_jid.user, "236395184570386"); assert!(!participant_jid.is_status_broadcast()); @@ -1749,14 +1766,13 @@ mod tests { let fallback_sender: Jid = "status@broadcast".parse().unwrap(); - let participant_str = node + let participant_jid = node .attrs() - .optional_string("participant") - .map(|s| s.to_string()) - .unwrap_or_else(|| fallback_sender.to_string()); + .optional_jid("participant") + .unwrap_or_else(|| fallback_sender.clone()); // Falls back to sender (status@broadcast) — not ideal but won't crash - assert_eq!(participant_str, "status@broadcast"); + assert!(participant_jid.is_status_broadcast()); } /// Test that dedupe keys are correctly differentiated per-participant diff --git a/src/send.rs b/src/send.rs index 4069fe645..8267496f9 100644 --- a/src/send.rs +++ b/src/send.rs @@ -6,11 +6,11 @@ use wacore::client::context::SendContextResolver; use wacore::libsignal::protocol::SignalProtocolError; use wacore::types::jid::JidExt; use wacore::types::message::AddressingMode; -use wacore_binary::builder::NodeBuilder; #[cfg(test)] -use wacore_binary::jid::DeviceKey; -use wacore_binary::jid::{Jid, JidExt as _}; -use wacore_binary::node::Node; +use wacore_binary::DeviceKey; +use wacore_binary::Node; +use wacore_binary::builder::NodeBuilder; +use wacore_binary::{Jid, JidExt as _, Server}; use waproto::whatsapp as wa; /// Options for [`Client::send_message_with_options`]. @@ -336,8 +336,7 @@ impl Client { } if jid.is_lid() { if let Some(pn) = self.lid_pn_cache.get_phone_number(&jid.user).await { - resolved_recipients - .push(Jid::new(&pn, wacore_binary::jid::DEFAULT_USER_SERVER)); + resolved_recipients.push(Jid::new(&pn, Server::Pn)); } else { return Err(anyhow!( "No PN mapping for LID {}. Ensure the recipient has been \ @@ -637,7 +636,7 @@ impl Client { /// On mismatch, invalidates sender key device cache and group info cache. fn spawn_phash_validation( &self, - rx: futures::channel::oneshot::Receiver<wacore_binary::Node>, + rx: futures::channel::oneshot::Receiver<std::sync::Arc<wacore_binary::OwnedNodeRef>>, our_phash: String, jid: Jid, invalidate_group_cache: bool, @@ -661,8 +660,8 @@ impl Client { return; } }; - if let Some(server) = ack.attrs().optional_string("phash") - && *server != our_phash + if let Some(server) = ack.get().get_attr("phash").map(|v| v.as_str()) + && server != our_phash { log::warn!( "Phash mismatch for {jid}: ours={our_phash}, server={server}. Invalidating caches." @@ -1276,7 +1275,7 @@ impl Client { // HMAC input is "user@lid" (account LID without device suffix), // matching WA Web's accountLid.toString() let recipient_lid = - wacore_binary::jid::Jid::new(*lid_user, "lid").to_string(); + wacore_binary::Jid::new(*lid_user, Server::Lid).to_string(); let cs_token = compute_cs_token(salt, &recipient_lid); extra_nodes.push(build_cs_token_node(&cs_token)); log::debug!(target: "Client/CsToken", "Attached cstoken for {} (NCT fallback)", to); @@ -1563,7 +1562,7 @@ impl Client { } if let Some(lid_user) = self.lid_pn_cache.get_current_lid(&jid.user).await { - Jid::new(&lid_user, "lid") + Jid::new(&lid_user, Server::Lid) } else { jid.to_non_ad() } @@ -1584,7 +1583,7 @@ impl Client { self.resolve_to_lid_jid(jid).await } else if jid.is_lid() { if let Some(pn) = self.lid_pn_cache.get_phone_number(&jid.user).await { - Jid::new(&pn, "s.whatsapp.net") + Jid::new(&pn, Server::Pn) } else { jid.to_non_ad() } diff --git a/src/sender_key_device_cache.rs b/src/sender_key_device_cache.rs index 1e330b697..516e88874 100644 --- a/src/sender_key_device_cache.rs +++ b/src/sender_key_device_cache.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use crate::cache::Cache; use crate::cache_config::CacheEntryConfig; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; /// Pre-parsed, pre-indexed sender key device map for one group. #[derive(Clone, Debug)] diff --git a/src/session.rs b/src/session.rs index 78266e3cc..3d8bccb99 100644 --- a/src/session.rs +++ b/src/session.rs @@ -7,7 +7,7 @@ mod tests { use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; - use wacore_binary::jid::Jid; + use wacore_binary::Jid; fn make_jid(user: &str) -> Jid { Jid::pn(user) diff --git a/src/spam_report.rs b/src/spam_report.rs index 98e3d706d..b15fbd886 100644 --- a/src/spam_report.rs +++ b/src/spam_report.rs @@ -43,7 +43,7 @@ impl Client { #[cfg(test)] mod tests { use super::*; - use wacore_binary::jid::Jid; + use wacore_binary::Jid; #[test] fn test_spam_flow_as_str() { diff --git a/src/test_utils.rs b/src/test_utils.rs index df0e4785b..f37fcd962 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1,6 +1,18 @@ use std::sync::Arc; use crate::Client; +use wacore_binary::{Node, OwnedNodeRef}; + +/// Marshal a `Node` into an `Arc<OwnedNodeRef>` for use in tests. +pub fn node_to_owned_ref(node: &Node) -> Arc<OwnedNodeRef> { + let bytes = wacore_binary::marshal::marshal(node).expect("marshal should succeed"); + // marshal() prepends a leading format byte; OwnedNodeRef::new expects raw protocol bytes + { + let mut bytes = bytes; + bytes.remove(0); + Arc::new(OwnedNodeRef::new(bytes).expect("OwnedNodeRef::new should succeed")) + } +} use crate::http::{HttpClient, HttpRequest, HttpResponse}; use crate::runtime_impl::TokioRuntime; use crate::store::SqliteStore; diff --git a/src/types/enc_handler.rs b/src/types/enc_handler.rs index e012045f7..fbfdba1a3 100644 --- a/src/types/enc_handler.rs +++ b/src/types/enc_handler.rs @@ -2,7 +2,7 @@ use crate::client::Client; use crate::types::message::MessageInfo; use anyhow::Result; use std::sync::Arc; -use wacore_binary::node::Node; +use wacore_binary::Node; /// Trait for handling custom encrypted message types #[async_trait::async_trait] @@ -28,7 +28,7 @@ mod tests { use anyhow::Result; use async_lock::Mutex; use std::sync::Arc; - use wacore_binary::node::Node; + use wacore_binary::Node; /// Mock handler for testing custom enc types #[derive(Debug)] diff --git a/src/unified_session.rs b/src/unified_session.rs index 256830b5f..1a56dde8b 100644 --- a/src/unified_session.rs +++ b/src/unified_session.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use std::sync::atomic::Ordering; use wacore::ib::{IbStanza, UnifiedSession}; use wacore::protocol::ProtocolNode; -use wacore_binary::node::Node; +use wacore_binary::Node; /// Manager for unified session telemetry. pub struct UnifiedSessionManager { @@ -43,8 +43,8 @@ impl UnifiedSessionManager { } /// Update server time offset from node's `t` attribute (Unix timestamp in seconds). - pub fn update_server_time_offset(&self, node: &Node) { - if let Some(t_val) = node.attrs.get("t").map(|v| v.as_str()) + pub fn update_server_time_offset(&self, node: &wacore_binary::NodeRef<'_>) { + if let Some(t_val) = node.get_attr("t").map(|v| v.as_str()) && let Ok(server_time) = t_val.parse::<i64>() && server_time > 0 { @@ -62,8 +62,13 @@ impl UnifiedSessionManager { /// /// This gives a more accurate clock skew estimate by assuming the server /// timestamp corresponds to the midpoint of the round trip. - pub fn update_server_time_offset_with_rtt(&self, node: &Node, start_time_ms: i64, rtt_ms: i64) { - if let Some(t_val) = node.attrs.get("t").map(|v| v.as_str()) + pub fn update_server_time_offset_with_rtt( + &self, + node: &wacore_binary::NodeRef<'_>, + start_time_ms: i64, + rtt_ms: i64, + ) { + if let Some(t_val) = node.get_attr("t").map(|v| v.as_str()) && let Ok(server_time) = t_val.parse::<i64>() && server_time > 0 { @@ -143,7 +148,7 @@ mod tests { .attr("t", server_time.to_string()) .build(); - manager.update_server_time_offset(&node); + manager.update_server_time_offset(&node.as_node_ref()); let offset = manager.server_time_offset_ms(); assert!( @@ -158,17 +163,17 @@ mod tests { let manager = UnifiedSessionManager::new(); let node = NodeBuilder::new("success").build(); - manager.update_server_time_offset(&node); + manager.update_server_time_offset(&node.as_node_ref()); assert_eq!(manager.server_time_offset_ms(), 0); let node = NodeBuilder::new("success") .attr("t", "not_a_number") .build(); - manager.update_server_time_offset(&node); + manager.update_server_time_offset(&node.as_node_ref()); assert_eq!(manager.server_time_offset_ms(), 0); let node = NodeBuilder::new("success").attr("t", "0").build(); - manager.update_server_time_offset(&node); + manager.update_server_time_offset(&node.as_node_ref()); assert_eq!(manager.server_time_offset_ms(), 0); } @@ -233,7 +238,7 @@ mod tests { let node = NodeBuilder::new("success") .attr("t", (wacore::time::now_secs() + 10).to_string()) .build(); - manager.update_server_time_offset(&node); + manager.update_server_time_offset(&node.as_node_ref()); let (_, seq1) = manager.prepare_send().await.unwrap(); assert_eq!(seq1, 1); diff --git a/src/usync.rs b/src/usync.rs index 98972dfbe..2c104758d 100644 --- a/src/usync.rs +++ b/src/usync.rs @@ -6,7 +6,7 @@ use crate::client::Client; use log::{debug, warn}; use std::collections::HashSet; use wacore::iq::usync::DeviceListSpec; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; impl Client { pub(crate) async fn get_user_devices(&self, jids: &[Jid]) -> Result<Vec<Jid>, anyhow::Error> { @@ -97,7 +97,7 @@ impl Client { ); self.clear_device_record( &user_list.user.user, - &user_list.user.server, + user_list.user.server.as_str(), existing, ) .await; diff --git a/tests/e2e/src/lib.rs b/tests/e2e/src/lib.rs index 7c84db3d1..fbb41f9d3 100644 --- a/tests/e2e/src/lib.rs +++ b/tests/e2e/src/lib.rs @@ -345,7 +345,7 @@ impl TestClient { timeout_secs: u64, ) -> anyhow::Result<Event> { self.wait_for_event(timeout_secs, |e| { - matches!(e, Event::Notification(node) if node.attrs.get("type").is_some_and(|v| v == "w:gp2")) + matches!(e, Event::Notification(node) if node.get().get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")) }) .await } diff --git a/tests/e2e/tests/memory_soak.rs b/tests/e2e/tests/memory_soak.rs index e45ba2340..51ac648d3 100644 --- a/tests/e2e/tests/memory_soak.rs +++ b/tests/e2e/tests/memory_soak.rs @@ -406,7 +406,7 @@ async fn test_heavy_group_soak() -> anyhow::Result<()> { for client in [&mut client_b, &mut client_c] { client .wait_for_event(15, |e| { - matches!(e, Event::Notification(node) if node.attrs.get("type").is_some_and(|v| v == "w:gp2")) + matches!(e, Event::Notification(node) if node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")) }) .await?; } @@ -426,7 +426,7 @@ async fn test_heavy_group_soak() -> anyhow::Result<()> { client_b .wait_for_event(15, |e| { - matches!(e, Event::Notification(node) if node.attrs.get("type").is_some_and(|v| v == "w:gp2")) + matches!(e, Event::Notification(node) if node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")) }) .await?; @@ -548,7 +548,7 @@ async fn test_heavy_mixed_soak() -> anyhow::Result<()> { for client in [&mut client_b, &mut client_c] { client .wait_for_event(15, |e| { - matches!(e, Event::Notification(node) if node.attrs.get("type").is_some_and(|v| v == "w:gp2")) + matches!(e, Event::Notification(node) if node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")) }) .await?; } diff --git a/tests/e2e/tests/offline_groups.rs b/tests/e2e/tests/offline_groups.rs index 0a06129ce..22ac8d374 100644 --- a/tests/e2e/tests/offline_groups.rs +++ b/tests/e2e/tests/offline_groups.rs @@ -144,7 +144,7 @@ async fn test_mixed_offline_event_ordering() -> anyhow::Result<()> { let result = client_c .wait_for_event(10, |e| { matches!(e, Event::Message(msg, _) if msg.conversation.is_some()) - || matches!(e, Event::Notification(node) if node.attrs.get("type").is_some_and(|v| v == "w:gp2")) + || matches!(e, Event::Notification(node) if node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")) }) .await; @@ -433,7 +433,7 @@ async fn test_offline_multi_sender_group_messages() -> anyhow::Result<()> { let result = client_c .wait_for_event(timeout_secs, |e| { matches!(e, Event::Message(msg, _) if msg.conversation.is_some()) - || matches!(e, Event::Notification(node) if node.attrs.get("type").is_some_and(|v| v == "w:gp2")) + || matches!(e, Event::Notification(node) if node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")) }) .await; diff --git a/tests/e2e/tests/presence.rs b/tests/e2e/tests/presence.rs index 7dbb4cf54..4438f0e36 100644 --- a/tests/e2e/tests/presence.rs +++ b/tests/e2e/tests/presence.rs @@ -51,7 +51,7 @@ async fn test_presence_available() -> anyhow::Result<()> { .await .map_err(|_| anyhow::anyhow!("Timed out waiting for presence node"))? .map_err(|_| anyhow::anyhow!("Presence waiter channel closed"))?; - info!("Client B received presence node: tag={}", node.tag); + info!("Client B received presence node: tag={}", node.tag()); client_a.disconnect().await; client_b.disconnect().await; diff --git a/tests/e2e/tests/privacy_tokens.rs b/tests/e2e/tests/privacy_tokens.rs index bca8eb52f..cdf591421 100644 --- a/tests/e2e/tests/privacy_tokens.rs +++ b/tests/e2e/tests/privacy_tokens.rs @@ -5,6 +5,7 @@ use log::info; use std::sync::Arc; use wacore::store::traits::TcTokenEntry; use wacore::types::events::Event; +use wacore_binary::OwnedNodeRef; use wacore_binary::node::Node; use whatsapp_rust::{NodeFilter, SendOptions}; @@ -27,7 +28,7 @@ async fn send_first_message_and_expect_463( recipient: &mut TestClient, recipient_jid: &whatsapp_rust::Jid, text: &str, -) -> anyhow::Result<Arc<Node>> { +) -> anyhow::Result<Arc<OwnedNodeRef>> { let msg_id = format!("E2E463{}", uuid::Uuid::new_v4().simple()); send_message_and_expect_463_with_id(sender, recipient, recipient_jid, text, msg_id).await } @@ -38,7 +39,7 @@ async fn send_message_and_expect_463_with_id( recipient_jid: &whatsapp_rust::Jid, text: &str, msg_id: String, -) -> anyhow::Result<Arc<Node>> { +) -> anyhow::Result<Arc<OwnedNodeRef>> { let waiter = sender.client.wait_for_node( NodeFilter::tag("ack") .attr("id", msg_id.clone()) @@ -65,9 +66,9 @@ async fn send_message_and_expect_463_with_id( .await .map_err(|_| anyhow::anyhow!("Timed out waiting for 463 nack"))? .map_err(|_| anyhow::anyhow!("463 nack waiter was canceled"))?; - assert_eq!(ack.tag, "ack"); + assert_eq!(ack.get().tag.as_ref(), "ack"); assert_eq!( - ack.attrs.get("error").map(|v| v.to_string()), + ack.get().get_attr("error").map(|v| v.as_str().into_owned()), Some("463".to_string()) ); diff --git a/wacore/appstate/src/patch_decode.rs b/wacore/appstate/src/patch_decode.rs index 472e9cb8e..19c5af6a2 100644 --- a/wacore/appstate/src/patch_decode.rs +++ b/wacore/appstate/src/patch_decode.rs @@ -3,7 +3,7 @@ use anyhow::{Result, anyhow}; use prost::Message; use std::str::FromStr; -use wacore_binary::node::Node; +use wacore_binary::node::{Node, NodeRef}; use waproto::whatsapp as wa; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -86,9 +86,18 @@ pub fn parse_patch_list(node: &Node) -> Result<PatchList> { parse_single_collection(collection) } +/// Zero-copy entry point for `parse_patch_list`. +pub fn parse_patch_list_ref(node: &NodeRef<'_>) -> Result<PatchList> { + parse_patch_list(&node.to_owned()) +} + /// Parse all `<collection>` children from a `<sync>` response into PatchLists. /// Used for batched multi-collection IQ responses. /// Tolerates both `<iq><sync>...</sync></iq>` and bare `<sync>...</sync>` roots. +pub fn parse_patch_lists_ref(node: &NodeRef<'_>) -> Result<Vec<PatchList>> { + parse_patch_lists(&node.to_owned()) +} + pub fn parse_patch_lists(node: &Node) -> Result<Vec<PatchList>> { let sync_node = if node.tag == "sync" { node diff --git a/wacore/benches/send_receive_benchmark.rs b/wacore/benches/send_receive_benchmark.rs index 368b6df78..29471f36b 100644 --- a/wacore/benches/send_receive_benchmark.rs +++ b/wacore/benches/send_receive_benchmark.rs @@ -187,7 +187,11 @@ impl User { .unwrap(); }); - let jid = Jid::new(user, server); + let jid = Jid::new( + user, + wacore_binary::jid::Server::try_from(server) + .expect("invalid server in benchmark fixture"), + ); let address = jid.to_protocol_address(); Self { diff --git a/wacore/binary/Cargo.toml b/wacore/binary/Cargo.toml index dfdf7f109..1e1f81152 100644 --- a/wacore/binary/Cargo.toml +++ b/wacore/binary/Cargo.toml @@ -23,6 +23,7 @@ compact_str = { workspace = true } flate2 = { workspace = true } phf = { version = "0.13.1", default-features = false } serde = { workspace = true, optional = true } +yoke = { workspace = true } [build-dependencies] phf_codegen = "0.13.1" diff --git a/wacore/binary/benches/binary_benchmark.rs b/wacore/binary/benches/binary_benchmark.rs index d07d26ca5..21da1451e 100644 --- a/wacore/binary/benches/binary_benchmark.rs +++ b/wacore/binary/benches/binary_benchmark.rs @@ -288,7 +288,7 @@ fn bench_attr_parser(marshaled: Vec<u8>) { // Skip the flag byte at position 0 let node_ref = unmarshal_ref(&marshaled[1..]).unwrap(); - let mut parser = node_ref.attr_parser(); + let mut parser = node_ref.attrs(); black_box(parser.optional_string("xmlns")); black_box(parser.optional_string("type")); black_box(parser.optional_jid("from")); diff --git a/wacore/binary/src/attrs.rs b/wacore/binary/src/attrs.rs index aacb92033..945977591 100644 --- a/wacore/binary/src/attrs.rs +++ b/wacore/binary/src/attrs.rs @@ -55,7 +55,7 @@ impl<'a> AttrParserRef<'a> { /// - String variant: Cow::Borrowed — zero copy /// - JID variant: Cow::Owned — allocates only when needed pub fn optional_string(&mut self, key: &str) -> Option<Cow<'a, str>> { - self.get_raw(key, false).map(|v| v.to_string_cow()) + self.get_raw(key, false).map(|v| v.as_str()) } /// Get a required string attribute, returning an error if missing. @@ -94,7 +94,7 @@ impl<'a> AttrParserRef<'a> { } fn get_string_value(&mut self, key: &str, require: bool) -> Option<Cow<'a, str>> { - self.get_raw(key, require).map(|v| v.to_string_cow()) + self.get_raw(key, require).map(|v| v.as_str()) } fn get_bool(&mut self, key: &str, require: bool) -> Option<bool> { diff --git a/wacore/binary/src/decoder.rs b/wacore/binary/src/decoder.rs index 69d68868f..a273ab0cc 100644 --- a/wacore/binary/src/decoder.rs +++ b/wacore/binary/src/decoder.rs @@ -108,8 +108,11 @@ impl<'a> Decoder<'a> { fn read_jid_pair(&mut self) -> Result<JidRef<'a>> { let user_val = self.read_value_as_string()?; - let server = self.read_value_as_string()?.unwrap_or(Cow::Borrowed("")); + let server_str = self.read_value_as_string()?.unwrap_or(Cow::Borrowed("")); let user = user_val.unwrap_or(Cow::Borrowed("")); + let server = crate::jid::Server::try_from(server_str.as_ref()).map_err(|_| { + BinaryError::AttrParse(format!("JID_PAIR unknown server: {}", server_str)) + })?; Ok(JidRef { user, server, @@ -128,20 +131,15 @@ impl<'a> Decoder<'a> { // Domain type mapping — must mirror encoder's server_to_domain_type(). // WA Web: 0=WHATSAPP, 1=LID, even+bit7=HOSTED, 129=HOSTED_LID, else throw. + // server_to_domain_type encodes Pn/unknown as the agent value directly, + // so unmapped agents round-trip as Pn with the original agent preserved. let server = match agent { - 0 => Cow::Borrowed(crate::jid::DEFAULT_USER_SERVER), - 1 => Cow::Borrowed(crate::jid::HIDDEN_USER_SERVER), - 128 => Cow::Borrowed(crate::jid::HOSTED_SERVER), - 129 => Cow::Borrowed(crate::jid::HOSTED_LID_SERVER), - n if (n & 128) != 0 && (n & 1) == 0 => { - // WA Web treats any even number with bit 7 set as HOSTED - Cow::Borrowed(crate::jid::HOSTED_SERVER) - } - _ => { - return Err(BinaryError::AttrParse(format!( - "AD_JID invalid domain type: {agent}" - ))); - } + 0 => crate::jid::Server::Pn, + 1 => crate::jid::Server::Lid, + 128 => crate::jid::Server::Hosted, + 129 => crate::jid::Server::HostedLid, + n if (n & 128) != 0 && (n & 1) == 0 => crate::jid::Server::Hosted, + _ => crate::jid::Server::Pn, }; Ok(JidRef { @@ -159,13 +157,13 @@ impl<'a> Decoder<'a> { .ok_or(BinaryError::InvalidNode)?; let device = self.read_u16_be()?; let integrator = self.read_u16_be()?; - let server = self.read_value_as_string()?.unwrap_or(Cow::Borrowed("")); - if server != crate::jid::INTEROP_SERVER { + let server_str = self.read_value_as_string()?.unwrap_or(Cow::Borrowed("")); + if server_str.as_ref() != crate::jid::INTEROP_SERVER { return Err(BinaryError::InvalidNode); } Ok(JidRef { user, - server, + server: crate::jid::Server::Interop, device, integrator, agent: 0, @@ -177,13 +175,13 @@ impl<'a> Decoder<'a> { .read_value_as_string()? .ok_or(BinaryError::InvalidNode)?; let device = self.read_u16_be()?; - let server = self.read_value_as_string()?.unwrap_or(Cow::Borrowed("")); - if server != crate::jid::MESSENGER_SERVER { + let server_str = self.read_value_as_string()?.unwrap_or(Cow::Borrowed("")); + if server_str.as_ref() != crate::jid::MESSENGER_SERVER { return Err(BinaryError::InvalidNode); } Ok(JidRef { user, - server, + server: crate::jid::Server::Messenger, device, agent: 0, integrator: 0, diff --git a/wacore/binary/src/encoder.rs b/wacore/binary/src/encoder.rs index 00f14a79f..fa003ea4a 100644 --- a/wacore/binary/src/encoder.rs +++ b/wacore/binary/src/encoder.rs @@ -359,12 +359,12 @@ fn split_jid_from_meta(input: &str, meta: ParsedJidMeta) -> (&str, &str) { /// (instead of only as a fallback) was the root cause of a regression /// where LID group messages were silently rejected by the server (error 421). #[inline] -fn server_to_domain_type(server: &str, agent: u8) -> u8 { +fn server_to_domain_type(server: jid::Server, agent: u8) -> u8 { match server { - jid::HIDDEN_USER_SERVER => 1, // "lid" - jid::HOSTED_SERVER => 128, // "hosted" - jid::HOSTED_LID_SERVER => 129, // "hosted.lid" - _ => agent, // s.whatsapp.net (0) and exotic servers + jid::Server::Lid => 1, + jid::Server::Hosted => 128, + jid::Server::HostedLid => 129, + _ => agent, } } @@ -556,7 +556,7 @@ fn owned_jid_encoded_size_with_cache(jid: &Jid, hints: &mut StringHintCache) -> } else { string_encoded_size_with_cache(&jid.user, hints) }; - 1 + user_size + string_encoded_size_with_cache(&jid.server, hints) + 1 + user_size + string_encoded_size_with_cache(jid.server.as_str(), hints) } } @@ -570,7 +570,7 @@ fn jid_ref_encoded_size_with_cache(jid: &JidRef<'_>, hints: &mut StringHintCache } else { string_encoded_size_with_cache(&jid.user, hints) }; - 1 + user_size + string_encoded_size_with_cache(&jid.server, hints) + 1 + user_size + string_encoded_size_with_cache(jid.server.as_str(), hints) } } @@ -753,7 +753,7 @@ impl<'a, W: ByteWriter> Encoder<'a, W> { BinaryError::AttrParse(format!("AD_JID device id out of range: {}", jid.device)) })?; self.write_u8(token::AD_JID)?; - self.write_u8(server_to_domain_type(&jid.server, jid.agent))?; + self.write_u8(server_to_domain_type(jid.server, jid.agent))?; self.write_u8(device)?; self.write_string(&jid.user)?; } else { @@ -764,7 +764,7 @@ impl<'a, W: ByteWriter> Encoder<'a, W> { } else { self.write_string(&jid.user)?; } - self.write_string(&jid.server)?; + self.write_string(jid.server.as_str())?; } Ok(()) } @@ -778,7 +778,7 @@ impl<'a, W: ByteWriter> Encoder<'a, W> { BinaryError::AttrParse(format!("AD_JID device id out of range: {}", jid.device)) })?; self.write_u8(token::AD_JID)?; - self.write_u8(server_to_domain_type(jid.server.as_ref(), jid.agent))?; + self.write_u8(server_to_domain_type(jid.server, jid.agent))?; self.write_u8(device)?; self.write_string(&jid.user)?; } else { @@ -789,7 +789,7 @@ impl<'a, W: ByteWriter> Encoder<'a, W> { } else { self.write_string(&jid.user)?; } - self.write_string(&jid.server)?; + self.write_string(jid.server.as_str())?; } Ok(()) } @@ -1408,8 +1408,7 @@ mod tests { "Round-trip device mismatch for {jid}" ); assert_eq!( - jid.server.as_ref(), - decoded_jid.server.as_ref(), + jid.server, decoded_jid.server, "Round-trip server mismatch for {jid}" ); } diff --git a/wacore/binary/src/jid.rs b/wacore/binary/src/jid.rs index c4662bd0a..54fc71f76 100644 --- a/wacore/binary/src/jid.rs +++ b/wacore/binary/src/jid.rs @@ -158,6 +158,100 @@ pub fn parse_jid_fast(s: &str) -> Option<ParsedJidParts<'_>> { }) } +/// Known WhatsApp server identifiers. +/// +/// Maps to the wire protocol's AD_JID domain type (u8) and the `@server` suffix +/// in JID string representation. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum Server { + #[default] + Pn = 0, + Lid = 1, + Group = 2, + Broadcast = 3, + Newsletter = 4, + Hosted = 5, + HostedLid = 6, + Messenger = 7, + Interop = 8, + Bot = 9, + Legacy = 10, +} + +#[cfg(feature = "serde")] +impl serde::Serialize for Server { + fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { + serializer.serialize_str(self.as_str()) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Server { + fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { + let s = <&str>::deserialize(deserializer)?; + Server::try_from(s).map_err(serde::de::Error::custom) + } +} + +impl Server { + #[inline] + pub fn as_str(self) -> &'static str { + match self { + Self::Pn => "s.whatsapp.net", + Self::Lid => "lid", + Self::Group => "g.us", + Self::Broadcast => "broadcast", + Self::Newsletter => "newsletter", + Self::Hosted => "hosted", + Self::HostedLid => "hosted.lid", + Self::Messenger => "msgr", + Self::Interop => "interop", + Self::Bot => "bot", + Self::Legacy => "c.us", + } + } +} + +impl fmt::Display for Server { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl PartialEq<str> for Server { + fn eq(&self, other: &str) -> bool { + self.as_str() == other + } +} + +impl PartialEq<&str> for Server { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } +} + +impl TryFrom<&str> for Server { + type Error = JidError; + fn try_from(s: &str) -> std::result::Result<Self, Self::Error> { + match s { + "s.whatsapp.net" => Ok(Self::Pn), + "lid" => Ok(Self::Lid), + "g.us" => Ok(Self::Group), + "broadcast" => Ok(Self::Broadcast), + "newsletter" => Ok(Self::Newsletter), + "hosted" => Ok(Self::Hosted), + "hosted.lid" => Ok(Self::HostedLid), + "msgr" => Ok(Self::Messenger), + "interop" => Ok(Self::Interop), + "bot" => Ok(Self::Bot), + "c.us" => Ok(Self::Legacy), + other => Err(JidError::InvalidFormat(format!("unknown server: {other}"))), + } + } +} + +// Keep string constants for backward compatibility and use in non-JID contexts pub const DEFAULT_USER_SERVER: &str = "s.whatsapp.net"; pub const SERVER_JID: &str = "s.whatsapp.net"; pub const GROUP_SERVER: &str = "g.us"; @@ -209,57 +303,58 @@ impl From<std::num::ParseIntError> for JidError { pub trait JidExt { fn user(&self) -> &str; - fn server(&self) -> &str; + fn server(&self) -> Server; fn device(&self) -> u16; fn integrator(&self) -> u16; fn is_ad(&self) -> bool { self.device() > 0 - && (self.server() == DEFAULT_USER_SERVER - || self.server() == HIDDEN_USER_SERVER - || self.server() == HOSTED_SERVER) + && matches!( + self.server(), + Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid + ) } fn is_interop(&self) -> bool { - self.server() == INTEROP_SERVER && self.integrator() > 0 + self.server() == Server::Interop && self.integrator() > 0 } fn is_messenger(&self) -> bool { - self.server() == MESSENGER_SERVER && self.device() > 0 + self.server() == Server::Messenger && self.device() > 0 } fn is_group(&self) -> bool { - self.server() == GROUP_SERVER + self.server() == Server::Group } fn is_broadcast_list(&self) -> bool { - self.server() == BROADCAST_SERVER && self.user() != STATUS_BROADCAST_USER + self.server() == Server::Broadcast && self.user() != STATUS_BROADCAST_USER } fn is_status_broadcast(&self) -> bool { - self.server() == BROADCAST_SERVER && self.user() == STATUS_BROADCAST_USER + self.server() == Server::Broadcast && self.user() == STATUS_BROADCAST_USER } fn is_bot(&self) -> bool { - (self.server() == DEFAULT_USER_SERVER + (self.server() == Server::Pn && self.device() == 0 && (self.user().starts_with("1313555") || self.user().starts_with("131655500"))) - || self.server() == BOT_SERVER + || self.server() == Server::Bot } fn is_newsletter(&self) -> bool { - self.server() == NEWSLETTER_SERVER + self.server() == Server::Newsletter } /// Returns true if this is a hosted/Cloud API device. /// Hosted devices have device ID 99 or use @hosted/@hosted.lid server. /// These devices should be excluded from group message fanout. fn is_hosted(&self) -> bool { - self.device() == 99 || self.server() == HOSTED_SERVER || self.server() == HOSTED_LID_SERVER + self.device() == 99 || matches!(self.server(), Server::Hosted | Server::HostedLid) } fn is_empty(&self) -> bool { - self.server().is_empty() + self.user().is_empty() } fn is_same_user_as(&self, other: &impl JidExt) -> bool { @@ -271,16 +366,16 @@ pub trait JidExt { #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] pub struct Jid { pub user: CompactString, - pub server: Cow<'static, str>, + pub server: Server, pub agent: u8, pub device: u16, pub integrator: u16, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, yoke::Yokeable)] pub struct JidRef<'a> { pub user: Cow<'a, str>, - pub server: Cow<'a, str>, + pub server: Server, pub agent: u8, pub device: u16, pub integrator: u16, @@ -290,8 +385,8 @@ impl JidExt for Jid { fn user(&self) -> &str { &self.user } - fn server(&self) -> &str { - &self.server + fn server(&self) -> Server { + self.server } fn device(&self) -> u16 { self.device @@ -302,10 +397,10 @@ impl JidExt for Jid { } impl Jid { - pub fn new(user: impl Into<CompactString>, server: &str) -> Self { + pub fn new(user: impl Into<CompactString>, server: Server) -> Self { Self { user: user.into(), - server: cow_server_from_str(server), + server, ..Default::default() } } @@ -314,7 +409,7 @@ impl Jid { pub fn pn(user: impl Into<CompactString>) -> Self { Self { user: user.into(), - server: Cow::Borrowed(DEFAULT_USER_SERVER), + server: Server::Pn, ..Default::default() } } @@ -323,7 +418,7 @@ impl Jid { pub fn lid(user: impl Into<CompactString>) -> Self { Self { user: user.into(), - server: Cow::Borrowed(HIDDEN_USER_SERVER), + server: Server::Lid, ..Default::default() } } @@ -332,7 +427,7 @@ impl Jid { pub fn status_broadcast() -> Self { Self { user: CompactString::from(STATUS_BROADCAST_USER), - server: Cow::Borrowed(BROADCAST_SERVER), + server: Server::Broadcast, agent: 0, device: 0, integrator: 0, @@ -343,7 +438,7 @@ impl Jid { pub fn group(id: impl Into<CompactString>) -> Self { Self { user: id.into(), - server: Cow::Borrowed(GROUP_SERVER), + server: Server::Group, ..Default::default() } } @@ -352,7 +447,7 @@ impl Jid { pub fn newsletter(id: impl Into<CompactString>) -> Self { Self { user: id.into(), - server: Cow::Borrowed(NEWSLETTER_SERVER), + server: Server::Newsletter, ..Default::default() } } @@ -361,7 +456,7 @@ impl Jid { pub fn pn_device(user: impl Into<CompactString>, device: u16) -> Self { Self { user: user.into(), - server: Cow::Borrowed(DEFAULT_USER_SERVER), + server: Server::Pn, device, ..Default::default() } @@ -371,7 +466,7 @@ impl Jid { pub fn lid_device(user: impl Into<CompactString>, device: u16) -> Self { Self { user: user.into(), - server: Cow::Borrowed(HIDDEN_USER_SERVER), + server: Server::Lid, device, ..Default::default() } @@ -380,13 +475,13 @@ impl Jid { /// Returns true if this is a Phone Number based JID (s.whatsapp.net) #[inline] pub fn is_pn(&self) -> bool { - self.server == DEFAULT_USER_SERVER + self.server == Server::Pn } /// Returns true if this is a LID based JID #[inline] pub fn is_lid(&self) -> bool { - self.server == HIDDEN_USER_SERVER + self.server == Server::Lid } /// Returns the user part without the device ID suffix (e.g., "123:4" -> "123") @@ -403,7 +498,7 @@ impl Jid { pub fn with_device(&self, device_id: u16) -> Self { Self { user: self.user.clone(), - server: self.server.clone(), + server: self.server, agent: self.agent, device: device_id, integrator: self.integrator, @@ -411,13 +506,8 @@ impl Jid { } pub fn actual_agent(&self) -> u8 { - match &*self.server { - DEFAULT_USER_SERVER => 0, - // For LID (HIDDEN_USER_SERVER), use the parsed agent value. - // LID user identifiers can contain dots (e.g., "100000000000001.1"), - // which are part of the identity, not agent separators. - // Only non-device LID JIDs (without ':') may have an agent suffix. - HIDDEN_USER_SERVER => self.agent, + match self.server { + Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid => 0, _ => self.agent, } } @@ -425,7 +515,7 @@ impl Jid { pub fn to_non_ad(&self) -> Self { Self { user: self.user.clone(), - server: self.server.clone(), + server: self.server, integrator: self.integrator, ..Default::default() } @@ -445,7 +535,7 @@ impl Jid { /// using a normalized key where the agent is 0 for standard servers (s.whatsapp.net, lid). pub fn normalize_for_prekey_bundle(&self) -> Self { let mut jid = self.clone(); - if jid.server == DEFAULT_USER_SERVER || jid.server == HIDDEN_USER_SERVER { + if matches!(jid.server, Server::Pn | Server::Lid) { jid.agent = 0; } jid @@ -453,11 +543,14 @@ impl Jid { pub fn to_ad_string(&self) -> String { if self.user.is_empty() { - self.server.to_string() + self.server.as_str().to_string() } else { format!( "{}.{}:{}@{}", - self.user, self.agent, self.device, self.server + self.user, + self.agent, + self.device, + self.server.as_str() ) } } @@ -473,7 +566,7 @@ impl Jid { pub fn device_key(&self) -> DeviceKey<'_> { DeviceKey { user: &self.user, - server: &self.server, + server: self.server, device: self.device, } } @@ -483,7 +576,7 @@ impl Jid { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct DeviceKey<'a> { pub user: &'a str, - pub server: &'a str, + pub server: Server, pub device: u16, } @@ -491,8 +584,8 @@ impl<'a> JidExt for JidRef<'a> { fn user(&self) -> &str { &self.user } - fn server(&self) -> &str { - &self.server + fn server(&self) -> Server { + self.server } fn device(&self) -> u16 { self.device @@ -503,7 +596,7 @@ impl<'a> JidExt for JidRef<'a> { } impl<'a> JidRef<'a> { - pub fn new(user: Cow<'a, str>, server: Cow<'a, str>) -> Self { + pub fn new(user: Cow<'a, str>, server: Server) -> Self { Self { user, server, @@ -516,7 +609,7 @@ impl<'a> JidRef<'a> { pub fn to_owned(&self) -> Jid { Jid { user: CompactString::from(self.user.as_ref()), - server: cow_server_from_str(&self.server), + server: self.server, agent: self.agent, device: self.device, integrator: self.integrator, @@ -524,25 +617,6 @@ impl<'a> JidRef<'a> { } } -/// Convert a server string to `Cow<'static, str>`, borrowing for known constants. -#[inline] -pub fn cow_server_from_str(server: &str) -> Cow<'static, str> { - match server { - DEFAULT_USER_SERVER => Cow::Borrowed(DEFAULT_USER_SERVER), - HIDDEN_USER_SERVER => Cow::Borrowed(HIDDEN_USER_SERVER), - GROUP_SERVER => Cow::Borrowed(GROUP_SERVER), - BROADCAST_SERVER => Cow::Borrowed(BROADCAST_SERVER), - LEGACY_USER_SERVER => Cow::Borrowed(LEGACY_USER_SERVER), - NEWSLETTER_SERVER => Cow::Borrowed(NEWSLETTER_SERVER), - HOSTED_SERVER => Cow::Borrowed(HOSTED_SERVER), - HOSTED_LID_SERVER => Cow::Borrowed(HOSTED_LID_SERVER), - MESSENGER_SERVER => Cow::Borrowed(MESSENGER_SERVER), - INTEROP_SERVER => Cow::Borrowed(INTEROP_SERVER), - BOT_SERVER => Cow::Borrowed(BOT_SERVER), - other => Cow::Owned(other.to_string()), - } -} - impl FromStr for Jid { type Err = JidError; fn from_str(s: &str) -> Result<Self, Self::Err> { @@ -550,7 +624,7 @@ impl FromStr for Jid { if let Some(parts) = parse_jid_fast(s) { return Ok(Jid { user: CompactString::from(parts.user), - server: cow_server_from_str(parts.server), + server: Server::try_from(parts.server)?, agent: parts.agent, device: parts.device, integrator: parts.integrator, @@ -564,26 +638,10 @@ impl FromStr for Jid { None => ("", s), }; - if user_part.is_empty() { - let known_servers = [ - DEFAULT_USER_SERVER, - GROUP_SERVER, - LEGACY_USER_SERVER, - BROADCAST_SERVER, - HIDDEN_USER_SERVER, - NEWSLETTER_SERVER, - HOSTED_SERVER, - MESSENGER_SERVER, - INTEROP_SERVER, - BOT_SERVER, - STATUS_BROADCAST_USER, - ]; - if !known_servers.contains(&server) { - return Err(JidError::InvalidFormat(format!( - "Invalid JID format: unknown server '{}'", - server - ))); - } + if user_part.is_empty() && Server::try_from(server).is_err() { + return Err(JidError::InvalidFormat(format!( + "unknown server '{server}'" + ))); } // Special handling for LID JIDs, as their user part can contain dots @@ -596,7 +654,7 @@ impl FromStr for Jid { }; return Ok(Jid { user: CompactString::from(user), - server: cow_server_from_str(server), + server: Server::try_from(server)?, device, agent: 0, integrator: 0, @@ -618,30 +676,18 @@ impl FromStr for Jid { && let Some((u, last_part)) = user.rsplit_once('.') && let Ok(num_val) = last_part.parse::<u16>() { + if num_val > u8::MAX as u16 { + return Err(JidError::InvalidFormat(format!( + "Agent component out of range: {num_val}" + ))); + } user = u; agent = num_val as u8; } - if let Some((u, last_part)) = user_part.rsplit_once('.') - && let Ok(num_val) = last_part.parse::<u16>() - { - if server == DEFAULT_USER_SERVER { - user = u; - device = num_val; - } else { - user = u; - if num_val > u8::MAX as u16 { - return Err(JidError::InvalidFormat(format!( - "Agent component out of range: {num_val}" - ))); - } - agent = num_val as u8; - } - } - Ok(Jid { user: CompactString::from(user), - server: cow_server_from_str(server), + server: Server::try_from(server)?, agent, device, integrator: 0, @@ -665,11 +711,10 @@ impl fmt::Display for Jid { // This is a guess based on the failure. The old JS logic is complex. // We will only append the agent if the server is NOT s.whatsapp.net or lid. // AND the server is not one that is derived *from* the agent (like 'hosted'). - let server_str = self.server(); // Use trait method - if server_str != DEFAULT_USER_SERVER - && server_str != HIDDEN_USER_SERVER - && server_str != HOSTED_SERVER - { + if !matches!( + self.server, + Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid + ) { write!(f, ".{}", self.agent)?; } } @@ -678,7 +723,7 @@ impl fmt::Display for Jid { write!(f, ":{}", self.device)?; } - write!(f, "@{}", self.server) + write!(f, "@{}", self.server.as_str()) } } } @@ -686,26 +731,17 @@ impl fmt::Display for Jid { impl<'a> fmt::Display for JidRef<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.user.is_empty() { - // Server-only JID (e.g., "s.whatsapp.net") - no @ prefix - write!(f, "{}", self.server) + write!(f, "{}", self.server.as_str()) } else { write!(f, "{}", self.user)?; - // The agent is encoded in the server type for AD JIDs. - // We should NOT append it to the user string for standard servers. - // Only non-standard servers might use an agent suffix. - // The old JS logic appears to never append the agent for s.whatsapp.net or lid. - if self.agent > 0 { - // This is a guess based on the failure. The old JS logic is complex. - // We will only append the agent if the server is NOT s.whatsapp.net or lid. - // AND the server is not one that is derived *from* the agent (like 'hosted'). - let server_str = self.server(); // Use trait method - if server_str != DEFAULT_USER_SERVER - && server_str != HIDDEN_USER_SERVER - && server_str != HOSTED_SERVER - { - write!(f, ".{}", self.agent)?; - } + if self.agent > 0 + && !matches!( + self.server, + Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid + ) + { + write!(f, ".{}", self.agent)?; } if self.device > 0 { @@ -849,51 +885,40 @@ mod tests { // The Display trait MUST NOT show the agent number. let jid1 = Jid { user: "1234567890".into(), - server: Cow::Borrowed("s.whatsapp.net"), + server: Server::Pn, device: 15, - agent: 2, // This agent would be decoded from binary but should be ignored in display + agent: 2, integrator: 0, }; - // Expected: "1234567890:15@s.whatsapp.net" (agent is omitted) - // Buggy: "1234567890.2:15@s.whatsapp.net" assert_eq!(jid1.to_string(), "1234567890:15@s.whatsapp.net"); - // Failure Case 2: A LID JID with a device, decoded with an agent. - // The Display trait MUST NOT show the agent number. let jid2 = Jid { user: "12345.6789".into(), - server: Cow::Borrowed("lid"), + server: Server::Lid, device: 25, - agent: 1, // This agent would be decoded from binary but should be ignored in display + agent: 1, integrator: 0, }; - // Expected: "12345.6789:25@lid" - // Buggy: "12345.6789.1:25@lid" assert_eq!(jid2.to_string(), "12345.6789:25@lid"); - // Failure Case 3: A JID that was decoded as "hosted" because of its agent. - // The Display trait MUST NOT show the agent number. let jid3 = Jid { user: "1234567890".into(), - server: Cow::Borrowed("hosted"), + server: Server::Hosted, device: 15, agent: 2, integrator: 0, }; - // Expected: "1234567890:15@hosted" - // Buggy: "1234567890.2:15@hosted" assert_eq!(jid3.to_string(), "1234567890:15@hosted"); - // Verification Case: A generic JID where the agent SHOULD be displayed. + // Agent SHOULD be displayed for non-AD servers (e.g., bot, interop) let jid4 = Jid { user: "user".into(), - server: Cow::Owned("custom.net".to_string()), + server: Server::Bot, device: 10, agent: 5, integrator: 0, }; - // The agent should be displayed because the server is not a special AD-JID type - assert_eq!(jid4.to_string(), "user.5:10@custom.net"); + assert_eq!(jid4.to_string(), "user.5:10@bot"); } #[test] @@ -1183,7 +1208,7 @@ mod tests { assert_eq!(parsed.server, "broadcast"); // Regular broadcast list should NOT be status broadcast - let broadcast_list = Jid::new("12345", BROADCAST_SERVER); + let broadcast_list = Jid::new("12345", Server::Broadcast); assert!(broadcast_list.is_broadcast_list()); assert!(!broadcast_list.is_status_broadcast()); } diff --git a/wacore/binary/src/lib.rs b/wacore/binary/src/lib.rs index 9122c921e..7e7e53710 100644 --- a/wacore/binary/src/lib.rs +++ b/wacore/binary/src/lib.rs @@ -15,8 +15,14 @@ pub mod util; pub use attrs::{AttrParser, AttrParserRef}; pub use compact_str::CompactString; pub use error::{BinaryError, Result}; +pub use jid::{ + BOT_SERVER, BROADCAST_SERVER, DEFAULT_USER_SERVER, DeviceKey, GROUP_SERVER, HIDDEN_USER_SERVER, + HOSTED_LID_SERVER, HOSTED_SERVER, INTEROP_SERVER, Jid, JidExt, JidRef, LEGACY_USER_SERVER, + MESSENGER_SERVER, MessageId, MessageServerId, NEWSLETTER_SERVER, SERVER_JID, + STATUS_BROADCAST_USER, Server, +}; pub use marshal::{ marshal, marshal_auto, marshal_exact, marshal_ref, marshal_ref_auto, marshal_ref_exact, marshal_ref_to, marshal_ref_to_vec, marshal_to, marshal_to_vec, }; -pub use node::{Node, NodeRef, NodeValue}; +pub use node::{Attrs, Node, NodeContent, NodeContentRef, NodeRef, NodeValue, OwnedNodeRef}; diff --git a/wacore/binary/src/node.rs b/wacore/binary/src/node.rs index 0fc2fd025..7131de0f3 100644 --- a/wacore/binary/src/node.rs +++ b/wacore/binary/src/node.rs @@ -278,18 +278,20 @@ pub type AttrsRef<'a> = Vec<(Cow<'a, str>, ValueRef<'a>)>; /// A decoded attribute value that can be either a string or a structured JID. /// This avoids string allocation when decoding JID tokens - the JidRef is returned /// directly and only converted to a string when actually needed. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, yoke::Yokeable)] pub enum ValueRef<'a> { String(Cow<'a, str>), Jid(JidRef<'a>), } impl<'a> ValueRef<'a> { - /// Get the value as a string slice, if it's a string variant. - pub fn as_str(&self) -> Option<&str> { + /// String view of the value. Works for both variants. + /// - String variant: Cow::Borrowed — zero copy + /// - Jid variant: Cow::Owned — allocates only when needed + pub fn as_str(&self) -> Cow<'a, str> { match self { - ValueRef::String(s) => Some(s.as_ref()), - ValueRef::Jid(_) => None, + ValueRef::String(s) => s.clone(), + ValueRef::Jid(j) => Cow::Owned(j.to_string()), } } @@ -308,15 +310,6 @@ impl<'a> ValueRef<'a> { ValueRef::String(s) => Jid::from_str(s.as_ref()).ok(), } } - - /// Convert to a string, formatting the JID if necessary. - /// Returns a Cow to avoid allocation when the value is already a string. - pub fn to_string_cow(&self) -> Cow<'a, str> { - match self { - ValueRef::String(s) => s.clone(), - ValueRef::Jid(j) => Cow::Owned(j.to_string()), - } - } } use std::str::FromStr; @@ -340,7 +333,7 @@ pub enum NodeContent { Nodes(Vec<Node>), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, yoke::Yokeable)] pub enum NodeContentRef<'a> { Bytes(Cow<'a, [u8]>), String(Cow<'a, str>), @@ -368,7 +361,7 @@ pub struct Node { pub content: Option<NodeContent>, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, yoke::Yokeable)] pub struct NodeRef<'a> { pub tag: Cow<'a, str>, pub attrs: AttrsRef<'a>, @@ -401,7 +394,7 @@ impl Node { NodeValue::String(s) => ValueRef::String(Cow::Borrowed(s.as_str())), NodeValue::Jid(j) => ValueRef::Jid(JidRef { user: Cow::Borrowed(&j.user), - server: Cow::Borrowed(&j.server), + server: j.server, agent: j.agent, device: j.device, integrator: j.integrator, @@ -478,7 +471,7 @@ impl<'a> NodeRef<'a> { } } - pub fn attr_parser(&'a self) -> AttrParserRef<'a> { + pub fn attrs(&self) -> AttrParserRef<'_> { AttrParserRef::new(self) } @@ -528,6 +521,40 @@ impl<'a> NodeRef<'a> { .and_then(|nodes| nodes.iter().find(|node| node.tag == tag)) } + /// Extract text content, handling both String and Bytes (lossy UTF-8). + pub fn content_as_string(&self) -> Option<CompactString> { + match self.content.as_deref() { + Some(NodeContentRef::String(s)) => Some(CompactString::from(s.as_ref())), + Some(NodeContentRef::Bytes(b)) => Some(CompactString::from( + String::from_utf8_lossy(b.as_ref()).as_ref(), + )), + _ => None, + } + } + + /// Zero-copy byte content, if this node has Bytes content. + pub fn content_bytes(&self) -> Option<&[u8]> { + match self.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => Some(b.as_ref()), + _ => None, + } + } + + /// Zero-copy string content, if this node has String content. + pub fn content_str(&self) -> Option<&str> { + match self.content.as_deref() { + Some(NodeContentRef::String(s)) => Some(s.as_ref()), + _ => None, + } + } + + /// Child nodes from content, if this node has Nodes content. + /// Alias for `children()`. + #[inline] + pub fn content_nodes(&self) -> Option<&[NodeRef<'a>]> { + self.children() + } + pub fn to_owned(&self) -> Node { Node { tag: intern_cow(&self.tag), @@ -552,3 +579,115 @@ impl<'a> NodeRef<'a> { } } } + +// --------------------------------------------------------------------------- +// OwnedNodeRef — self-referential zero-copy node via yoke +// --------------------------------------------------------------------------- + +use yoke::Yoke; + +/// A decoded node that owns its decompressed buffer. The inner `NodeRef` +/// borrows string/byte payloads directly from the buffer, avoiding copies. +/// Container allocations (attribute Vec, child Vec) still occur during decode. +/// +/// Wrap in `Arc<OwnedNodeRef>` for cheap sharing across handlers. +pub struct OwnedNodeRef { + inner: Yoke<NodeRef<'static>, Vec<u8>>, +} + +impl OwnedNodeRef { + /// Decode a node from an owned buffer. The buffer should be the raw + /// binary-protocol bytes (after decompression, without the leading + /// format byte which `unpack` already strips). + pub fn new(buffer: Vec<u8>) -> crate::error::Result<Self> { + let inner = Yoke::try_attach_to_cart(buffer, |buf| crate::marshal::unmarshal_ref(buf))?; + Ok(Self { inner }) + } + + /// Access the borrowed node. + #[inline] + pub fn get(&self) -> &NodeRef<'_> { + self.inner.get() + } + + /// Convert to an owned `Node`, cloning all data out of the buffer. + /// Use sparingly — this is the allocation path that yoke is designed to avoid. + pub fn to_owned_node(&self) -> Node { + self.inner.get().to_owned() + } + + /// The tag name of this node. + #[inline] + pub fn tag(&self) -> &str { + &self.get().tag + } + + /// Get an attribute parser for this node. + #[inline] + pub fn attrs(&self) -> AttrParserRef<'_> { + self.get().attrs() + } + + /// Look up a single attribute by key. + #[inline] + pub fn get_attr(&self, key: &str) -> Option<&ValueRef<'_>> { + self.get().get_attr(key) + } + + /// Get child nodes, if content is a node list. + #[inline] + pub fn children(&self) -> Option<&[NodeRef<'_>]> { + self.get().children() + } + + /// Find a child node by tag. + #[inline] + pub fn get_optional_child(&self, tag: &str) -> Option<&NodeRef<'_>> { + self.get().get_optional_child(tag) + } + + /// Find a child by traversing a path of tags. + #[inline] + pub fn get_optional_child_by_tag(&self, tags: &[&str]) -> Option<&NodeRef<'_>> { + self.get().get_optional_child_by_tag(tags) + } + + /// Get children matching a tag. + #[inline] + pub fn get_children_by_tag<'b>( + &'b self, + tag: &'b str, + ) -> impl Iterator<Item = &'b NodeRef<'b>> { + self.get().get_children_by_tag(tag) + } + + /// Zero-copy byte content, if this node has Bytes content. + #[inline] + pub fn content_bytes(&self) -> Option<&[u8]> { + self.get().content_bytes() + } + + /// Zero-copy string content, if this node has String content. + #[inline] + pub fn content_str(&self) -> Option<&str> { + self.get().content_str() + } + + /// Child nodes from content, if this node has Nodes content. + #[inline] + pub fn content_nodes(&self) -> Option<&[NodeRef<'_>]> { + self.get().content_nodes() + } + + /// Extract text content, handling both String and Bytes (lossy UTF-8). + #[inline] + pub fn content_as_string(&self) -> Option<CompactString> { + self.get().content_as_string() + } +} + +impl std::fmt::Debug for OwnedNodeRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.inner.get().fmt(f) + } +} diff --git a/wacore/derive/src/lib.rs b/wacore/derive/src/lib.rs index a312f8823..b482effd7 100644 --- a/wacore/derive/src/lib.rs +++ b/wacore/derive/src/lib.rs @@ -326,7 +326,7 @@ pub fn derive_protocol_node(input: TokenStream) -> TokenStream { builder.build() } - fn try_from_node(node: &::wacore_binary::node::Node) -> ::anyhow::Result<Self> { + fn try_from_node_ref(node: &::wacore_binary::node::NodeRef<'_>) -> ::anyhow::Result<Self> { if node.tag != #tag { return Err(::anyhow::anyhow!("expected <{}>, got <{}>", #tag, node.tag)); } @@ -388,7 +388,7 @@ fn generate_empty_impl(name: &syn::Ident, tag: &str) -> proc_macro2::TokenStream ::wacore_binary::builder::NodeBuilder::new(#tag).build() } - fn try_from_node(node: &::wacore_binary::node::Node) -> ::anyhow::Result<Self> { + fn try_from_node_ref(node: &::wacore_binary::node::NodeRef<'_>) -> ::anyhow::Result<Self> { if node.tag != #tag { return Err(::anyhow::anyhow!("expected <{}>, got <{}>", #tag, node.tag)); } diff --git a/wacore/src/appstate_sync.rs b/wacore/src/appstate_sync.rs index 4f35ab405..8c0d87528 100644 --- a/wacore/src/appstate_sync.rs +++ b/wacore/src/appstate_sync.rs @@ -9,12 +9,15 @@ use thiserror::Error; use crate::appstate::hash::HashState; use crate::appstate::keys::ExpandedAppStateKeys; -use crate::appstate::patch_decode::{PatchList, WAPatchName, parse_patch_list, parse_patch_lists}; +use crate::appstate::patch_decode::{ + PatchList, WAPatchName, parse_patch_list, parse_patch_list_ref, parse_patch_lists, + parse_patch_lists_ref, +}; use crate::appstate::{ collect_key_ids_from_patch_list, expand_app_state_keys, process_patch, process_snapshot, }; use crate::store::traits::Backend; -use wacore_binary::node::Node; +use wacore_binary::{Node, NodeRef}; use waproto::whatsapp as wa; // Re-export Mutation from appstate for convenience @@ -92,6 +95,66 @@ impl AppStateProcessor { Ok(()) } + pub async fn decode_patch_list_ref<FDownload>( + &self, + stanza_root: &NodeRef<'_>, + download: FDownload, + validate_macs: bool, + ) -> Result<(Vec<Mutation>, HashState, PatchList)> + where + FDownload: Fn(&wa::ExternalBlobReference) -> Result<Vec<u8>> + Send + Sync, + { + let mut pl = parse_patch_list_ref(stanza_root)?; + + // Download external snapshot if present (matches WhatsApp Web behavior) + if pl.snapshot.is_none() + && let Some(ext) = &pl.snapshot_ref + && let Ok(data) = download(ext) + && let Ok(snapshot) = wa::SyncdSnapshot::decode(data.as_slice()) + { + pl.snapshot = Some(snapshot); + } + + // Download external mutations for each patch (matches WhatsApp Web behavior) + for patch in &mut pl.patches { + if let Some(ext) = &patch.external_mutations { + let patch_version = patch.version.as_ref().and_then(|v| v.version).unwrap_or(0); + match download(ext) { + Ok(data) => match wa::SyncdMutations::decode(data.as_slice()) { + Ok(ext_mutations) => { + log::trace!( + target: "AppState", + "Downloaded external mutations for patch v{}: {} mutations (inline had {})", + patch_version, + ext_mutations.mutations.len(), + patch.mutations.len() + ); + patch.mutations = ext_mutations.mutations; + } + Err(e) => { + log::warn!( + target: "AppState", + "Failed to decode external mutations for patch v{}: {}", + patch_version, + e + ); + } + }, + Err(e) => { + log::warn!( + target: "AppState", + "Failed to download external mutations for patch v{}: {}", + patch_version, + e + ); + } + } + } + } + + self.process_patch_list(pl, validate_macs).await + } + pub async fn decode_patch_list<FDownload>( &self, stanza_root: &Node, @@ -153,6 +216,20 @@ impl AppStateProcessor { self.process_patch_list(pl, validate_macs).await } + pub async fn decode_multi_patch_list_ref<FDownload>( + &self, + stanza_root: &NodeRef<'_>, + download: &FDownload, + validate_macs: bool, + ) -> Result<Vec<(Vec<Mutation>, HashState, PatchList)>> + where + FDownload: Fn(&wa::ExternalBlobReference) -> Result<Vec<u8>> + Send + Sync, + { + let patch_lists = parse_patch_lists_ref(stanza_root)?; + self.process_patch_lists(patch_lists, download, validate_macs) + .await + } + /// Decode a multi-collection IQ response into per-collection results. /// Each collection is parsed and processed independently. pub async fn decode_multi_patch_list<FDownload>( @@ -165,6 +242,19 @@ impl AppStateProcessor { FDownload: Fn(&wa::ExternalBlobReference) -> Result<Vec<u8>> + Send + Sync, { let patch_lists = parse_patch_lists(stanza_root)?; + self.process_patch_lists(patch_lists, download, validate_macs) + .await + } + + async fn process_patch_lists<FDownload>( + &self, + patch_lists: Vec<PatchList>, + download: &FDownload, + validate_macs: bool, + ) -> Result<Vec<(Vec<Mutation>, HashState, PatchList)>> + where + FDownload: Fn(&wa::ExternalBlobReference) -> Result<Vec<u8>> + Send + Sync, + { let mut results = Vec::with_capacity(patch_lists.len()); for mut pl in patch_lists { diff --git a/wacore/src/client/context.rs b/wacore/src/client/context.rs index 44bc2bccc..95d9cbd4d 100644 --- a/wacore/src/client/context.rs +++ b/wacore/src/client/context.rs @@ -3,7 +3,7 @@ use crate::types::message::AddressingMode; use async_trait::async_trait; use std::collections::HashMap; use wacore_binary::CompactString; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; fn build_pn_to_lid_map(lid_to_pn_map: &HashMap<CompactString, Jid>) -> HashMap<CompactString, Jid> { lid_to_pn_map diff --git a/wacore/src/ib.rs b/wacore/src/ib.rs index 953b32429..1f28737af 100644 --- a/wacore/src/ib.rs +++ b/wacore/src/ib.rs @@ -5,7 +5,7 @@ use crate::protocol::ProtocolNode; use anyhow::Result; use wacore_binary::builder::NodeBuilder; -use wacore_binary::node::Node; +use wacore_binary::{Node, NodeRef}; /// Unified session telemetry node. /// @@ -78,7 +78,7 @@ impl ProtocolNode for IbStanza { .build() } - fn try_from_node(node: &Node) -> Result<Self> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self> { if node.tag != "ib" { return Err(anyhow::anyhow!("expected <ib>, got <{}>", node.tag)); } @@ -86,7 +86,9 @@ impl ProtocolNode for IbStanza { if let Some(children) = node.children() { for child in children { if child.tag == "unified_session" { - return Ok(Self::unified_session(UnifiedSession::try_from_node(child)?)); + return Ok(Self::unified_session(UnifiedSession::try_from_node_ref( + child, + )?)); } } } diff --git a/wacore/src/iq/blocklist.rs b/wacore/src/iq/blocklist.rs index e3bf2f09f..fb371f15f 100644 --- a/wacore/src/iq/blocklist.rs +++ b/wacore/src/iq/blocklist.rs @@ -11,8 +11,8 @@ use crate::request::InfoQuery; use anyhow::Result; use log::warn; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{Node, NodeContent, NodeRef}; /// IQ namespace for blocklist operations. pub const BLOCKLIST_IQ_NAMESPACE: &str = "blocklist"; /// Action to perform on a blocklist entry. @@ -81,7 +81,7 @@ impl ProtocolNode for BlocklistResponse { NodeBuilder::new("list").children(children).build() } - fn try_from_node(node: &Node) -> Result<Self> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self> { // Response can be either: // 1. <list><item .../></list> // 2. Direct <item .../> children in the response node @@ -90,13 +90,10 @@ impl ProtocolNode for BlocklistResponse { } else { node.get_children_by_tag("item") } - .filter_map(|item| match BlocklistEntry::try_from_node(item) { + .filter_map(|item| match BlocklistEntry::try_from_node_ref(item) { Ok(entry) => Some(entry), Err(e) => { - warn!( - target: "blocklist", - "Failed to parse blocklist entry: {e}" - ); + warn!(target: "blocklist", "Failed to parse blocklist entry: {e}"); None } }) @@ -113,12 +110,25 @@ impl IqSpec for GetBlocklistSpec { type Response = Vec<BlocklistEntry>; fn build_iq(&self) -> InfoQuery<'static> { - InfoQuery::get(BLOCKLIST_IQ_NAMESPACE, Jid::new("", SERVER_JID), None) + InfoQuery::get(BLOCKLIST_IQ_NAMESPACE, Jid::new("", Server::Pn), None) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { - let blocklist = BlocklistResponse::try_from_node(response)?; - Ok(blocklist.entries) + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { + // BlocklistResponse checks for a <list> child or direct <item> children + let entries = if let Some(list) = response.get_optional_child("list") { + list.get_children_by_tag("item") + } else { + response.get_children_by_tag("item") + } + .filter_map(|item| match BlocklistEntry::try_from_node_ref(item) { + Ok(entry) => Some(entry), + Err(e) => { + warn!(target: "blocklist", "Failed to parse blocklist entry: {e}"); + None + } + }) + .collect(); + Ok(entries) } } @@ -154,12 +164,12 @@ impl IqSpec for UpdateBlocklistSpec { fn build_iq(&self) -> InfoQuery<'static> { InfoQuery::set( BLOCKLIST_IQ_NAMESPACE, - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![self.request.clone().into_node()])), ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response> { Ok(()) } } diff --git a/wacore/src/iq/business.rs b/wacore/src/iq/business.rs index e15f47246..4887501bd 100644 --- a/wacore/src/iq/business.rs +++ b/wacore/src/iq/business.rs @@ -5,8 +5,8 @@ use crate::iq::node::optional_attr; use crate::iq::spec::IqSpec; use crate::request::InfoQuery; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{NodeContent, NodeContentRef, NodeRef}; #[derive(Debug, Clone, PartialEq, Eq, StringEnum)] pub enum DayOfWeek { @@ -52,10 +52,10 @@ impl serde::Serialize for BusinessHourMode { } } -fn node_text(node: &Node) -> Option<String> { - match &node.content { - Some(NodeContent::String(s)) => Some(s.to_string()), - Some(NodeContent::Bytes(b)) => String::from_utf8(b.clone()).ok(), +fn node_text(node: &NodeRef<'_>) -> Option<String> { + match node.content.as_deref() { + Some(NodeContentRef::String(s)) => Some(s.to_string()), + Some(NodeContentRef::Bytes(b)) => std::str::from_utf8(b).ok().map(|s| s.to_string()), _ => None, } } @@ -114,7 +114,7 @@ impl IqSpec for BusinessProfileSpec { fn build_iq(&self) -> InfoQuery<'static> { InfoQuery::get( "w:biz", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![ NodeBuilder::new("business_profile") .attr("v", "244") @@ -124,7 +124,7 @@ impl IqSpec for BusinessProfileSpec { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { let biz_node = match response.get_optional_child("business_profile") { Some(n) => n, None => return Ok(None), diff --git a/wacore/src/iq/chatstate.rs b/wacore/src/iq/chatstate.rs index 244d9f40d..5f65e8a38 100644 --- a/wacore/src/iq/chatstate.rs +++ b/wacore/src/iq/chatstate.rs @@ -20,12 +20,12 @@ //! ``` use crate::StringEnum; -use crate::iq::node::optional_jid; use crate::protocol::ProtocolNode; use anyhow::Result; use thiserror::Error; -use wacore_binary::jid::Jid; -use wacore_binary::node::Node; +use wacore_binary::Jid; +use wacore_binary::Node; +use wacore_binary::NodeRef; /// Error type for chatstate parsing failures. #[derive(Debug, Error)] @@ -75,11 +75,15 @@ impl ReceivedChatState { /// - `<composing/>` → Typing /// - `<composing media="audio"/>` → RecordingAudio /// - `<paused/>` → Idle - pub fn from_child_node(child: &Node) -> Self { + pub fn from_child_node(child: &NodeRef<'_>) -> Self { match child.tag.as_ref() { "composing" => { // Check for media="audio" to distinguish recording from typing - if child.attrs.get("media").is_some_and(|v| v == "audio") { + if child + .get_attr("media") + .map(|v| v.as_str()) + .is_some_and(|s| s == "audio") + { Self::RecordingAudio } else { Self::Typing @@ -128,15 +132,16 @@ impl ChatstateStanza { /// /// Use this method when you need to distinguish between different failure modes /// (e.g., to ignore self-echo chatstates without logging warnings). - pub fn parse(node: &Node) -> Result<Self, ChatstateParseError> { + pub fn parse(node: &NodeRef<'_>) -> Result<Self, ChatstateParseError> { if node.tag != "chatstate" { return Err(ChatstateParseError::WrongTag(node.tag.to_string())); } - let from = match optional_jid(node, "from")? { + let mut attrs = node.attrs(); + let from = match attrs.optional_jid("from") { Some(jid) => jid, None => { - if optional_jid(node, "to")?.is_some() { + if attrs.optional_jid("to").is_some() { return Err(ChatstateParseError::SelfEcho); } return Err(ChatstateParseError::MissingFrom); @@ -144,7 +149,7 @@ impl ChatstateStanza { }; // Parse 'participant' attribute (optional, present in groups) - let source = match optional_jid(node, "participant")? { + let source = match attrs.optional_jid("participant") { Some(participant) => ChatstateSource::Group { from, participant }, None => ChatstateSource::User { from }, }; @@ -170,7 +175,7 @@ impl ProtocolNode for ChatstateStanza { unimplemented!("ChatstateStanza is incoming-only") } - fn try_from_node(node: &Node) -> Result<Self> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self> { Self::parse(node).map_err(Into::into) } } @@ -267,7 +272,7 @@ mod tests { .children([NodeBuilder::new("composing").build()]) .build(); - let result = ChatstateStanza::parse(&node); + let result = ChatstateStanza::parse(&node.as_node_ref()); assert!(matches!(result, Err(ChatstateParseError::MissingFrom))); } @@ -279,7 +284,7 @@ mod tests { .children([NodeBuilder::new("composing").build()]) .build(); - let result = ChatstateStanza::parse(&node); + let result = ChatstateStanza::parse(&node.as_node_ref()); assert!(matches!(result, Err(ChatstateParseError::SelfEcho))); } @@ -289,7 +294,7 @@ mod tests { .attr("from", "1234567890@s.whatsapp.net") .build(); - let result = ChatstateStanza::parse(&node); + let result = ChatstateStanza::parse(&node.as_node_ref()); assert!(matches!(result, Err(ChatstateParseError::WrongTag(_)))); } @@ -322,7 +327,7 @@ mod tests { .children([NodeBuilder::new("composing").build()]) .build(); - let stanza = ChatstateStanza::parse(&node).unwrap(); + let stanza = ChatstateStanza::parse(&node.as_node_ref()).unwrap(); assert!(matches!(stanza.source, ChatstateSource::User { .. })); assert_eq!(stanza.state, ReceivedChatState::Typing); @@ -336,7 +341,7 @@ mod tests { fn test_parse_jid_attribute_as_jid_type() { // In the binary protocol, JID attributes are stored as actual JID types, // not strings. This test simulates that by passing a Jid directly to attr(). - use wacore_binary::jid::Jid; + use wacore_binary::Jid; let jid: Jid = "236395184570386@lid".parse().unwrap(); let node = NodeBuilder::new("chatstate") @@ -344,7 +349,7 @@ mod tests { .children([NodeBuilder::new("composing").build()]) .build(); - let stanza = ChatstateStanza::parse(&node).unwrap(); + let stanza = ChatstateStanza::parse(&node.as_node_ref()).unwrap(); assert!(matches!(stanza.source, ChatstateSource::User { .. })); assert_eq!(stanza.state, ReceivedChatState::Typing); @@ -357,7 +362,7 @@ mod tests { #[test] fn test_parse_group_chatstate_with_jid_types() { // Test group chatstate with JID-typed attributes (as binary protocol stores them) - use wacore_binary::jid::Jid; + use wacore_binary::Jid; let group_jid: Jid = "123456789-1234567890@g.us".parse().unwrap(); let participant_jid: Jid = "236395184570386@lid".parse().unwrap(); @@ -368,7 +373,7 @@ mod tests { .children([NodeBuilder::new("composing").build()]) .build(); - let stanza = ChatstateStanza::parse(&node).unwrap(); + let stanza = ChatstateStanza::parse(&node.as_node_ref()).unwrap(); assert!(matches!(stanza.source, ChatstateSource::Group { .. })); assert_eq!(stanza.state, ReceivedChatState::Typing); diff --git a/wacore/src/iq/contacts.rs b/wacore/src/iq/contacts.rs index 9616c2226..fe61d2894 100644 --- a/wacore/src/iq/contacts.rs +++ b/wacore/src/iq/contacts.rs @@ -27,8 +27,8 @@ use crate::iq::tctoken::build_tc_token_node; use crate::request::InfoQuery; use anyhow::anyhow; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{NodeContent, NodeRef}; /// Profile picture information. #[derive(Debug, Clone)] @@ -122,13 +122,13 @@ impl IqSpec for ProfilePictureSpec { InfoQuery::get( "w:profile:picture", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![picture_builder.build()])), ) .with_target_ref(&self.jid) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { let picture_node = match response.get_optional_child("picture") { Some(p) => p, None => return Ok(None), @@ -268,7 +268,7 @@ impl IqSpec for SetProfilePictureSpec { let mut iq = InfoQuery::set( "w:profile:picture", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![picture_builder.build()])), ); @@ -279,7 +279,7 @@ impl IqSpec for SetProfilePictureSpec { iq } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { if self.image_data.is_some() { // Set operation: server must return <picture id="..."/> let picture_node = response @@ -350,7 +350,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert!(result.is_some()); let pic = result.unwrap(); @@ -374,7 +374,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert!(result.is_none()); } @@ -385,7 +385,7 @@ mod tests { let response = NodeBuilder::new("iq").attr("type", "result").build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert!(result.is_none()); } @@ -490,7 +490,7 @@ mod tests { .children([NodeBuilder::new("picture").attr("id", "987654321").build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.id, "987654321"); } } diff --git a/wacore/src/iq/devices.rs b/wacore/src/iq/devices.rs index 7098cac07..1a7dc4606 100644 --- a/wacore/src/iq/devices.rs +++ b/wacore/src/iq/devices.rs @@ -5,8 +5,8 @@ use std::time::Duration; use crate::iq::spec::IqSpec; use crate::request::InfoQuery; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{NodeContent, NodeRef}; /// WA Web uses a 3s timeout for the logout IQ (Socket/Model.js). const LOGOUT_TIMEOUT: Duration = Duration::from_secs(3); @@ -33,13 +33,13 @@ impl IqSpec for RemoveCompanionDeviceSpec { InfoQuery::set( "md", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![child])), ) .with_timeout(LOGOUT_TIMEOUT) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { Ok(()) } } diff --git a/wacore/src/iq/dirty.rs b/wacore/src/iq/dirty.rs index 17fcb5ad4..41d158853 100644 --- a/wacore/src/iq/dirty.rs +++ b/wacore/src/iq/dirty.rs @@ -2,8 +2,8 @@ use crate::StringEnum; use crate::iq::spec::IqSpec; use crate::request::InfoQuery; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{Node, NodeContent, NodeRef}; pub const DIRTY_NAMESPACE: &str = "urn:xmpp:whatsapp:dirty"; @@ -110,12 +110,12 @@ impl IqSpec for CleanDirtyBitsSpec { InfoQuery::set( DIRTY_NAMESPACE, - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(children)), ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { // Clean dirty bits just needs a successful response Ok(()) } @@ -227,7 +227,7 @@ mod tests { let spec = CleanDirtyBitsSpec::single(DirtyBit::new(DirtyType::AccountSync)); let response = NodeBuilder::new("iq").attr("type", "result").build(); - let result = spec.parse_response(&response); + let result = spec.parse_response(&response.as_node_ref()); assert!(result.is_ok()); } diff --git a/wacore/src/iq/groups.rs b/wacore/src/iq/groups.rs index 9a50289e4..cd06b2a2d 100644 --- a/wacore/src/iq/groups.rs +++ b/wacore/src/iq/groups.rs @@ -1,5 +1,5 @@ use crate::StringEnum; -use crate::iq::node::{collect_children, optional_attr, required_attr, required_child}; +use crate::iq::node::{collect_children, required_attr, required_child}; use crate::iq::spec::IqSpec; use crate::protocol::ProtocolNode; use crate::request::InfoQuery; @@ -7,8 +7,8 @@ use anyhow::{Result, anyhow}; use std::num::NonZeroU32; use typed_builder::TypedBuilder; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{GROUP_SERVER, Jid}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{Node, NodeContent, NodeRef}; // Re-export AddressingMode from types::message for convenience pub use crate::types::message::AddressingMode; @@ -367,18 +367,16 @@ impl ProtocolNode for GroupParticipantResponse { builder.build() } - fn try_from_node(node: &Node) -> Result<Self> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self> { if node.tag != "participant" { return Err(anyhow!("expected <participant>, got <{}>", node.tag)); } - let jid = node - .attrs() + let mut attrs = node.attrs(); + let jid = attrs .optional_jid("jid") .ok_or_else(|| anyhow!("participant missing required 'jid' attribute"))?; - let phone_number = node.attrs().optional_jid("phone_number"); - // Default to Member for unknown participant types to avoid failing the whole group parse - let participant_type = node - .attrs() + let phone_number = attrs.optional_jid("phone_number"); + let participant_type = attrs .optional_string("type") .and_then(|s| ParticipantType::try_from(s.as_ref()).ok()) .unwrap_or(ParticipantType::Member); @@ -559,55 +557,46 @@ impl ProtocolNode for GroupInfoResponse { builder.children(children).build() } - fn try_from_node(node: &Node) -> Result<Self> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self> { + use wacore_binary::NodeContentRef; if node.tag != "group" { return Err(anyhow!("expected <group>, got <{}>", node.tag)); } - let id_str = required_attr(node, "id")?; + let mut attrs = node.attrs(); + let id_str = attrs + .optional_string("id") + .ok_or_else(|| anyhow!("missing required attribute id"))?; let id = if id_str.contains('@') { id_str.parse()? } else { - Jid::group(id_str) + Jid::group(id_str.as_ref()) }; let subject = GroupSubject::new_unchecked( - optional_attr(node, "subject") + attrs + .optional_string("subject") .as_deref() .unwrap_or_default(), ); let addressing_mode = AddressingMode::try_from( - optional_attr(node, "addressing_mode") + attrs + .optional_string("addressing_mode") .as_deref() .unwrap_or("pn"), )?; - let participants = collect_children::<GroupParticipantResponse>(node, "participant")?; - - // Parse attributes - let creator = node - .attrs() - .optional_string("creator") - .and_then(|s| s.parse::<Jid>().ok()); - let creation_time = node - .attrs() - .optional_string("creation") - .and_then(|s| s.parse::<u64>().ok()); - let subject_time = node - .attrs() - .optional_string("s_t") - .and_then(|s| s.parse::<u64>().ok()); - let subject_owner = node - .attrs() - .optional_string("s_o") - .and_then(|s| s.parse::<Jid>().ok()); - let size = node - .attrs() + let creator = attrs.optional_jid("creator"); + let creation_time = attrs.optional_u64("creation"); + let subject_time = attrs.optional_u64("s_t"); + let subject_owner = attrs.optional_jid("s_o"); + let size = attrs .optional_string("size") .and_then(|s| s.parse::<u32>().ok()); - // Parse settings from child nodes + let participants = collect_children::<GroupParticipantResponse>(node, "participant")?; + let is_locked = node.get_optional_child_by_tag(&["locked"]).is_some(); let is_announcement = node.get_optional_child_by_tag(&["announcement"]).is_some(); @@ -627,19 +616,18 @@ impl ProtocolNode for GroupInfoResponse { let member_add_mode = node .get_optional_child_by_tag(&["member_add_mode"]) - .and_then(|n| match &n.content { - Some(NodeContent::String(s)) => MemberAddMode::try_from(s.as_str()).ok(), + .and_then(|n| match n.content.as_deref() { + Some(NodeContentRef::String(s)) => MemberAddMode::try_from(s.as_ref()).ok(), _ => None, }); let member_link_mode = node .get_optional_child_by_tag(&["member_link_mode"]) - .and_then(|n| match &n.content { - Some(NodeContent::String(s)) => MemberLinkMode::try_from(s.as_str()).ok(), + .and_then(|n| match n.content.as_deref() { + Some(NodeContentRef::String(s)) => MemberLinkMode::try_from(s.as_ref()).ok(), _ => None, }); - // Description lives inside <description id="..." participant="..." t="..."><body>text</body></description> let description_node = node.get_optional_child_by_tag(&["description"]); let description = description_node .and_then(|n| n.get_optional_child("body")) @@ -654,7 +642,6 @@ impl ProtocolNode for GroupInfoResponse { .and_then(|n| n.attrs().optional_string("t")) .and_then(|s| s.parse::<u64>().ok()); - // Parse community fields let is_parent_group = node.get_optional_child_by_tag(&["parent"]).is_some(); let parent_group_jid = node .get_optional_child_by_tag(&["linked_parent"]) @@ -734,7 +721,7 @@ impl ProtocolNode for GroupParticipatingRequest { NodeBuilder::new("participating").children(children).build() } - fn try_from_node(node: &Node) -> Result<Self> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self> { if node.tag != "participating" { return Err(anyhow!("expected <participating>, got <{}>", node.tag)); } @@ -761,7 +748,7 @@ impl ProtocolNode for GroupParticipatingResponse { NodeBuilder::new("groups").children(children).build() } - fn try_from_node(node: &Node) -> Result<Self> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self> { if node.tag != "groups" { return Err(anyhow!("expected <groups>, got <{}>", node.tag)); } @@ -798,9 +785,9 @@ impl IqSpec for GroupQueryIq { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let group_node = required_child(response, "group")?; - GroupInfoResponse::try_from_node(group_node) + GroupInfoResponse::try_from_node_ref(group_node) } } @@ -820,16 +807,16 @@ impl IqSpec for GroupParticipatingIq { fn build_iq(&self) -> InfoQuery<'static> { InfoQuery::get( GROUP_IQ_NAMESPACE, - Jid::new("", GROUP_SERVER), + Jid::new("", Server::Group), Some(NodeContent::Nodes(vec![ GroupParticipatingRequest::new().into_node(), ])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let groups_node = required_child(response, "groups")?; - GroupParticipatingResponse::try_from_node(groups_node) + GroupParticipatingResponse::try_from_node_ref(groups_node) } } @@ -851,14 +838,14 @@ impl IqSpec for GroupCreateIq { fn build_iq(&self) -> InfoQuery<'static> { InfoQuery::set( GROUP_IQ_NAMESPACE, - Jid::new("", GROUP_SERVER), + Jid::new("", Server::Group), Some(NodeContent::Nodes(vec![build_create_group_node( &self.options, )])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let group_node = required_child(response, "group")?; let group_id_str = required_attr(group_node, "id")?; @@ -927,7 +914,7 @@ impl IqSpec for SetGroupSubjectIq { ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response> { Ok(()) } } @@ -1005,7 +992,7 @@ impl IqSpec for SetGroupDescriptionIq { ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response> { Ok(()) } } @@ -1042,12 +1029,12 @@ impl IqSpec for LeaveGroupIq { InfoQuery::set( GROUP_IQ_NAMESPACE, - Jid::new("", GROUP_SERVER), + Jid::new("", Server::Group), Some(NodeContent::Nodes(vec![leave_node])), ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response> { Ok(()) } } @@ -1098,7 +1085,7 @@ macro_rules! define_group_participant_iq { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let action_node = required_child(response, $action)?; collect_children::<ParticipantChangeResponse>(action_node, "participant") } @@ -1147,7 +1134,7 @@ macro_rules! define_group_participant_iq { ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response> { Ok(()) } } @@ -1220,7 +1207,7 @@ impl IqSpec for AddParticipantsIq { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let action_node = required_child(response, "add")?; collect_children::<ParticipantChangeResponse>(action_node, "participant") } @@ -1295,7 +1282,7 @@ impl IqSpec for GetGroupInviteLinkIq { } } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let invite_node = required_child(response, "invite")?; let code = required_attr(invite_node, "code")?; Ok(format!("https://chat.whatsapp.com/{code}")) @@ -1355,7 +1342,7 @@ impl IqSpec for SetGroupLockedIq { ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response> { Ok(()) } } @@ -1408,7 +1395,7 @@ impl IqSpec for SetGroupAnnouncementIq { ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response> { Ok(()) } } @@ -1471,7 +1458,7 @@ impl IqSpec for SetGroupEphemeralIq { ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response> { Ok(()) } } @@ -1519,7 +1506,7 @@ impl IqSpec for SetGroupMembershipApprovalIq { ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response> { Ok(()) } } @@ -1599,7 +1586,7 @@ impl IqSpec for LinkSubgroupsIq { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let links_node = required_child(response, "links")?; let link_node = required_child(links_node, "link")?; @@ -1673,7 +1660,7 @@ impl IqSpec for UnlinkSubgroupsIq { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let unlink_node = required_child(response, "unlink")?; let mut groups = Vec::new(); @@ -1725,7 +1712,7 @@ impl IqSpec for DeleteCommunityIq { ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response> { Ok(()) } } @@ -1769,10 +1756,10 @@ impl IqSpec for QueryLinkedGroupIq { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let linked_node = required_child(response, "linked_group")?; let group_node = required_child(linked_node, "group")?; - GroupInfoResponse::try_from_node(group_node) + GroupInfoResponse::try_from_node_ref(group_node) } } @@ -1814,10 +1801,10 @@ impl IqSpec for JoinLinkedGroupIq { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let linked_node = required_child(response, "linked_group")?; let group_node = required_child(linked_node, "group")?; - GroupInfoResponse::try_from_node(group_node) + GroupInfoResponse::try_from_node_ref(group_node) } } @@ -1855,7 +1842,7 @@ impl IqSpec for GetLinkedGroupsParticipantsIq { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let container = required_child(response, "linked_groups_participants")?; // Participants may be direct children or nested inside <group> nodes. @@ -1895,7 +1882,7 @@ impl JoinGroupResult { } /// Shared response parser for group join IQs (both code-based and V4 invite). -fn parse_join_group_response(response: &Node) -> Result<JoinGroupResult> { +fn parse_join_group_response(response: &NodeRef<'_>) -> Result<JoinGroupResult> { if let Some(group_node) = response.get_optional_child("group") { let jid_str = required_attr(group_node, "jid")?; let jid: Jid = jid_str @@ -1935,7 +1922,7 @@ impl IqSpec for AcceptGroupInviteIq { type Response = JoinGroupResult; fn build_iq(&self) -> InfoQuery<'static> { - let to = Jid::new("", GROUP_SERVER); + let to = Jid::new("", Server::Group); InfoQuery::set_ref( GROUP_IQ_NAMESPACE, &to, @@ -1945,7 +1932,7 @@ impl IqSpec for AcceptGroupInviteIq { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { parse_join_group_response(response) } } @@ -1991,7 +1978,7 @@ impl IqSpec for AcceptGroupInviteV4Iq { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { parse_join_group_response(response) } } @@ -2022,7 +2009,7 @@ impl IqSpec for GetGroupInviteInfoIq { type Response = GroupInfoResponse; fn build_iq(&self) -> InfoQuery<'static> { - let to = Jid::new("", GROUP_SERVER); + let to = Jid::new("", Server::Group); InfoQuery::get_ref( GROUP_IQ_NAMESPACE, &to, @@ -2032,9 +2019,9 @@ impl IqSpec for GetGroupInviteInfoIq { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let group_node = required_child(response, "group")?; - GroupInfoResponse::try_from_node(group_node) + GroupInfoResponse::try_from_node_ref(group_node) } } @@ -2082,7 +2069,7 @@ impl IqSpec for GetMembershipRequestsIq { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let requests_node = response .get_optional_child("membership_approval_requests") .ok_or_else(|| anyhow!("missing membership_approval_requests"))?; @@ -2165,7 +2152,7 @@ impl IqSpec for MembershipRequestActionIq { ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { let action_node = required_child(response, "membership_requests_action")?; let action_tag = if self.approve { "approve" } else { "reject" }; let inner = required_child(action_node, action_tag)?; @@ -2214,7 +2201,7 @@ impl IqSpec for SetMemberAddModeIq { ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response> { Ok(()) } } @@ -2386,7 +2373,7 @@ mod tests { assert_eq!(iq.namespace, GROUP_IQ_NAMESPACE); assert_eq!(iq.query_type, InfoQueryType::Set); // Leave goes to g.us, not the group JID - assert_eq!(iq.to.server, GROUP_SERVER); + assert_eq!(iq.to.server, Server::Group); } #[test] @@ -2541,7 +2528,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result, "https://chat.whatsapp.com/AbCdEfGhIjKl"); } @@ -2737,7 +2724,7 @@ mod tests { .build(); let spec = LinkSubgroupsIq::new(&parent, std::slice::from_ref(&sub)); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.groups.len(), 1); assert_eq!(result.groups[0].jid, sub); assert!(result.groups[0].error.is_none()); @@ -2788,7 +2775,7 @@ mod tests { .build(); let spec = UnlinkSubgroupsIq::new(&parent, std::slice::from_ref(&sub), false); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.groups.len(), 1); assert_eq!(result.groups[0].jid, sub); assert_eq!(result.groups[0].error, Some(406)); diff --git a/wacore/src/iq/keepalive.rs b/wacore/src/iq/keepalive.rs index db154e0be..c150b1fcd 100644 --- a/wacore/src/iq/keepalive.rs +++ b/wacore/src/iq/keepalive.rs @@ -12,8 +12,8 @@ use crate::iq::spec::IqSpec; use crate::request::InfoQuery; use std::time::Duration; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::Node; +use wacore_binary::NodeRef; +use wacore_binary::{Jid, Server}; /// Keepalive ping to keep the connection alive. #[derive(Debug, Clone, Default)] @@ -39,14 +39,14 @@ impl IqSpec for KeepaliveSpec { type Response = (); fn build_iq(&self) -> InfoQuery<'static> { - let mut iq = InfoQuery::get("w:p", Jid::new("", SERVER_JID), None); + let mut iq = InfoQuery::get("w:p", Jid::new("", Server::Pn), None); if let Some(timeout) = self.timeout { iq = iq.with_timeout(timeout); } iq } - fn parse_response(&self, _response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { // Keepalive just needs a successful response, no parsing needed Ok(()) } @@ -82,7 +82,7 @@ mod tests { let spec = KeepaliveSpec::new(); let response = NodeBuilder::new("iq").build(); - let result = spec.parse_response(&response); + let result = spec.parse_response(&response.as_node_ref()); assert!(result.is_ok()); } } diff --git a/wacore/src/iq/mediaconn.rs b/wacore/src/iq/mediaconn.rs index 916012091..bd9360b88 100644 --- a/wacore/src/iq/mediaconn.rs +++ b/wacore/src/iq/mediaconn.rs @@ -22,8 +22,8 @@ use crate::protocol::ProtocolNode; use crate::request::InfoQuery; use anyhow::anyhow; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{Node, NodeContent, NodeRef}; #[derive(Debug, Clone, PartialEq, Eq, StringEnum)] pub enum HostType { @@ -189,7 +189,7 @@ impl ProtocolNode for MediaConnHostExtended { builder.build() } - fn try_from_node(node: &Node) -> Result<Self, anyhow::Error> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self, anyhow::Error> { if node.tag != "host" { return Err(anyhow!("expected <host>, got <{}>", node.tag)); } @@ -198,7 +198,7 @@ impl ProtocolNode for MediaConnHostExtended { let hostname = attrs .optional_string("hostname") .ok_or_else(|| anyhow!("missing hostname attribute"))? - .to_string(); + .into_owned(); let host_type = attrs .optional_string("type") .map(|s| HostType::from(s.as_ref())) @@ -300,7 +300,7 @@ impl ProtocolNode for MediaConnResponseExtended { builder.build() } - fn try_from_node(node: &Node) -> Result<Self, anyhow::Error> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self, anyhow::Error> { if node.tag != "media_conn" { return Err(anyhow!("expected <media_conn>, got <{}>", node.tag)); } @@ -309,17 +309,17 @@ impl ProtocolNode for MediaConnResponseExtended { let auth = attrs .optional_string("auth") .ok_or_else(|| anyhow!("missing auth attribute"))? - .to_string(); + .into_owned(); let ttl = attrs.optional_u64("ttl").unwrap_or(0); let auth_ttl = attrs.optional_u64("auth_ttl"); let max_buckets = attrs.optional_u64("max_buckets"); let ip_token = attrs.optional_string("ip_token").map(|s| s.into_owned()); let set_ip_token = attrs.optional_u64("set_ip_token"); - let mut hosts = Vec::new(); - for host_node in node.get_children_by_tag("host") { - hosts.push(MediaConnHostExtended::try_from_node(host_node)?); - } + let hosts = node + .get_children_by_tag("host") + .map(MediaConnHostExtended::try_from_node_ref) + .collect::<Result<Vec<_>, _>>()?; Ok(Self { auth, @@ -351,12 +351,12 @@ impl IqSpec for MediaConnSpec { InfoQuery::set( "w:m", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![media_conn_node])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { let media_conn_node = response .get_optional_child("media_conn") .ok_or_else(|| anyhow!("Missing media_conn node in response"))?; @@ -375,7 +375,7 @@ impl IqSpec for MediaConnSpec { let mut hosts: Vec<MediaConnHost> = media_conn_node .get_children_by_tag("host") .filter_map(|host_node| { - let ext = MediaConnHostExtended::try_from_node(host_node).ok()?; + let ext = MediaConnHostExtended::try_from_node_ref(host_node).ok()?; Some(MediaConnHost { hostname: ext.hostname, host_type: ext.host_type, @@ -444,7 +444,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.auth, "test-auth-token"); assert_eq!(result.ttl, 3600); @@ -460,7 +460,7 @@ mod tests { let response = NodeBuilder::new("iq").attr("type", "result").build(); - let result = spec.parse_response(&response); + let result = spec.parse_response(&response.as_node_ref()); assert!(result.is_err()); } diff --git a/wacore/src/iq/mex.rs b/wacore/src/iq/mex.rs index 2f9b257ec..e23741083 100644 --- a/wacore/src/iq/mex.rs +++ b/wacore/src/iq/mex.rs @@ -22,8 +22,8 @@ use anyhow::anyhow; use serde::{Deserialize, Serialize}; use serde_json::Value; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{NodeContent, NodeContentRef, NodeRef}; /// MEX GraphQL error extensions. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -134,20 +134,20 @@ impl IqSpec for MexQuerySpec { InfoQuery::get( "w:mex", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![query_node])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { let result_node = response .get_optional_child("result") .ok_or_else(|| anyhow!("Missing <result> node in MEX response"))?; // Handle both binary and string content from the server - let mex_response: MexResponse = match &result_node.content { - Some(NodeContent::Bytes(bytes)) => serde_json::from_slice(bytes)?, - Some(NodeContent::String(s)) => serde_json::from_str(s)?, + let mex_response: MexResponse = match result_node.content.as_deref() { + Some(NodeContentRef::Bytes(bytes)) => serde_json::from_slice(bytes)?, + Some(NodeContentRef::String(s)) => serde_json::from_str(s)?, _ => return Err(anyhow!("MEX result node content is not binary or string")), }; diff --git a/wacore/src/iq/node.rs b/wacore/src/iq/node.rs index 6d3261049..414111eea 100644 --- a/wacore/src/iq/node.rs +++ b/wacore/src/iq/node.rs @@ -1,80 +1,41 @@ -//! Helper functions for parsing protocol nodes in IQ responses. -//! -//! These functions provide a consistent way to extract required and optional -//! children/attributes from protocol nodes with clear error messages. - use std::borrow::Cow; use crate::protocol::ProtocolNode; use anyhow::anyhow; -use wacore_binary::jid::Jid; -use wacore_binary::node::Node; +use wacore_binary::NodeRef; -/// Get a required child node by tag, returning an error if not found. -pub fn required_child<'a>(node: &'a Node, tag: &str) -> Result<&'a Node, anyhow::Error> { +/// Get a required child node by tag from a `NodeRef`. +pub(crate) fn required_child<'a>( + node: &'a NodeRef<'_>, + tag: &str, +) -> Result<&'a NodeRef<'a>, anyhow::Error> { node.get_optional_child(tag) .ok_or_else(|| anyhow!("<{tag}> child not found")) } -/// Get an optional child node by tag. -pub fn optional_child<'a>(node: &'a Node, tag: &str) -> Option<&'a Node> { +/// Get an optional child node by tag from a `NodeRef`. +pub(crate) fn optional_child<'a>(node: &'a NodeRef<'_>, tag: &str) -> Option<&'a NodeRef<'a>> { node.get_optional_child(tag) } -/// Get a required string attribute, returning an error if not found. -/// -/// Handles both string and JID-typed attribute values transparently: -/// JID values are formatted to their string representation. -pub fn required_attr(node: &Node, key: &str) -> Result<String, anyhow::Error> { - node.attrs - .get(key) +/// Get a required string attribute from a `NodeRef`. +pub(crate) fn required_attr(node: &NodeRef<'_>, key: &str) -> Result<String, anyhow::Error> { + node.get_attr(key) .map(|v| v.to_string()) .ok_or_else(|| anyhow!("missing required attribute {key}")) } -/// Get an optional string attribute. -pub fn optional_attr<'a>(node: &'a Node, key: &str) -> Option<Cow<'a, str>> { +/// Get an optional string attribute from a `NodeRef`. +pub(crate) fn optional_attr<'a>(node: &'a NodeRef<'_>, key: &str) -> Option<Cow<'a, str>> { node.attrs().optional_string(key) } -/// Get an optional u64 attribute. -pub fn optional_u64(node: &Node, key: &str) -> Option<u64> { - node.attrs().optional_u64(key) -} - -/// Get a required JID attribute, returning an error if not found or invalid. -/// -/// This properly handles JID attributes stored as either: -/// - Direct JID values (binary protocol stores JIDs as structured data) -/// - String values that need to be parsed -pub fn required_jid(node: &Node, key: &str) -> Result<Jid, anyhow::Error> { - node.attrs() - .optional_jid(key) - .ok_or_else(|| anyhow!("missing required attribute {key}")) -} - -/// Get an optional JID attribute. -/// -/// This properly handles JID attributes stored as either: -/// - Direct JID values (binary protocol stores JIDs as structured data) -/// - String values that need to be parsed -/// -/// Returns `Ok(None)` if the attribute is missing. -/// Note: Parse errors are handled internally by the attrs parser. -pub fn optional_jid(node: &Node, key: &str) -> Result<Option<Jid>, anyhow::Error> { - Ok(node.attrs().optional_jid(key)) -} - -/// Parse all children with a given tag into a Vec of ProtocolNodes. -/// -/// Returns an error if any child fails to parse. -/// -/// # Example -/// ```ignore -/// let participants = collect_children::<GroupParticipantResponse>(node, "participant")?; -/// ``` -pub fn collect_children<T: ProtocolNode>(node: &Node, tag: &str) -> Result<Vec<T>, anyhow::Error> { +/// Parse children with a given tag into a Vec using `ProtocolNode::try_from_node_ref`. +pub(crate) fn collect_children<T: ProtocolNode>( + node: &NodeRef<'_>, + tag: &str, +) -> Result<Vec<T>, anyhow::Error> { node.get_children_by_tag(tag) - .map(|child| T::try_from_node(child)) + .map(|child| T::try_from_node_ref(child)) .collect() } diff --git a/wacore/src/iq/passive.rs b/wacore/src/iq/passive.rs index faaee46b7..14a9686d7 100644 --- a/wacore/src/iq/passive.rs +++ b/wacore/src/iq/passive.rs @@ -22,8 +22,8 @@ use crate::iq::spec::IqSpec; use crate::request::InfoQuery; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{NodeContent, NodeRef}; /// IQ namespace for passive mode. pub const PASSIVE_NAMESPACE: &str = "passive"; @@ -61,12 +61,12 @@ impl IqSpec for PassiveModeSpec { InfoQuery::set( PASSIVE_NAMESPACE, - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![child_node])), ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { // Passive mode just needs a successful response Ok(()) } @@ -113,7 +113,7 @@ mod tests { let spec = PassiveModeSpec::passive(); let response = NodeBuilder::new("iq").attr("type", "result").build(); - let result = spec.parse_response(&response); + let result = spec.parse_response(&response.as_node_ref()); assert!(result.is_ok()); } } diff --git a/wacore/src/iq/prekeys.rs b/wacore/src/iq/prekeys.rs index 39e1c132c..f31bda2d5 100644 --- a/wacore/src/iq/prekeys.rs +++ b/wacore/src/iq/prekeys.rs @@ -44,25 +44,25 @@ use crate::protocol::ProtocolNode; use crate::request::InfoQuery; use anyhow::anyhow; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{Node, NodeContent, NodeContentRef, NodeRef}; // Re-export PreKeyBundle for convenience pub use crate::libsignal::protocol::{PreKeyBundle, PublicKey}; -/// Extract binary content from an optional node as `Vec<u8>`. -fn extract_content_bytes(node: Option<&Node>) -> Vec<u8> { - node.and_then(|n| match &n.content { - Some(NodeContent::Bytes(b)) => Some(b.clone()), +/// Extract binary content from an optional `NodeRef` as `Vec<u8>`. +fn extract_content_bytes(node: Option<&NodeRef<'_>>) -> Vec<u8> { + node.and_then(|n| match n.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => Some(b.to_vec()), _ => None, }) .unwrap_or_default() } -/// Extract binary content from an optional node as a big-endian unsigned integer. -fn extract_content_uint(node: Option<&Node>) -> u32 { - node.and_then(|n| match &n.content { - Some(NodeContent::Bytes(b)) => { +/// Extract binary content from an optional `NodeRef` as a big-endian unsigned integer. +fn extract_content_uint(node: Option<&NodeRef<'_>>) -> u32 { + node.and_then(|n| match n.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => { let mut buf = [0u8; 4]; let len = b.len().min(4); buf[4 - len..].copy_from_slice(&b[..len]); @@ -97,12 +97,12 @@ impl IqSpec for PreKeyCountSpec { InfoQuery::get( "encrypt", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![count_node])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { let count_node = response .get_optional_child("count") .ok_or_else(|| anyhow!("Missing <count> node in response"))?; @@ -158,12 +158,12 @@ impl IqSpec for PreKeyFetchSpec { InfoQuery::get( "encrypt", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![content])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { PreKeyUtils::parse_prekeys_response(response) } } @@ -238,12 +238,12 @@ impl IqSpec for DigestKeyBundleSpec { InfoQuery::get( "encrypt", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![digest_node])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { let digest_node = response .get_optional_child("digest") .ok_or_else(|| anyhow::anyhow!("missing <digest> child in response"))?; @@ -253,8 +253,8 @@ impl IqSpec for DigestKeyBundleSpec { let reg_id = extract_content_uint(Some(reg_node)); let identity_node = required_child(digest_node, "identity")?; - let identity = match &identity_node.content { - Some(NodeContent::Bytes(b)) if !b.is_empty() => b.clone(), + let identity = match identity_node.content.as_deref() { + Some(NodeContentRef::Bytes(b)) if !b.is_empty() => b.to_vec(), _ => return Err(anyhow!("missing or empty bytes in <identity>")), }; @@ -284,8 +284,8 @@ impl IqSpec for DigestKeyBundleSpec { .unwrap_or_default(); let hash_node = required_child(digest_node, "hash")?; - let hash = match &hash_node.content { - Some(NodeContent::Bytes(b)) if !b.is_empty() => b.clone(), + let hash = match hash_node.content.as_deref() { + Some(NodeContentRef::Bytes(b)) if !b.is_empty() => b.to_vec(), _ => return Err(anyhow!("missing or empty bytes in <hash>")), }; @@ -383,12 +383,12 @@ impl IqSpec for PreKeyUploadSpec { InfoQuery::set( "encrypt", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(content)), ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { // Pre-key upload just needs a successful response Ok(()) } @@ -461,44 +461,32 @@ impl ProtocolNode for SignedPreKeyNode { .build() } - fn try_from_node(node: &Node) -> Result<Self, anyhow::Error> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self, anyhow::Error> { if node.tag != "skey" { return Err(anyhow!("expected <skey>, got <{}>", node.tag)); } let id_node = required_child(node, "id")?; - let id_bytes = id_node - .content - .as_ref() - .and_then(|c| match c { - NodeContent::Bytes(b) => Some(b), - _ => None, - }) - .ok_or_else(|| anyhow!("missing bytes in <id>"))?; + let id_bytes = match id_node.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => b, + _ => return Err(anyhow!("missing bytes in <id>")), + }; let id = expand_from_3bytes(id_bytes)?; let value_node = required_child(node, "value")?; - let public_bytes = value_node - .content - .as_ref() - .and_then(|c| match c { - NodeContent::Bytes(b) => Some(b.clone()), - _ => None, - }) - .ok_or_else(|| anyhow!("missing bytes in <value>"))?; + let public_bytes = match value_node.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => b.to_vec(), + _ => return Err(anyhow!("missing bytes in <value>")), + }; if public_bytes.len() != 32 { return Err(anyhow!("signed prekey public key must be 32 bytes")); } let sig_node = required_child(node, "signature")?; - let signature = sig_node - .content - .as_ref() - .and_then(|c| match c { - NodeContent::Bytes(b) => Some(b.clone()), - _ => None, - }) - .ok_or_else(|| anyhow!("missing bytes in <signature>"))?; + let signature = match sig_node.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => b.to_vec(), + _ => return Err(anyhow!("missing bytes in <signature>")), + }; if signature.len() != 64 { return Err(anyhow!("signed prekey signature must be 64 bytes")); } @@ -548,31 +536,23 @@ impl ProtocolNode for OneTimePreKeyNode { .build() } - fn try_from_node(node: &Node) -> Result<Self, anyhow::Error> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self, anyhow::Error> { if node.tag != "key" { return Err(anyhow!("expected <key>, got <{}>", node.tag)); } let id_node = required_child(node, "id")?; - let id_bytes = id_node - .content - .as_ref() - .and_then(|c| match c { - NodeContent::Bytes(b) => Some(b), - _ => None, - }) - .ok_or_else(|| anyhow!("missing bytes in <id>"))?; + let id_bytes = match id_node.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => b, + _ => return Err(anyhow!("missing bytes in <id>")), + }; let id = expand_from_3bytes(id_bytes)?; let value_node = required_child(node, "value")?; - let public_bytes = value_node - .content - .as_ref() - .and_then(|c| match c { - NodeContent::Bytes(b) => Some(b.clone()), - _ => None, - }) - .ok_or_else(|| anyhow!("missing bytes in <value>"))?; + let public_bytes = match value_node.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => b.to_vec(), + _ => return Err(anyhow!("missing bytes in <value>")), + }; if public_bytes.len() != 32 { return Err(anyhow!("one-time prekey public key must be 32 bytes")); } @@ -698,7 +678,7 @@ impl ProtocolNode for PreKeyBundleUserNode { .build() } - fn try_from_node(node: &Node) -> Result<Self, anyhow::Error> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self, anyhow::Error> { if node.tag != "user" { return Err(anyhow!("expected <user>, got <{}>", node.tag)); } @@ -710,14 +690,10 @@ impl ProtocolNode for PreKeyBundleUserNode { // Parse registration ID (4 bytes big-endian) let reg_node = required_child(node, "registration")?; - let reg_bytes = reg_node - .content - .as_ref() - .and_then(|c| match c { - NodeContent::Bytes(b) => Some(b), - _ => None, - }) - .ok_or_else(|| anyhow!("missing bytes in <registration>"))?; + let reg_bytes = match reg_node.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => b, + _ => return Err(anyhow!("missing bytes in <registration>")), + }; if reg_bytes.len() != 4 { return Err(anyhow!("registration ID must be 4 bytes")); } @@ -726,32 +702,28 @@ impl ProtocolNode for PreKeyBundleUserNode { // Parse identity key (32 bytes) let identity_node = required_child(node, "identity")?; - let identity_key = identity_node - .content - .as_ref() - .and_then(|c| match c { - NodeContent::Bytes(b) => Some(b.clone()), - _ => None, - }) - .ok_or_else(|| anyhow!("missing bytes in <identity>"))?; + let identity_key = match identity_node.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => b.to_vec(), + _ => return Err(anyhow!("missing bytes in <identity>")), + }; if identity_key.len() != 32 { return Err(anyhow!("identity key must be 32 bytes")); } // Parse signed prekey let skey_node = required_child(node, "skey")?; - let signed_pre_key = SignedPreKeyNode::try_from_node(skey_node)?; + let signed_pre_key = SignedPreKeyNode::try_from_node_ref(skey_node)?; // Parse optional one-time prekey let one_time_pre_key = match node.get_optional_child("key") { - Some(n) => Some(OneTimePreKeyNode::try_from_node(n)?), + Some(n) => Some(OneTimePreKeyNode::try_from_node_ref(n)?), None => None, }; // Parse optional device identity let device_identity = match node.get_optional_child("device-identity") { - Some(n) => match &n.content { - Some(NodeContent::Bytes(b)) => Some(b.clone()), + Some(n) => match n.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => Some(b.to_vec()), _ => return Err(anyhow!("device-identity must be bytes")), }, None => None, @@ -797,7 +769,7 @@ mod tests { .children([NodeBuilder::new("count").attr("value", "42").build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.count, 42); } @@ -810,7 +782,7 @@ mod tests { .children([NodeBuilder::new("count").build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.count, 0); // Default to 0 if missing } @@ -890,7 +862,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.reg_id, 12345); assert_eq!(result.identity, vec![0x01; 32]); assert_eq!(result.skey_id, 1); @@ -907,7 +879,7 @@ mod tests { let response = NodeBuilder::new("iq").attr("type", "result").build(); // Missing <digest> child should error - assert!(spec.parse_response(&response).is_err()); + assert!(spec.parse_response(&response.as_node_ref()).is_err()); } #[test] @@ -957,7 +929,7 @@ mod tests { let response = NodeBuilder::new("iq").attr("type", "result").build(); - let result = spec.parse_response(&response); + let result = spec.parse_response(&response.as_node_ref()); assert!(result.is_ok()); } diff --git a/wacore/src/iq/privacy.rs b/wacore/src/iq/privacy.rs index c9ec2f75f..b28bfb4a9 100644 --- a/wacore/src/iq/privacy.rs +++ b/wacore/src/iq/privacy.rs @@ -54,8 +54,8 @@ use crate::iq::spec::IqSpec; use crate::request::InfoQuery; use crate::types::message::AddressingMode; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{Node, NodeContent, NodeRef}; /// IQ namespace for privacy settings. pub const PRIVACY_NAMESPACE: &str = "privacy"; @@ -191,14 +191,14 @@ impl IqSpec for PrivacySettingsSpec { fn build_iq(&self) -> InfoQuery<'static> { InfoQuery::get( PRIVACY_NAMESPACE, - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![ NodeBuilder::new("privacy").build(), ])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { use crate::iq::node::{optional_attr, required_child}; let privacy_node = required_child(response, "privacy")?; @@ -323,7 +323,7 @@ impl IqSpec for SetPrivacySettingSpec { InfoQuery::set( PRIVACY_NAMESPACE, - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![ privacy_node.children([category_node.build()]).build(), ])), @@ -332,7 +332,7 @@ impl IqSpec for SetPrivacySettingSpec { /// Parse the SET response. WA Web's `setPrivacyParser` extracts `{name, value, dhash}` /// per category; we only need the dhash for disallowed-list conflict resolution. - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { use crate::iq::node::optional_attr; let dhash = response.get_optional_child("privacy").and_then(|privacy| { @@ -370,7 +370,7 @@ impl IqSpec for SetDefaultDisappearingModeSpec { fn build_iq(&self) -> InfoQuery<'static> { InfoQuery::set( "disappearing_mode", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![ NodeBuilder::new("disappearing_mode") .attr("duration", self.duration.to_string()) @@ -379,7 +379,7 @@ impl IqSpec for SetDefaultDisappearingModeSpec { ) } - fn parse_response(&self, _response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { Ok(()) } } @@ -429,7 +429,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.settings.len(), 3); assert_eq!(result.settings[0].category, PrivacyCategory::Last); @@ -489,7 +489,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.settings.len(), 9); assert_eq!(result.settings[0].category, PrivacyCategory::Last); @@ -596,7 +596,8 @@ mod tests { // --- SET spec tests --- fn attr_str<'a>(node: &'a Node, key: &str) -> Option<Cow<'a, str>> { - crate::iq::node::optional_attr(node, key) + let node_ref = node.as_node_ref(); + crate::iq::node::optional_attr(&node_ref, key).map(|c| Cow::Owned(c.into_owned())) } #[test] @@ -634,12 +635,12 @@ mod tests { users: vec![ DisallowedListUserEntry { action: DisallowedListAction::Add, - jid: Jid::new("100000000000001", "lid"), - pn_jid: Some(Jid::new("5511999999999", "s.whatsapp.net")), + jid: Jid::new("100000000000001", Server::Lid), + pn_jid: Some(Jid::new("15550001111", Server::Pn)), }, DisallowedListUserEntry { action: DisallowedListAction::Remove, - jid: Jid::new("100000000000002", "lid"), + jid: Jid::new("100000000000002", Server::Lid), pn_jid: None, }, ], @@ -688,7 +689,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.dhash.as_deref(), Some("updated_hash_456")); } @@ -705,7 +706,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert!(result.dhash.is_none()); } diff --git a/wacore/src/iq/profile.rs b/wacore/src/iq/profile.rs index fa838f6da..331b2d65b 100644 --- a/wacore/src/iq/profile.rs +++ b/wacore/src/iq/profile.rs @@ -14,8 +14,7 @@ use std::borrow::Cow; use crate::iq::spec::IqSpec; use crate::request::InfoQuery; -use wacore_binary::jid::DEFAULT_USER_SERVER; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Node, NodeContent, NodeRef, Server}; /// IQ spec for setting the user's own status text (about). pub struct SetStatusTextSpec { @@ -34,7 +33,7 @@ impl IqSpec for SetStatusTextSpec { fn build_iq(&self) -> InfoQuery<'static> { InfoQuery::set( "status", - DEFAULT_USER_SERVER.parse().expect("valid server JID"), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![Node { tag: Cow::Borrowed("status"), attrs: Default::default(), @@ -43,7 +42,7 @@ impl IqSpec for SetStatusTextSpec { ) } - fn parse_response(&self, _response: &Node) -> anyhow::Result<Self::Response> { + fn parse_response(&self, _response: &NodeRef<'_>) -> anyhow::Result<Self::Response> { Ok(()) } } diff --git a/wacore/src/iq/props.rs b/wacore/src/iq/props.rs index 6317e88ea..808d78d95 100644 --- a/wacore/src/iq/props.rs +++ b/wacore/src/iq/props.rs @@ -27,8 +27,8 @@ use crate::iq::spec::IqSpec; use crate::protocol::ProtocolNode; use crate::request::InfoQuery; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{Node, NodeContent, NodeRef}; /// IQ namespace for A/B props. pub const PROPS_NAMESPACE: &str = "abt"; @@ -91,7 +91,7 @@ impl crate::protocol::ProtocolNode for AbProp { builder.build() } - fn try_from_node(node: &Node) -> Result<Self, anyhow::Error> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self, anyhow::Error> { use crate::iq::node::optional_attr; if node.tag != "prop" { @@ -138,7 +138,7 @@ impl crate::protocol::ProtocolNode for SamplingProp { .build() } - fn try_from_node(node: &Node) -> Result<Self, anyhow::Error> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self, anyhow::Error> { use crate::iq::node::optional_attr; if node.tag != "prop" { @@ -188,17 +188,17 @@ impl crate::protocol::ProtocolNode for AbPropConfig { } } - fn try_from_node(node: &Node) -> Result<Self, anyhow::Error> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self, anyhow::Error> { if node.tag != "prop" { return Err(anyhow::anyhow!("expected <prop>, got <{}>", node.tag)); } - let experiment = AbProp::try_from_node(node); + let experiment = AbProp::try_from_node_ref(node); if let Ok(prop) = experiment { return Ok(Self::Experiment(prop)); } - let sampling = SamplingProp::try_from_node(node); + let sampling = SamplingProp::try_from_node_ref(node); if let Ok(prop) = sampling { return Ok(Self::Sampling(prop)); } @@ -262,7 +262,7 @@ impl crate::protocol::ProtocolNode for PropsResponse { builder.build() } - fn try_from_node(node: &Node) -> Result<Self, anyhow::Error> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self, anyhow::Error> { use crate::iq::node::optional_attr; if node.tag != "props" { @@ -279,7 +279,7 @@ impl crate::protocol::ProtocolNode for PropsResponse { let mut props = Vec::new(); for child in node.get_children_by_tag("prop") { - props.push(AbPropConfig::try_from_node(child)?); + props.push(AbPropConfig::try_from_node_ref(child)?); } Ok(Self { @@ -341,17 +341,16 @@ impl IqSpec for PropsSpec { InfoQuery::get( PROPS_NAMESPACE, - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![builder.build()])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { use crate::iq::node::required_child; - // Find the props child node and parse it using ProtocolNode let props_node = required_child(response, "props")?; - PropsResponse::try_from_node(props_node) + PropsResponse::try_from_node_ref(props_node) } } @@ -433,7 +432,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.ab_key, Some("test_key".to_string())); assert_eq!(result.hash, Some("abcdef".to_string())); assert_eq!(result.refresh, Some(3600)); @@ -476,7 +475,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert!(result.delta_update); } diff --git a/wacore/src/iq/spam_report.rs b/wacore/src/iq/spam_report.rs index b7629e995..d20d408e8 100644 --- a/wacore/src/iq/spam_report.rs +++ b/wacore/src/iq/spam_report.rs @@ -20,8 +20,8 @@ use crate::iq::spec::IqSpec; use crate::request::InfoQuery; use crate::types::spam_report::{SpamReportRequest, SpamReportResult, build_spam_list_node}; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{NodeContent, NodeContentRef, NodeRef}; // Re-export types for convenience pub use crate::types::spam_report::{ @@ -48,17 +48,17 @@ impl IqSpec for SpamReportSpec { InfoQuery::set( "spam", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![spam_list_node])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { // Extract report_id from response if present let report_id = response .get_optional_child_by_tag(&["report_id"]) - .and_then(|n| match &n.content { - Some(NodeContent::String(s)) => Some(s.to_string()), + .and_then(|n| match n.content.as_deref() { + Some(NodeContentRef::String(s)) => Some(s.to_string()), _ => None, }); @@ -119,7 +119,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.report_id, Some("REPORT_ABC123".to_string())); } @@ -136,7 +136,7 @@ mod tests { let response = NodeBuilder::new("iq").attr("type", "result").build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.report_id, None); } } diff --git a/wacore/src/iq/spec.rs b/wacore/src/iq/spec.rs index d723819bf..fa2787868 100644 --- a/wacore/src/iq/spec.rs +++ b/wacore/src/iq/spec.rs @@ -1,5 +1,4 @@ use crate::request::InfoQuery; -use wacore_binary::node::Node; /// A reusable IQ specification that pairs a request builder with a response parser. /// @@ -13,5 +12,8 @@ pub trait IqSpec { fn build_iq(&self) -> InfoQuery<'static>; /// Parse the IQ response node into the typed response. - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error>; + fn parse_response( + &self, + response: &wacore_binary::NodeRef<'_>, + ) -> Result<Self::Response, anyhow::Error>; } diff --git a/wacore/src/iq/tctoken.rs b/wacore/src/iq/tctoken.rs index 94accbc36..7f71baf8e 100644 --- a/wacore/src/iq/tctoken.rs +++ b/wacore/src/iq/tctoken.rs @@ -38,12 +38,11 @@ //! <tctoken><!-- raw token bytes --></tctoken> //! ``` -use crate::iq::node::{optional_attr, required_attr, required_child}; use crate::iq::spec::IqSpec; use crate::request::InfoQuery; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{Node, NodeContent, NodeRef}; use super::privacy::PRIVACY_NAMESPACE; @@ -264,14 +263,14 @@ impl IqSpec for IssuePrivacyTokensSpec { InfoQuery::set( PRIVACY_NAMESPACE, - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![ NodeBuilder::new("tokens").children(token_nodes).build(), ])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { let tokens_node = match response.get_optional_child("tokens") { Some(n) => n, None => return Ok(IssuePrivacyTokensResponse::default()), @@ -279,26 +278,26 @@ impl IqSpec for IssuePrivacyTokensSpec { let mut tokens = Vec::new(); for token_node in tokens_node.get_children_by_tag("token") { - let jid_str = required_attr(token_node, "jid")?; - let jid: Jid = jid_str - .parse() - .map_err(|e| anyhow::anyhow!("invalid jid '{}': {}", jid_str, e))?; - let t_str = required_attr(token_node, "t")?; + let jid: Jid = token_node + .attrs() + .optional_jid("jid") + .ok_or_else(|| anyhow::anyhow!("missing required attribute jid"))?; + let t_str = token_node + .get_attr("t") + .map(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("missing required attribute t"))?; let timestamp: i64 = t_str .parse() .map_err(|e| anyhow::anyhow!("invalid timestamp '{}': {}", t_str, e))?; - let token_bytes = match &token_node.content { - Some(NodeContent::Bytes(data)) => data.clone(), - _ => { - log::warn!(target: "TcToken", "Token node for {} has no binary content, skipping", jid); - continue; - } + let Some(token_data) = token_node.content_bytes() else { + log::warn!(target: "TcToken", "Token node for {} has no binary content, skipping", jid); + continue; }; tokens.push(ReceivedTcToken { jid, - token: token_bytes, + token: token_data.to_vec(), timestamp, }); } @@ -313,19 +312,26 @@ impl IqSpec for IssuePrivacyTokensSpec { /// Returns `ParsedTokenData` items without JID — the caller is responsible for /// resolving the sender JID from the notification's `sender_lid` / `from` attributes. pub fn parse_privacy_token_notification( - notification: &Node, + notification: &NodeRef<'_>, ) -> Result<Vec<ParsedTokenData>, anyhow::Error> { - let tokens_node = required_child(notification, "tokens")?; + let tokens_node = notification + .get_optional_child("tokens") + .ok_or_else(|| anyhow::anyhow!("<tokens> child not found"))?; let mut tokens = Vec::new(); for token_node in tokens_node.get_children_by_tag("token") { - let token_type = optional_attr(token_node, "type"); - let token_type = token_type.as_deref().unwrap_or(""); + let token_type = token_node + .get_attr("type") + .map(|v| v.as_str()) + .unwrap_or_default(); if token_type != "trusted_contact" { continue; } - let t_str = required_attr(token_node, "t")?; + let t_str = token_node + .get_attr("t") + .map(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("missing required attribute t"))?; let timestamp: i64 = t_str.parse().map_err(|e| { anyhow::anyhow!( "invalid timestamp '{}' in privacy_token notification: {}", @@ -334,16 +340,13 @@ pub fn parse_privacy_token_notification( ) })?; - let token_bytes = match &token_node.content { - Some(NodeContent::Bytes(data)) => data.clone(), - _ => { - log::warn!(target: "TcToken", "Notification token node has no binary content, skipping"); - continue; - } + let Some(token_data) = token_node.content_bytes() else { + log::warn!(target: "TcToken", "Notification token node has no binary content, skipping"); + continue; }; tokens.push(ParsedTokenData { - token: token_bytes, + token: token_data.to_vec(), timestamp, }); } @@ -529,7 +532,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.tokens.len(), 1); assert_eq!(result.tokens[0].jid.to_string(), "100000000000001@lid"); assert_eq!(result.tokens[0].token, vec![0xDE, 0xAD, 0xBE, 0xEF]); @@ -555,7 +558,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert!(result.tokens.is_empty()); } @@ -572,7 +575,7 @@ mod tests { .build()]) .build(); - let tokens = parse_privacy_token_notification(&notification).unwrap(); + let tokens = parse_privacy_token_notification(&notification.as_node_ref()).unwrap(); assert_eq!(tokens.len(), 1); assert_eq!(tokens[0].token, vec![0xCA, 0xFE]); assert_eq!(tokens[0].timestamp, 1707000000); @@ -596,7 +599,7 @@ mod tests { .build()]) .build(); - let tokens = parse_privacy_token_notification(&notification).unwrap(); + let tokens = parse_privacy_token_notification(&notification.as_node_ref()).unwrap(); assert_eq!(tokens.len(), 1); assert_eq!(tokens[0].timestamp, 2000); } @@ -612,7 +615,7 @@ mod tests { .build()]) .build(); - let tokens = parse_privacy_token_notification(&notification).unwrap(); + let tokens = parse_privacy_token_notification(&notification.as_node_ref()).unwrap(); assert!(tokens.is_empty()); } @@ -645,7 +648,7 @@ mod tests { let response = NodeBuilder::new("iq").attr("type", "result").build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert!(result.tokens.is_empty()); } diff --git a/wacore/src/iq/usync.rs b/wacore/src/iq/usync.rs index 851c8dfa0..baf94fc5f 100644 --- a/wacore/src/iq/usync.rs +++ b/wacore/src/iq/usync.rs @@ -57,8 +57,8 @@ use anyhow::anyhow; use log::warn; use std::collections::HashMap; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, Server}; +use wacore_binary::{Node, NodeContent, NodeContentRef, NodeRef}; /// Usync mode. #[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)] @@ -119,7 +119,7 @@ fn build_user_nodes(users: &[IsOnWhatsAppUser]) -> Vec<Node> { } /// Parse LID JID from a `<lid val="..."/>` child node. -fn parse_lid_jid(user_node: &Node) -> Option<Jid> { +fn parse_lid_jid(user_node: &NodeRef<'_>) -> Option<Jid> { user_node.get_optional_child("lid").and_then(|lid_node| { lid_node .attrs() @@ -137,7 +137,7 @@ struct ParsedUserFields { } /// Parse common fields from a usync `<user>` node. -fn parse_user_common_fields(user_node: &Node) -> Option<ParsedUserFields> { +fn parse_user_common_fields(user_node: &NodeRef<'_>) -> Option<ParsedUserFields> { let jid = user_node .attrs() .optional_string("jid")? @@ -152,8 +152,8 @@ fn parse_user_common_fields(user_node: &Node) -> Option<ParsedUserFields> { if status_node.get_optional_child("error").is_some() { return None; } - match &status_node.content { - Some(NodeContent::String(s)) if !s.is_empty() => Some(s.to_string()), + match status_node.content.as_deref() { + Some(NodeContentRef::String(s)) if !s.is_empty() => Some(s.to_string()), _ => None, } }); @@ -169,7 +169,7 @@ fn parse_user_common_fields(user_node: &Node) -> Option<ParsedUserFields> { } /// Parse picture ID as String (used in UserInfo). -fn parse_picture_id_string(user_node: &Node) -> Option<String> { +fn parse_picture_id_string(user_node: &NodeRef<'_>) -> Option<String> { user_node .get_optional_child("picture") .and_then(|pic_node| { @@ -245,7 +245,7 @@ fn build_business_query_node() -> Node { } /// Check `<usync><result>` for per-protocol errors. -fn check_usync_result_errors(usync: &Node) -> Result<(), anyhow::Error> { +fn check_usync_result_errors(usync: &NodeRef<'_>) -> Result<(), anyhow::Error> { let Some(result_node) = usync.get_optional_child("result") else { return Ok(()); }; @@ -294,12 +294,12 @@ impl IqSpec for IsOnWhatsAppSpec { InfoQuery::get( "usync", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![usync_node])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { let usync = response .get_optional_child("usync") .ok_or_else(|| anyhow!("Response missing <usync> node"))?; @@ -333,7 +333,7 @@ impl IqSpec for IsOnWhatsAppSpec { true } else { contact_node - .map(|c| c.attrs.get("type").is_some_and(|v| v == "in")) + .map(|c| c.get_attr("type").is_some_and(|v| v.as_str() == "in")) .unwrap_or(false) }; @@ -407,12 +407,12 @@ impl IqSpec for UserInfoSpec { InfoQuery::get( "usync", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![usync_node])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { let usync = response .get_optional_child("usync") .ok_or_else(|| anyhow!("Response missing <usync> node"))?; @@ -532,12 +532,12 @@ impl IqSpec for DeviceListSpec { InfoQuery::get( "usync", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![usync_node])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { let list_node = response .get_optional_child_by_tag(&["usync", "list"]) .ok_or_else(|| anyhow!("<usync> or <list> not found in usync response"))?; @@ -552,13 +552,13 @@ impl IqSpec for DeviceListSpec { .ok_or_else(|| anyhow!("user node missing required 'jid' attribute"))?; // Extract LID mapping if present - if user_jid.server == wacore_binary::jid::DEFAULT_USER_SERVER + if user_jid.server == wacore_binary::Server::Pn && let Some(lid_node) = user_node.get_optional_child("lid") { let lid_val = lid_node.attrs().optional_string("val").unwrap_or_default(); if !lid_val.is_empty() && let Ok(lid_jid) = lid_val.parse::<Jid>() - && lid_jid.server == wacore_binary::jid::HIDDEN_USER_SERVER + && lid_jid.server == wacore_binary::Server::Lid { lid_mappings.push(UsyncLidMapping { phone_number: user_jid.user.clone(), @@ -588,10 +588,8 @@ impl IqSpec for DeviceListSpec { let devices_parent = user_node.get_optional_child("devices"); let key_index_bytes = devices_parent .and_then(|dp| dp.get_optional_child("key-index-list")) - .and_then(|ki| match &ki.content { - Some(wacore_binary::node::NodeContent::Bytes(b)) if !b.is_empty() => { - Some(b.clone()) - } + .and_then(|ki| match ki.content.as_deref() { + Some(NodeContentRef::Bytes(b)) if !b.is_empty() => Some(b.to_vec()), _ => None, }); @@ -697,12 +695,12 @@ impl IqSpec for LidQuerySpec { InfoQuery::get( "usync", - Jid::new("", SERVER_JID), + Jid::new("", Server::Pn), Some(NodeContent::Nodes(vec![usync_node])), ) } - fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> { + fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response, anyhow::Error> { let usync = response .get_optional_child("usync") .ok_or_else(|| anyhow!("LID query response missing <usync> node"))?; @@ -874,7 +872,7 @@ mod tests { .build()]) .build(); - let results = spec.parse_response(&response).unwrap(); + let results = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].jid.user, "1234567890"); assert!(results[0].is_registered); @@ -903,7 +901,7 @@ mod tests { .build()]) .build(); - let results = spec.parse_response(&response).unwrap(); + let results = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(results.len(), 1); assert!(!results[0].is_registered); assert!(!results[0].is_business); @@ -933,7 +931,7 @@ mod tests { .build()]) .build(); - let results = spec.parse_response(&response).unwrap(); + let results = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].jid.user, "100000001"); assert!(results[0].jid.is_lid()); @@ -988,7 +986,7 @@ mod tests { .build()]) .build(); - let results = spec.parse_response(&response).unwrap(); + let results = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(results.len(), 1); let info = results.get(&jid).unwrap(); assert_eq!(info.jid.user, "1234567890"); @@ -1073,7 +1071,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.device_lists.len(), 1); assert_eq!(result.device_lists[0].user.user, "1234567890"); assert_eq!(result.device_lists[0].devices.len(), 3); @@ -1127,7 +1125,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.device_lists.len(), 2); assert_eq!(result.device_lists[0].user.user, "1111111111"); assert_eq!(result.device_lists[0].devices.len(), 1); @@ -1164,7 +1162,7 @@ mod tests { .build()]) .build(); - let result = spec.parse_response(&response).unwrap(); + let result = spec.parse_response(&response.as_node_ref()).unwrap(); assert_eq!(result.device_lists.len(), 1); assert_eq!(result.lid_mappings.len(), 1); assert_eq!(result.lid_mappings[0].phone_number, "1234567890"); diff --git a/wacore/src/media_retry.rs b/wacore/src/media_retry.rs index 2b6e6f7fd..4b2f0ba50 100644 --- a/wacore/src/media_retry.rs +++ b/wacore/src/media_retry.rs @@ -17,9 +17,9 @@ use hkdf::Hkdf; use prost::Message; use rand::Rng; use sha2::Sha256; +use wacore_binary::Jid; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::Jid; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Node, NodeContentRef, NodeRef}; use waproto::whatsapp as wa; const MEDIA_RETRY_HKDF_INFO: &str = "WhatsApp Media Retry Notification"; @@ -48,10 +48,10 @@ fn derive_media_retry_key(media_key: &[u8]) -> Result<[u8; 32]> { Ok(key) } -/// Extract byte content from a Node. -fn get_bytes_content(node: &Node) -> Option<&[u8]> { - match &node.content { - Some(NodeContent::Bytes(b)) => Some(b.as_slice()), +/// Extract byte content from a NodeRef. +fn get_bytes_content_ref<'a>(node: &'a NodeRef<'_>) -> Option<&'a [u8]> { + match node.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => Some(b.as_ref()), _ => None, } } @@ -171,18 +171,21 @@ pub fn build_media_retry_receipt( /// - `<encrypt>` with `<enc_p>` and `<enc_iv>` — encrypted success response /// /// WA Web: `WAWebHandleMediaRetryNotification` -pub fn parse_media_retry_notification(node: &Node, media_key: &[u8]) -> Result<MediaRetryResult> { +pub fn parse_media_retry_notification( + node: &NodeRef<'_>, + media_key: &[u8], +) -> Result<MediaRetryResult> { let msg_id = node - .attrs() - .optional_string("id") + .get_attr("id") + .map(|v| v.as_str()) .ok_or_else(|| anyhow!("notification missing 'id' attribute"))? - .to_string(); + .into_owned(); // Check for error child first if let Some(error_node) = node.get_optional_child_by_tag(&["error"]) { let code = error_node - .attrs() - .optional_string("code") + .get_attr("code") + .map(|v| v.as_str()) .and_then(|s| s.parse::<i32>().ok()) .unwrap_or(0); return Ok(match code { @@ -199,12 +202,12 @@ pub fn parse_media_retry_notification(node: &Node, media_key: &[u8]) -> Result<M let enc_p = encrypt_node .get_optional_child_by_tag(&["enc_p"]) - .and_then(get_bytes_content) + .and_then(get_bytes_content_ref) .ok_or_else(|| anyhow!("missing enc_p in encrypt node"))?; let enc_iv = encrypt_node .get_optional_child_by_tag(&["enc_iv"]) - .and_then(get_bytes_content) + .and_then(get_bytes_content_ref) .ok_or_else(|| anyhow!("missing enc_iv in encrypt node"))?; let notification = decrypt_media_retry_notification(media_key, &msg_id, enc_iv, enc_p)?; diff --git a/wacore/src/message_processing.rs b/wacore/src/message_processing.rs index 134064442..0444e9c4a 100644 --- a/wacore/src/message_processing.rs +++ b/wacore/src/message_processing.rs @@ -5,7 +5,7 @@ //! runtime (Tokio, bridge, etc.) without side effects. use crate::types::events::DecryptFailMode; -use wacore_binary::node::Node; +use wacore_binary::Node; use waproto::whatsapp as wa; // --------------------------------------------------------------------------- @@ -127,7 +127,7 @@ pub fn categorize_enc_nodes<'a>(enc_nodes: &[&'a Node]) -> CategorizedEncNodes<' }; let ciphertext: &[u8] = match &enc_node.content { - Some(wacore_binary::node::NodeContent::Bytes(b)) => b, + Some(wacore_binary::NodeContent::Bytes(b)) => b, _ => { log::warn!("Enc node has no byte content, skipping"); continue; @@ -273,7 +273,7 @@ pub fn process_decrypted_plaintext( #[cfg(test)] mod tests { use super::*; - use wacore_binary::node::{Attrs, Node, NodeContent, NodeValue}; + use wacore_binary::{Attrs, Node, NodeContent, NodeValue}; fn make_enc_node(enc_type: &str, content: &[u8]) -> Node { let mut attrs = Attrs::new(); diff --git a/wacore/src/messages.rs b/wacore/src/messages.rs index b814d22ae..4478c851f 100644 --- a/wacore/src/messages.rs +++ b/wacore/src/messages.rs @@ -29,7 +29,7 @@ impl MessageUtils { buf } - pub fn participant_list_hash(devices: &[wacore_binary::jid::Jid]) -> Result<String> { + pub fn participant_list_hash(devices: &[wacore_binary::Jid]) -> Result<String> { // Hash sorted ad_strings incrementally (avoids join() allocation). let mut jids: Vec<String> = devices.iter().map(|j| j.to_ad_string()).collect(); jids.sort_unstable(); @@ -145,14 +145,14 @@ pub fn is_sender_key_distribution_only(msg: &wa::Message) -> bool { /// attributes. It requires the own JID and optional LID to determine /// `is_from_me`. pub fn parse_message_info( - node: &wacore_binary::node::Node, - own_jid: &wacore_binary::jid::Jid, - own_lid: Option<&wacore_binary::jid::Jid>, + node: &wacore_binary::NodeRef<'_>, + own_jid: &wacore_binary::Jid, + own_lid: Option<&wacore_binary::Jid>, ) -> Result<crate::types::message::MessageInfo> { use crate::types::message::{ AddressingMode, EditAttribute, MessageCategory, MessageInfo, MessageSource, }; - use wacore_binary::jid::{self, JidExt as _}; + use wacore_binary::{JidExt as _, STATUS_BROADCAST_USER, Server}; let mut attrs = node.attrs(); let from = attrs.jid("from"); @@ -160,7 +160,7 @@ pub fn parse_message_info( .optional_string("addressing_mode") .and_then(|s| AddressingMode::try_from(s.as_ref()).ok()); - let mut source = if from.server == jid::BROADCAST_SERVER { + let mut source = if from.server == Server::Broadcast { let participant = attrs.jid("participant"); let is_from_me = participant.matches_user_or_lid(own_jid, own_lid); @@ -169,7 +169,7 @@ pub fn parse_message_info( sender: participant.clone(), is_from_me, is_group: true, - broadcast_list_owner: if from.user != jid::STATUS_BROADCAST_USER { + broadcast_list_owner: if from.user != STATUS_BROADCAST_USER { Some(participant.clone()) } else { None @@ -201,9 +201,9 @@ pub fn parse_message_info( .map(|r| r.to_non_ad()) .unwrap_or_else(|| from.to_non_ad()); // Populate sender_alt so LID-PN cache warms from self-messages - let sender_alt = if from.server == jid::HIDDEN_USER_SERVER { + let sender_alt = if from.server == Server::Lid { Some(own_jid.clone()) - } else if from.server == jid::DEFAULT_USER_SERVER && own_lid.is_some() { + } else if from.server == Server::Pn && own_lid.is_some() { own_lid.cloned() } else { None @@ -217,7 +217,7 @@ pub fn parse_message_info( ..Default::default() } } else { - let sender_alt = if from.server == jid::HIDDEN_USER_SERVER { + let sender_alt = if from.server == Server::Lid { attrs.optional_jid("sender_pn") } else { attrs.optional_jid("sender_lid") diff --git a/wacore/src/pair.rs b/wacore/src/pair.rs index 244966159..584890197 100644 --- a/wacore/src/pair.rs +++ b/wacore/src/pair.rs @@ -9,8 +9,8 @@ use prost::Message; use sha2::Sha256; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, SERVER_JID}; -use wacore_binary::node::Node; +use wacore_binary::{Jid, SERVER_JID}; +use wacore_binary::{Node, NodeRef}; use waproto::whatsapp as wa; use waproto::whatsapp::AdvEncryptionType; @@ -82,6 +82,21 @@ impl PairUtils { } } + /// Builds acknowledgment node for a pairing request from a NodeRef. + pub fn build_ack_node_ref(request_node: &NodeRef<'_>) -> Option<Node> { + let to = request_node.get_attr("from").map(|v| v.as_str())?; + let id = request_node.get_attr("id").map(|v| v.as_str())?; + Some( + NodeBuilder::new("iq") + .attrs([ + ("to", to.to_string()), + ("id", id.to_string()), + ("type", "result".to_string()), + ]) + .build(), + ) + } + /// Builds pair error node pub fn build_pair_error_node(req_id: &str, code: u16, text: &str) -> Node { let error_node = NodeBuilder::new("error") diff --git a/wacore/src/pair_code.rs b/wacore/src/pair_code.rs index f90d9e609..ede29ce9b 100644 --- a/wacore/src/pair_code.rs +++ b/wacore/src/pair_code.rs @@ -29,9 +29,9 @@ use hkdf::Hkdf; use hmac::{Hmac, Mac}; use rand::RngExt; use sha2::Sha256; +use wacore_binary::SERVER_JID; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::SERVER_JID; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Node, NodeContentRef, NodeRef}; // Type aliases type Aes256Ctr = Ctr128BE<aes::Aes256>; @@ -341,12 +341,11 @@ impl PairCodeUtils { } /// Parses the stage 1 response to extract the pairing ref. - pub fn parse_companion_hello_response(node: &Node) -> Option<Vec<u8>> { + pub fn parse_companion_hello_response(node: &NodeRef<'_>) -> Option<Vec<u8>> { node.get_optional_child_by_tag(&["link_code_companion_reg"]) .and_then(|n| n.get_optional_child_by_tag(&["link_code_pairing_ref"])) - .and_then(|n| n.content.as_ref()) - .and_then(|c| match c { - NodeContent::Bytes(b) => Some(b.clone()), + .and_then(|n| match n.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => Some(b.to_vec()), _ => None, }) } diff --git a/wacore/src/prekeys.rs b/wacore/src/prekeys.rs index b90b60648..6d21e2921 100644 --- a/wacore/src/prekeys.rs +++ b/wacore/src/prekeys.rs @@ -1,10 +1,9 @@ use crate::libsignal::protocol::{IdentityKey, PreKeyBundle, PreKeyId, PublicKey, SignedPreKeyId}; -use crate::xml::DisplayableNode; use std::collections::HashMap; use wacore_binary::CompactString; +use wacore_binary::Jid; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::Jid; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Node, NodeRef}; pub struct PreKeyUtils; @@ -96,29 +95,33 @@ impl PreKeyUtils { } pub fn parse_prekeys_response( - resp_node: &Node, + resp_node: &NodeRef<'_>, ) -> Result<HashMap<Jid, PreKeyBundle>, anyhow::Error> { let list_node = resp_node .get_optional_child("list") .ok_or_else(|| anyhow::anyhow!("<list> not found in pre-key response"))?; let mut bundles = HashMap::new(); - for user_node in list_node.children().unwrap_or_default() { - if user_node.tag != "user" { + for user_node_ref in list_node.children().unwrap_or_default() { + if user_node_ref.tag != "user" { continue; } - let mut attrs = user_node.attrs(); - let mut jid = attrs.jid("jid").normalize_for_prekey_bundle(); + let mut jid = user_node_ref + .attrs() + .jid("jid") + .normalize_for_prekey_bundle(); if jid.device == 0 - && (jid.server == wacore_binary::jid::DEFAULT_USER_SERVER - || jid.server == wacore_binary::jid::HIDDEN_USER_SERVER) + && matches!( + jid.server, + wacore_binary::Server::Pn | wacore_binary::Server::Lid + ) && let Some((user_base, device_str)) = jid.user.split_once(':') && let Ok(device) = device_str.parse::<u16>() { jid.user = CompactString::from(user_base); jid.device = device; } - let bundle = match Self::node_to_pre_key_bundle(&jid, user_node) { + let bundle = match Self::node_to_pre_key_bundle_ref(&jid, user_node_ref) { Ok(b) => b, Err(e) => { log::warn!("Failed to parse prekey bundle for {}: {}", jid, e); @@ -131,10 +134,16 @@ impl PreKeyUtils { Ok(bundles) } - fn node_to_pre_key_bundle(jid: &Jid, node: &Node) -> Result<PreKeyBundle, anyhow::Error> { - fn extract_bytes(node: Option<&Node>) -> Result<Vec<u8>, anyhow::Error> { - match node.and_then(|n| n.content.as_ref()) { - Some(NodeContent::Bytes(b)) => Ok(b.clone()), + fn node_to_pre_key_bundle_ref( + jid: &Jid, + node: &NodeRef<'_>, + ) -> Result<PreKeyBundle, anyhow::Error> { + use crate::xml::DisplayableNodeRef; + use wacore_binary::NodeContentRef; + + fn extract_bytes_ref(node: Option<&NodeRef<'_>>) -> Result<Vec<u8>, anyhow::Error> { + match node.and_then(|n| n.content.as_deref()) { + Some(NodeContentRef::Bytes(b)) => Ok(b.to_vec()), _ => Err(anyhow::anyhow!("Expected bytes in node content")), } } @@ -142,11 +151,11 @@ impl PreKeyUtils { if let Some(error_node) = node.get_optional_child("error") { return Err(anyhow::anyhow!( "Error getting prekeys: {}", - DisplayableNode(error_node) + DisplayableNodeRef(error_node) )); } - let reg_id_bytes = extract_bytes(node.get_optional_child("registration"))?; + let reg_id_bytes = extract_bytes_ref(node.get_optional_child("registration"))?; if reg_id_bytes.len() != 4 { return Err(anyhow::anyhow!("Invalid registration ID length")); } @@ -157,21 +166,19 @@ impl PreKeyUtils { reg_id_bytes[3], ]); - let keys_node = node.get_optional_child("keys").unwrap_or(node); // unwrap_or is fine here - - let identity_key_bytes = extract_bytes(keys_node.get_optional_child("identity"))?; + let keys_node = node.get_optional_child("keys").unwrap_or(node); + let identity_key_bytes = extract_bytes_ref(keys_node.get_optional_child("identity"))?; let identity_key_array: [u8; 32] = identity_key_bytes.try_into().map_err(|v: Vec<u8>| { anyhow::anyhow!("Invalid identity key length: got {}, expected 32", v.len()) })?; - let identity_key = IdentityKey::new(PublicKey::from_djb_public_key_bytes(&identity_key_array)?); let mut pre_key_tuple = None; if let Some(pre_key_node) = keys_node.get_optional_child("key") - && let Some((id, key_bytes)) = Self::node_to_pre_key(pre_key_node)? + && let Some((id, key_bytes)) = Self::node_to_pre_key_ref(pre_key_node)? { let pre_key_id: PreKeyId = id.into(); let pre_key_public = PublicKey::from_djb_public_key_bytes(&key_bytes)?; @@ -182,7 +189,7 @@ impl PreKeyUtils { .get_optional_child("skey") .ok_or(anyhow::anyhow!("Missing signed prekey"))?; let (signed_pre_key_id_u32, signed_pre_key_public_bytes, signed_pre_key_signature) = - Self::node_to_signed_pre_key(signed_pre_key_node)?; + Self::node_to_signed_pre_key_ref(signed_pre_key_node)?; let signed_pre_key_id: SignedPreKeyId = signed_pre_key_id_u32.into(); let signed_pre_key_public = @@ -201,16 +208,18 @@ impl PreKeyUtils { Ok(bundle) } - fn node_to_pre_key(node: &Node) -> Result<Option<(u32, [u8; 32])>, anyhow::Error> { - let id_node_content = node + fn node_to_pre_key_ref(node: &NodeRef<'_>) -> Result<Option<(u32, [u8; 32])>, anyhow::Error> { + use wacore_binary::NodeContentRef; + + let id_content = node .get_optional_child("id") - .and_then(|n| n.content.as_ref()); + .and_then(|n| n.content.as_deref()); - let id = match id_node_content { - Some(NodeContent::Bytes(b)) if !b.is_empty() => { + let id = match id_content { + Some(NodeContentRef::Bytes(b)) if !b.is_empty() => { if b.len() == 3 { Ok(u32::from_be_bytes([0, b[0], b[1], b[2]])) - } else if let Ok(s) = std::str::from_utf8(b) { + } else if let Ok(s) = std::str::from_utf8(b.as_ref()) { let trimmed_s = s.trim(); if trimmed_s.is_empty() { Err(anyhow::anyhow!("ID content is only whitespace")) @@ -231,10 +240,10 @@ impl PreKeyUtils { let value_bytes = node .get_optional_child("value") - .and_then(|n| n.content.as_ref()) + .and_then(|n| n.content.as_deref()) .and_then(|c| { - if let NodeContent::Bytes(b) = c { - Some(b.clone()) + if let NodeContentRef::Bytes(b) = c { + Some(b.to_vec()) } else { None } @@ -249,17 +258,21 @@ impl PreKeyUtils { Ok(Some((id, value_arr))) } - fn node_to_signed_pre_key(node: &Node) -> Result<(u32, [u8; 32], [u8; 64]), anyhow::Error> { - let (id, public_key_bytes) = match Self::node_to_pre_key(node)? { + fn node_to_signed_pre_key_ref( + node: &NodeRef<'_>, + ) -> Result<(u32, [u8; 32], [u8; 64]), anyhow::Error> { + use wacore_binary::NodeContentRef; + + let (id, public_key_bytes) = match Self::node_to_pre_key_ref(node)? { Some((id, key)) => (id, key), None => return Err(anyhow::anyhow!("Signed pre-key is missing ID or value")), }; let signature_bytes = node .get_optional_child("signature") - .and_then(|n| n.content.as_ref()) + .and_then(|n| n.content.as_deref()) .and_then(|c| { - if let NodeContent::Bytes(b) = c { - Some(b.clone()) + if let NodeContentRef::Bytes(b) = c { + Some(b.to_vec()) } else { None } @@ -282,8 +295,7 @@ mod tests { use crate::libsignal::protocol::{IdentityKeyPair, KeyPair}; use crate::protocol::ProtocolNode; - use std::borrow::Cow; - use wacore_binary::node::NodeValue; + use wacore_binary::NodeValue; fn create_mock_bundle(device_id: u32) -> PreKeyBundle { let mut rng = rand::make_rng::<rand::rngs::StdRng>(); @@ -313,7 +325,7 @@ mod tests { let raw_jid = Jid { user: "100000012345678:33".into(), - server: Cow::Borrowed("lid"), + server: wacore_binary::Server::Lid, agent: 1, device: 0, integrator: 0, @@ -326,7 +338,8 @@ mod tests { .children([NodeBuilder::new("list").children([user_node]).build()]) .build(); - let bundles = PreKeyUtils::parse_prekeys_response(&response).expect("parse bundles"); + let bundles = + PreKeyUtils::parse_prekeys_response(&response.as_node_ref()).expect("parse bundles"); assert!(bundles.contains_key(&base_jid)); assert!(!bundles.contains_key(&raw_jid)); diff --git a/wacore/src/proto_helpers.rs b/wacore/src/proto_helpers.rs index ae69d50d0..38667e513 100644 --- a/wacore/src/proto_helpers.rs +++ b/wacore/src/proto_helpers.rs @@ -1,5 +1,5 @@ use std::str::FromStr; -use wacore_binary::jid::{Jid, JidExt}; +use wacore_binary::{Jid, JidExt}; use waproto::whatsapp as wa; /// Invokes a callback macro with the list of all message types that have `context_info`. diff --git a/wacore/src/protocol/mod.rs b/wacore/src/protocol/mod.rs index 484e46c67..eef212b64 100644 --- a/wacore/src/protocol/mod.rs +++ b/wacore/src/protocol/mod.rs @@ -2,7 +2,7 @@ pub mod keepalive; pub mod retry; use anyhow::Result; -use wacore_binary::node::Node; +use wacore_binary::{Node, NodeRef}; /// Represents a type that maps to a WhatsApp Protocol node. pub trait ProtocolNode: Sized { @@ -12,8 +12,15 @@ pub trait ProtocolNode: Sized { /// Convert the struct into a protocol `Node`. fn into_node(self) -> Node; - /// Parse a protocol `Node` into the struct. - fn try_from_node(node: &Node) -> Result<Self>; + /// Parse a `NodeRef` into the struct (zero-copy canonical path). + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self>; + + /// Parse an owned `Node` into the struct. + /// + /// The default implementation borrows as a `NodeRef` and delegates. + fn try_from_node(node: &Node) -> Result<Self> { + Self::try_from_node_ref(&node.as_node_ref()) + } } /// Trait for parsing a string enum from a `&str`. @@ -91,13 +98,13 @@ macro_rules! define_simple_node { $tag } - fn into_node(self) -> wacore_binary::node::Node { + fn into_node(self) -> wacore_binary::Node { wacore_binary::builder::NodeBuilder::new($tag) $(.attr($attr_name, self.$field.to_string()))* .build() } - fn try_from_node(node: &wacore_binary::node::Node) -> anyhow::Result<Self> { + fn try_from_node_ref(node: &wacore_binary::NodeRef<'_>) -> anyhow::Result<Self> { if node.tag != $tag { return Err(anyhow::anyhow!("expected <{}>, got <{}>", $tag, node.tag)); } @@ -143,11 +150,11 @@ macro_rules! define_empty_node { $tag } - fn into_node(self) -> wacore_binary::node::Node { + fn into_node(self) -> wacore_binary::Node { wacore_binary::builder::NodeBuilder::new($tag).build() } - fn try_from_node(node: &wacore_binary::node::Node) -> anyhow::Result<Self> { + fn try_from_node_ref(node: &wacore_binary::NodeRef<'_>) -> anyhow::Result<Self> { if node.tag != $tag { return Err(anyhow::anyhow!("expected <{}>, got <{}>", $tag, node.tag)); } diff --git a/wacore/src/protocol/retry.rs b/wacore/src/protocol/retry.rs index 3abd71d34..3ea999f8b 100644 --- a/wacore/src/protocol/retry.rs +++ b/wacore/src/protocol/retry.rs @@ -5,7 +5,7 @@ //! (session management, message resend, cache interaction) remains in //! `whatsapp-rust/src/retry.rs`. -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Node, NodeContent}; /// Maximum retry attempts we'll honor (matches WhatsApp Web's MAX_RETRY = 5). /// We refuse to resend if the requester has already retried this many times. @@ -106,7 +106,7 @@ pub fn should_include_keys(retry_count: u8, reason: RetryReason) -> bool { mod tests { use super::*; use std::borrow::Cow; - use wacore_binary::node::Attrs; + use wacore_binary::Attrs; #[test] fn get_bytes_content_extracts_bytes() { diff --git a/wacore/src/reporting_token.rs b/wacore/src/reporting_token.rs index 565b49476..544964cab 100644 --- a/wacore/src/reporting_token.rs +++ b/wacore/src/reporting_token.rs @@ -22,9 +22,9 @@ use hkdf::Hkdf; use hmac::{Hmac, KeyInit, Mac}; use prost::Message; use sha2::Sha256; +use wacore_binary::Jid; +use wacore_binary::Node; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::Jid; -use wacore_binary::node::Node; use waproto::whatsapp as wa; /// Wire type constants for protobuf parsing @@ -857,7 +857,7 @@ mod tests { #[test] fn test_build_reporting_node() { - use wacore_binary::node::NodeContent; + use wacore_binary::NodeContent; let expected_token = [ 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, diff --git a/wacore/src/request.rs b/wacore/src/request.rs index bf20d00ed..840115ad0 100644 --- a/wacore/src/request.rs +++ b/wacore/src/request.rs @@ -4,8 +4,8 @@ use sha2::{Digest, Sha256}; use std::time::Duration; use thiserror::Error; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{self, Jid, JidExt}; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Jid, JidExt, LEGACY_USER_SERVER}; +use wacore_binary::{Node, NodeContent, NodeRef}; /// IQ request type for WhatsApp protocol queries. #[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)] @@ -156,7 +156,7 @@ impl RequestUtils { if let Some(jid) = user_jid { data.extend_from_slice(jid.user.as_bytes()); data.extend_from_slice(b"@"); - data.extend_from_slice(jid::LEGACY_USER_SERVER.as_bytes()); + data.extend_from_slice(LEGACY_USER_SERVER.as_bytes()); } let mut random_bytes = [0u8; 16]; @@ -198,31 +198,31 @@ impl RequestUtils { builder.build() } - pub fn parse_iq_response(&self, response_node: &Node) -> Box<Result<(), IqError>> { + pub fn parse_iq_response(&self, response_node: &NodeRef<'_>) -> Result<(), IqError> { if response_node.tag == "stream:error" || response_node.tag == "xmlstreamend" { - return Box::new(Err(IqError::Disconnected(response_node.clone()))); + return Err(IqError::Disconnected(response_node.to_owned())); } - if let Some(res_type) = response_node.attrs.get("type") - && res_type == "error" + if let Some(res_type) = response_node.get_attr("type") + && res_type.as_str() == "error" { let error_child = response_node.get_optional_child_by_tag(&["error"]); if let Some(error_node) = error_child { - let mut parser = wacore_binary::attrs::AttrParser::new(error_node); + let mut parser = error_node.attrs(); let code = parser.optional_u64("code").unwrap_or(0) as u16; let text = parser .optional_string("text") .as_deref() .unwrap_or("") .to_string(); - return Box::new(Err(IqError::ServerError { code, text })); + return Err(IqError::ServerError { code, text }); } - return Box::new(Err(IqError::ServerError { + return Err(IqError::ServerError { code: 0, text: "Malformed error response".to_string(), - })); + }); } - Box::new(Ok(())) + Ok(()) } } diff --git a/wacore/src/send.rs b/wacore/src/send.rs index 0aab5adf0..e012f16f0 100644 --- a/wacore/src/send.rs +++ b/wacore/src/send.rs @@ -13,9 +13,9 @@ use anyhow::{Result, anyhow}; use prost::Message as ProtoMessage; use rand::{CryptoRng, Rng}; use std::collections::HashSet; +use wacore_binary::Node; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::{Jid, JidExt as _}; -use wacore_binary::node::Node; +use wacore_binary::{Jid, JidExt as _}; use wacore_libsignal::crypto::aes_256_cbc_encrypt_into; use waproto::whatsapp as wa; use waproto::whatsapp::message::DeviceSentMessage; @@ -1286,8 +1286,8 @@ pub fn ensure_status_participants( mut stanza: Node, group_info: &crate::client::context::GroupInfo, ) -> Node { + use wacore_binary::NodeContent; use wacore_binary::builder::NodeBuilder; - use wacore_binary::node::NodeContent; // Build bare <to jid="USER_JID"/> entries for each participant. // WhatsApp Web uses USER_JID (not DEVICE_JID) for the participantList. @@ -1356,7 +1356,7 @@ mod tests { use crate::client::context::{GroupInfo, SendContextResolver}; use crate::libsignal::protocol::{IdentityKeyPair, KeyPair, PreKeyBundle}; use std::collections::HashMap; - use wacore_binary::jid::Jid; + use wacore_binary::Jid; /// Mock implementation of SendContextResolver for testing struct MockSendContextResolver { @@ -2178,7 +2178,7 @@ mod tests { }; use crate::types::message::AddressingMode; use std::collections::HashMap; - use wacore_binary::node::NodeContent; + use wacore_binary::NodeContent; struct MemSessionStore(HashMap<ProtocolAddress, Vec<u8>>); impl MemSessionStore { diff --git a/wacore/src/session.rs b/wacore/src/session.rs index 1c5ed4114..28d7ed904 100644 --- a/wacore/src/session.rs +++ b/wacore/src/session.rs @@ -11,7 +11,7 @@ use async_lock::Mutex; use futures::channel::oneshot; use std::collections::{HashMap, HashSet}; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; // Tests live in whatsapp-rust/src/session.rs (they use tokio for spawning) diff --git a/wacore/src/stanza/business.rs b/wacore/src/stanza/business.rs index 13cd0c00d..75ee55be3 100644 --- a/wacore/src/stanza/business.rs +++ b/wacore/src/stanza/business.rs @@ -2,11 +2,10 @@ //! //! Reference: WhatsApp Web `WAWebHandleBusinessNotification` -use crate::iq::node::{optional_attr, optional_child}; use anyhow::{Result, anyhow}; use serde::Serialize; -use wacore_binary::jid::Jid; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::Jid; +use wacore_binary::NodeRef; /// Business notification type based on child element. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] @@ -51,15 +50,16 @@ pub struct VerifiedName { } impl VerifiedName { - pub fn try_from_node(node: &Node) -> Result<Self> { + pub fn try_from_node(node: &NodeRef<'_>) -> Result<Self> { + use wacore_binary::NodeContentRef; let name = node .attrs() .optional_string("name") .map(|s| s.into_owned()) .or_else(|| { node.get_optional_child_by_tag(&["name"]) - .and_then(|n| match &n.content { - Some(NodeContent::String(s)) => Some(s.to_string()), + .and_then(|n| match n.content.as_deref() { + Some(NodeContentRef::String(s)) => Some(s.to_string()), _ => None, }) }); @@ -72,8 +72,8 @@ impl VerifiedName { .attrs() .optional_string("issuer") .map(|s| s.into_owned()); - let certificate = match &node.content { - Some(NodeContent::Bytes(b)) => Some(b.clone()), + let certificate = match node.content.as_deref() { + Some(NodeContentRef::Bytes(b)) => Some(b.to_vec()), _ => None, }; @@ -119,30 +119,33 @@ pub struct BusinessNotification { } impl BusinessNotification { - pub fn try_parse(node: &Node) -> Result<Self> { + pub fn try_parse(node: &NodeRef<'_>) -> Result<Self> { if node.tag != "notification" { return Err(anyhow!("expected <notification>, got <{}>", node.tag)); } - if !node.attrs.get("type").is_some_and(|v| v == "business") { + if node + .get_attr("type") + .map(|v| v.as_str()) + .is_none_or(|s| s != "business") + { return Err(anyhow!("expected type='business'")); } - let from = node - .attrs() + let mut attrs = node.attrs(); + let from = attrs .optional_jid("from") .ok_or_else(|| anyhow!("notification missing required 'from' attribute"))?; - - let stanza_id = optional_attr(node, "id") - .map(|s| s.into_owned()) - .unwrap_or_default(); - - let timestamp = match node.attrs().optional_u64("t") { + let stanza_id = node + .get_attr("id") + .map(|v| v.as_str()) + .unwrap_or_default() + .into_owned(); + let timestamp = match attrs.optional_u64("t") { Some(t) => i64::try_from(t) .map_err(|_| anyhow!("notification timestamp {} exceeds i64::MAX", t))?, None => 0, }; - // Parse based on child elements per WhatsApp Web priority order let ( notification_type, jid, @@ -169,7 +172,7 @@ impl BusinessNotification { #[allow(clippy::type_complexity)] fn parse_content( - node: &Node, + node: &NodeRef<'_>, ) -> Result<( BusinessNotificationType, Option<Jid>, @@ -179,7 +182,9 @@ impl BusinessNotification { Vec<String>, Vec<BusinessSubscription>, )> { - if let Some(remove_node) = optional_child(node, "remove") { + use wacore_binary::NodeContentRef; + + if let Some(remove_node) = node.get_optional_child("remove") { if let Some(jid) = remove_node.attrs().optional_jid("jid") { return Ok(( BusinessNotificationType::RemoveJid, @@ -203,7 +208,7 @@ impl BusinessNotification { } } - if let Some(vn_node) = optional_child(node, "verified_name") { + if let Some(vn_node) = node.get_optional_child("verified_name") { let verified_name = VerifiedName::try_from_node(vn_node)?; if let Some(jid) = vn_node.attrs().optional_jid("jid") { return Ok(( @@ -228,7 +233,7 @@ impl BusinessNotification { } } - if let Some(profile_node) = optional_child(node, "profile") { + if let Some(profile_node) = node.get_optional_child("profile") { if let Some(hash) = profile_node.attrs().optional_string("hash") { return Ok(( BusinessNotificationType::ProfileHash, @@ -251,7 +256,7 @@ impl BusinessNotification { )); } - if let Some(catalog_node) = optional_child(node, "product_catalog") + if let Some(catalog_node) = node.get_optional_child("product_catalog") && let Some(children) = catalog_node.children() { let mut product_ids = Vec::new(); @@ -260,7 +265,7 @@ impl BusinessNotification { for child in children { if child.tag == "product" && let Some(id_node) = child.get_optional_child_by_tag(&["id"]) - && let Some(NodeContent::String(id)) = &id_node.content + && let Some(NodeContentRef::String(id)) = id_node.content.as_deref() { product_ids.push(id.to_string()); } else if child.tag == "collection" @@ -294,7 +299,7 @@ impl BusinessNotification { } } - if let Some(subs_node) = optional_child(node, "subscriptions") { + if let Some(subs_node) = node.get_optional_child("subscriptions") { let mut subscriptions = Vec::new(); if let Some(children) = subs_node.children() { for child in children.iter().filter(|c| c.tag == "subscription") { @@ -383,7 +388,7 @@ mod tests { .build()]) .build(); - let parsed = BusinessNotification::try_parse(&node).unwrap(); + let parsed = BusinessNotification::try_parse(&node.as_node_ref()).unwrap(); assert_eq!( parsed.notification_type, BusinessNotificationType::RemoveJid @@ -406,7 +411,7 @@ mod tests { .build()]) .build(); - let parsed = BusinessNotification::try_parse(&node).unwrap(); + let parsed = BusinessNotification::try_parse(&node.as_node_ref()).unwrap(); assert_eq!( parsed.notification_type, BusinessNotificationType::RemoveHash @@ -429,7 +434,7 @@ mod tests { .build()]) .build(); - let parsed = BusinessNotification::try_parse(&node).unwrap(); + let parsed = BusinessNotification::try_parse(&node.as_node_ref()).unwrap(); assert_eq!( parsed.notification_type, BusinessNotificationType::VerifiedNameJid @@ -452,7 +457,7 @@ mod tests { .children([NodeBuilder::new("profile").build()]) .build(); - let parsed = BusinessNotification::try_parse(&node).unwrap(); + let parsed = BusinessNotification::try_parse(&node.as_node_ref()).unwrap(); assert_eq!(parsed.notification_type, BusinessNotificationType::Profile); assert!(parsed.is_profile_update()); } @@ -469,7 +474,7 @@ mod tests { .build()]) .build(); - let parsed = BusinessNotification::try_parse(&node).unwrap(); + let parsed = BusinessNotification::try_parse(&node.as_node_ref()).unwrap(); assert_eq!( parsed.notification_type, BusinessNotificationType::ProfileHash @@ -496,7 +501,7 @@ mod tests { .build()]) .build(); - let parsed = BusinessNotification::try_parse(&node).unwrap(); + let parsed = BusinessNotification::try_parse(&node.as_node_ref()).unwrap(); assert_eq!(parsed.notification_type, BusinessNotificationType::Product); assert_eq!(parsed.product_ids, vec!["product_1", "product_2"]); } @@ -517,7 +522,7 @@ mod tests { .build()]) .build(); - let parsed = BusinessNotification::try_parse(&node).unwrap(); + let parsed = BusinessNotification::try_parse(&node.as_node_ref()).unwrap(); assert_eq!( parsed.notification_type, BusinessNotificationType::Subscriptions @@ -538,7 +543,7 @@ mod tests { .children([NodeBuilder::new("ctwa_suggestion").build()]) .build(); - let parsed = BusinessNotification::try_parse(&node).unwrap(); + let parsed = BusinessNotification::try_parse(&node.as_node_ref()).unwrap(); assert_eq!(parsed.notification_type, BusinessNotificationType::Unknown); } @@ -549,7 +554,7 @@ mod tests { .attr("from", "15551234567@s.whatsapp.net") .build(); - let result = BusinessNotification::try_parse(&node); + let result = BusinessNotification::try_parse(&node.as_node_ref()); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("type='business'")); } diff --git a/wacore/src/stanza/devices.rs b/wacore/src/stanza/devices.rs index 288482cd7..809ed67e6 100644 --- a/wacore/src/stanza/devices.rs +++ b/wacore/src/stanza/devices.rs @@ -11,13 +11,13 @@ //! - `hash` attribute is REQUIRED for update use crate::StringEnum; -use crate::iq::node::{optional_attr, optional_child, required_attr, required_child}; +use crate::iq::node::{required_attr, required_child}; use crate::protocol::ProtocolNode; use anyhow::{Result, anyhow}; use serde::Serialize; +use wacore_binary::Jid; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::Jid; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::{Node, NodeRef}; /// Device notification operation type. /// @@ -68,7 +68,8 @@ impl ProtocolNode for KeyIndexInfo { builder.build() } - fn try_from_node(node: &Node) -> Result<Self> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self> { + use wacore_binary::NodeContentRef; if node.tag != "key-index-list" { return Err(anyhow!("expected <key-index-list>, got <{}>", node.tag)); } @@ -78,8 +79,8 @@ impl ProtocolNode for KeyIndexInfo { .ok_or_else(|| anyhow!("key-index-list missing required 'ts' attribute"))?; let timestamp = i64::try_from(ts_u64) .map_err(|_| anyhow!("key-index-list 'ts' value {} exceeds i64::MAX", ts_u64))?; - let signed_bytes = match &node.content { - Some(NodeContent::Bytes(b)) if !b.is_empty() => Some(b.clone()), + let signed_bytes = match node.content.as_deref() { + Some(NodeContentRef::Bytes(b)) if !b.is_empty() => Some(b.to_vec()), _ => None, }; Ok(Self { @@ -136,17 +137,16 @@ impl ProtocolNode for DeviceElement { builder.build() } - fn try_from_node(node: &Node) -> Result<Self> { + fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self> { if node.tag != "device" { return Err(anyhow!("expected <device>, got <{}>", node.tag)); } - let jid = node - .attrs() + let mut attrs = node.attrs(); + let jid = attrs .optional_jid("jid") .ok_or_else(|| anyhow!("device missing required 'jid' attribute"))?; - // Parse key-index with checked conversion (u64 -> u32) - let key_index = match node.attrs().optional_u64("key-index") { + let key_index = match attrs.optional_u64("key-index") { Some(v) => Some( u32::try_from(v) .map_err(|_| anyhow!("device 'key-index' value {} exceeds u32::MAX", v))?, @@ -154,10 +154,8 @@ impl ProtocolNode for DeviceElement { None => None, }; - let lid = node.attrs().optional_jid("lid"); + let lid = attrs.optional_jid("lid"); - // Per WhatsApp Web: validate device ID matches between jid and lid attributes - // Reference: 5Yec01dI04o.js:23169-23175 if let Some(ref lid_jid) = lid { let jid_device_id = jid.device; let lid_device_id = lid_jid.device; @@ -212,31 +210,28 @@ pub struct DeviceOperation { } impl DeviceOperation { - /// Parse from an add/remove/update child node. + /// Parse from an add/remove/update child `NodeRef`. /// /// Per WhatsApp Web (5Yec01dI04o.js:23141-23157): /// - `key-index-list` is REQUIRED for add/remove operations /// - `ts` attribute is REQUIRED for remove operations - pub fn try_from_child(node: &Node) -> Result<Self> { + pub fn try_from_child(node: &NodeRef<'_>) -> Result<Self> { let operation_type = DeviceNotificationType::try_from(node.tag.as_ref()) .map_err(|_| anyhow!("unknown device operation: {}", node.tag))?; match operation_type { DeviceNotificationType::Add | DeviceNotificationType::Remove => { - // Per WhatsApp Web: key-index-list is required for add/remove let key_index_node = required_child(node, "key-index-list")?; - let key_index = KeyIndexInfo::try_from_node(key_index_node)?; + let key_index = KeyIndexInfo::try_from_node_ref(key_index_node)?; - // Per WhatsApp Web: timestamp is required for remove if operation_type == DeviceNotificationType::Remove && key_index.timestamp == 0 { return Err(anyhow!( "timestamp is required to handle device remove notification" )); } - // Parse device element let device_node = required_child(node, "device")?; - let device = DeviceElement::try_from_node(device_node)?; + let device = DeviceElement::try_from_node_ref(device_node)?; Ok(Self { operation_type, @@ -298,42 +293,44 @@ pub struct DeviceNotification { } impl DeviceNotification { - /// Parse from a `<notification type="devices">` node. + /// Parse from a `<notification type="devices">` NodeRef. /// /// Per WhatsApp Web: Only ONE operation per notification is processed. /// Priority order: remove > add > update /// Returns error if no operation is found. - pub fn try_parse(node: &Node) -> Result<Self> { + pub fn try_parse(node: &NodeRef<'_>) -> Result<Self> { if node.tag != "notification" { return Err(anyhow!("expected <notification>, got <{}>", node.tag)); } - if !node.attrs.get("type").is_some_and(|v| v == "devices") { + if node + .get_attr("type") + .is_none_or(|v| v.as_str() != "devices") + { return Err(anyhow!("expected type='devices'")); } - let from = node - .attrs() + let mut parser = node.attrs(); + let from = parser .optional_jid("from") .ok_or_else(|| anyhow!("notification missing required 'from' attribute"))?; - let lid_user = node.attrs().optional_jid("lid"); - let stanza_id = optional_attr(node, "id") - .map(|s| s.into_owned()) - .unwrap_or_default(); - - // Parse timestamp with checked conversion - let timestamp = match node.attrs().optional_u64("t") { + let lid_user = parser.optional_jid("lid"); + let stanza_id = node + .get_attr("id") + .map(|v| v.as_str()) + .unwrap_or_default() + .into_owned(); + let timestamp = match parser.optional_u64("t") { Some(t) => i64::try_from(t) .map_err(|_| anyhow!("notification timestamp {} exceeds i64::MAX", t))?, None => 0, }; // Per WhatsApp Web: Priority order is remove > add > update - // Only one operation is processed per notification - let operation = if let Some(remove_node) = optional_child(node, "remove") { + let operation = if let Some(remove_node) = node.get_optional_child("remove") { DeviceOperation::try_from_child(remove_node)? - } else if let Some(add_node) = optional_child(node, "add") { + } else if let Some(add_node) = node.get_optional_child("add") { DeviceOperation::try_from_child(add_node)? - } else if let Some(update_node) = optional_child(node, "update") { + } else if let Some(update_node) = node.get_optional_child("update") { DeviceOperation::try_from_child(update_node)? } else { return Err(anyhow!( @@ -417,7 +414,7 @@ mod tests { .build()]) .build(); - let parsed = DeviceNotification::try_parse(&node).unwrap(); + let parsed = DeviceNotification::try_parse(&node.as_node_ref()).unwrap(); assert_eq!(parsed.from.user, "185169143189667"); assert_eq!(parsed.stanza_id, "511477682"); assert_eq!(parsed.timestamp, 1769296817); @@ -452,7 +449,7 @@ mod tests { .build()]) .build(); - let parsed = DeviceNotification::try_parse(&node).unwrap(); + let parsed = DeviceNotification::try_parse(&node.as_node_ref()).unwrap(); // Check LID-PN mapping detection let (lid, pn) = parsed.lid_pn_mapping().unwrap(); @@ -481,7 +478,7 @@ mod tests { .build()]) .build(); - let parsed = DeviceNotification::try_parse(&node).unwrap(); + let parsed = DeviceNotification::try_parse(&node.as_node_ref()).unwrap(); let op = &parsed.operation; assert_eq!(op.operation_type, DeviceNotificationType::Update); @@ -502,7 +499,7 @@ mod tests { .children([NodeBuilder::new("update").attr("hash", "test_hash").build()]) .build(); - let parsed = DeviceNotification::try_parse(&node).unwrap(); + let parsed = DeviceNotification::try_parse(&node.as_node_ref()).unwrap(); // No mapping should be detected when from is also a LID assert!(parsed.lid_pn_mapping().is_none()); } @@ -522,7 +519,7 @@ mod tests { .build()]) .build(); - let result = DeviceNotification::try_parse(&node); + let result = DeviceNotification::try_parse(&node.as_node_ref()); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("key-index-list")); } @@ -547,7 +544,7 @@ mod tests { .build()]) .build(); - let result = DeviceNotification::try_parse(&node); + let result = DeviceNotification::try_parse(&node.as_node_ref()); assert!(result.is_err()); assert!( result @@ -576,7 +573,7 @@ mod tests { .build()]) .build(); - let result = DeviceNotification::try_parse(&node); + let result = DeviceNotification::try_parse(&node.as_node_ref()); assert!(result.is_err()); assert!( result @@ -605,7 +602,7 @@ mod tests { .build()]) .build(); - let parsed = DeviceNotification::try_parse(&node).unwrap(); + let parsed = DeviceNotification::try_parse(&node.as_node_ref()).unwrap(); assert_eq!(parsed.operation.devices[0].device_id(), 64); assert!(parsed.operation.devices[0].lid.is_some()); } @@ -620,7 +617,7 @@ mod tests { .attr("t", "1000") .build(); // No operation children - let result = DeviceNotification::try_parse(&node); + let result = DeviceNotification::try_parse(&node.as_node_ref()); assert!(result.is_err()); assert!( result @@ -659,7 +656,7 @@ mod tests { ]) .build(); - let parsed = DeviceNotification::try_parse(&node).unwrap(); + let parsed = DeviceNotification::try_parse(&node.as_node_ref()).unwrap(); // Should process remove, not add assert_eq!( parsed.operation.operation_type, @@ -679,7 +676,7 @@ mod tests { .children([NodeBuilder::new("update").build()]) // Missing hash attribute .build(); - let result = DeviceNotification::try_parse(&node); + let result = DeviceNotification::try_parse(&node.as_node_ref()); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("hash")); } diff --git a/wacore/src/stanza/groups.rs b/wacore/src/stanza/groups.rs index 493faab10..002dfe9aa 100644 --- a/wacore/src/stanza/groups.rs +++ b/wacore/src/stanza/groups.rs @@ -11,8 +11,8 @@ //! - Participant lists are nested `<participant jid="..." />` children use serde::Serialize; -use wacore_binary::jid::Jid; -use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::Jid; +use wacore_binary::{Node, NodeRef}; /// Parsed group notification containing one or more actions. #[derive(Debug, Clone)] @@ -197,18 +197,20 @@ impl GroupNotificationAction { } impl GroupNotification { - /// Parse a `<notification type="w:gp2">` node into a typed GroupNotification. + /// Parse from a `NodeRef`. /// - /// Returns `None` if the `from` attribute is missing (invalid notification). - pub fn try_from_node(node: &Node) -> Option<Self> { - let group_jid = node.attrs().optional_jid("from")?; - let participant = node.attrs().optional_jid("participant"); - let participant_pn = node.attrs().optional_jid("participant_pn"); - let timestamp = node.attrs().optional_u64("t").unwrap_or(0); + /// Most fields are parsed zero-copy. Only `Create`/`Link`/`Unlink` actions + /// call `.to_owned()` on their specific child node (structurally required to store `raw: Node`). + pub fn try_from_node_ref(node: &NodeRef<'_>) -> Option<Self> { + let mut attrs = node.attrs(); + let group_jid = attrs.optional_jid("from")?; + let participant = attrs.optional_jid("participant"); + let participant_pn = attrs.optional_jid("participant_pn"); + let timestamp = attrs.optional_u64("t").unwrap_or(0); let is_lid_addressing_mode = node - .attrs - .get("addressing_mode") - .is_some_and(|v| v == "lid"); + .get_attr("addressing_mode") + .map(|v| v.as_str()) + .is_some_and(|s| s == "lid"); let actions = node .children() @@ -227,9 +229,11 @@ impl GroupNotification { } /// Parse a single child element into a GroupNotificationAction. -fn parse_action(node: &Node) -> Option<GroupNotificationAction> { +/// +/// Only `Create`/`Link`/`Unlink` call `.to_owned()` because those variants store `raw: Node`. +fn parse_action(node: &NodeRef<'_>) -> Option<GroupNotificationAction> { + use wacore_binary::NodeContentRef; let action = match node.tag.as_ref() { - // Participant management "add" => GroupNotificationAction::Add { participants: parse_participants(node), reason: node @@ -253,8 +257,6 @@ fn parse_action(node: &Node) -> Option<GroupNotificationAction> { "modify" => GroupNotificationAction::Modify { participants: parse_participants(node), }, - - // Metadata "subject" => GroupNotificationAction::Subject { subject: node .attrs() @@ -281,8 +283,6 @@ fn parse_action(node: &Node) -> Option<GroupNotificationAction> { }; GroupNotificationAction::Description { id, description } } - - // Settings "locked" => GroupNotificationAction::Locked { threshold: node .attrs() @@ -308,17 +308,15 @@ fn parse_action(node: &Node) -> Option<GroupNotificationAction> { GroupNotificationAction::MembershipApprovalMode { enabled } } "member_add_mode" => { - let mode = match &node.content { - Some(NodeContent::String(s)) => s.to_string(), - Some(NodeContent::Bytes(b)) => String::from_utf8_lossy(b).into_owned(), + let mode = match node.content.as_deref() { + Some(NodeContentRef::String(s)) => s.to_string(), + Some(NodeContentRef::Bytes(b)) => String::from_utf8_lossy(b.as_ref()).into_owned(), _ => String::new(), }; GroupNotificationAction::MemberAddMode { mode } } "no_frequently_forwarded" => GroupNotificationAction::NoFrequentlyForwarded, "frequently_forwarded_ok" => GroupNotificationAction::FrequentlyForwardedOk, - - // Invites "invite" => GroupNotificationAction::Invite { code: node .attrs() @@ -338,17 +336,16 @@ fn parse_action(node: &Node) -> Option<GroupNotificationAction> { .to_string(), }, "growth_unlocked" => GroupNotificationAction::GrowthUnlocked, - - // Group lifecycle - "create" => GroupNotificationAction::Create { raw: node.clone() }, + // These three variants store owned Node — only convert what's needed. + "create" => GroupNotificationAction::Create { + raw: node.to_owned(), + }, "delete" => GroupNotificationAction::Delete { reason: node .attrs() .optional_string("reason") .map(|s| s.into_owned()), }, - - // Community linking "link" => GroupNotificationAction::Link { link_type: node .attrs() @@ -356,7 +353,7 @@ fn parse_action(node: &Node) -> Option<GroupNotificationAction> { .as_deref() .unwrap_or_default() .to_string(), - raw: node.clone(), + raw: node.to_owned(), }, "unlink" => GroupNotificationAction::Unlink { unlink_type: node @@ -369,23 +366,17 @@ fn parse_action(node: &Node) -> Option<GroupNotificationAction> { .attrs() .optional_string("unlink_reason") .map(|s| s.into_owned()), - raw: node.clone(), + raw: node.to_owned(), }, - - // Skip silently — not actionable "missing_participant_identification" => return None, - - // Unknown tag — preserve for forward compatibility other => GroupNotificationAction::Unknown { tag: other.to_string(), }, }; - Some(action) } -/// Parse `<participant jid="..." phone_number="..."/>` children from an action node. -fn parse_participants(node: &Node) -> Vec<GroupParticipantInfo> { +fn parse_participants(node: &NodeRef<'_>) -> Vec<GroupParticipantInfo> { node.children() .map(|children| { children @@ -404,8 +395,8 @@ fn parse_participants(node: &Node) -> Vec<GroupParticipantInfo> { #[cfg(test)] mod tests { use super::*; + use wacore_binary::Jid; use wacore_binary::builder::NodeBuilder; - use wacore_binary::jid::Jid; fn group_jid() -> Jid { "120363012345678901@g.us".parse().unwrap() @@ -441,7 +432,7 @@ mod tests { .build(), ]); - let notif = GroupNotification::try_from_node(&node).unwrap(); + let notif = GroupNotification::try_from_node_ref(&node.as_node_ref()).unwrap(); assert_eq!(notif.group_jid, group_jid()); assert_eq!(notif.participant, Some(admin_jid())); assert_eq!(notif.timestamp, 1704067200); @@ -470,7 +461,7 @@ mod tests { .build(), ]); - let notif = GroupNotification::try_from_node(&node).unwrap(); + let notif = GroupNotification::try_from_node_ref(&node.as_node_ref()).unwrap(); assert_eq!(notif.actions.len(), 1); match &notif.actions[0] { @@ -500,7 +491,7 @@ mod tests { .build(), ]); - let notif = GroupNotification::try_from_node(&node).unwrap(); + let notif = GroupNotification::try_from_node_ref(&node.as_node_ref()).unwrap(); match &notif.actions[0] { GroupNotificationAction::Description { id, description } => { assert_eq!(id, "desc123"); @@ -519,7 +510,7 @@ mod tests { .build(), ]); - let notif = GroupNotification::try_from_node(&node).unwrap(); + let notif = GroupNotification::try_from_node_ref(&node.as_node_ref()).unwrap(); match &notif.actions[0] { GroupNotificationAction::Description { id, description } => { assert_eq!(id, "desc123"); @@ -540,7 +531,7 @@ mod tests { .build(), ]); - let notif = GroupNotification::try_from_node(&node).unwrap(); + let notif = GroupNotification::try_from_node_ref(&node.as_node_ref()).unwrap(); assert_eq!(notif.actions.len(), 3); match &notif.actions[0] { @@ -569,7 +560,7 @@ mod tests { fn test_parse_not_ephemeral() { let node = make_notification(vec![NodeBuilder::new("not_ephemeral").build()]); - let notif = GroupNotification::try_from_node(&node).unwrap(); + let notif = GroupNotification::try_from_node_ref(&node.as_node_ref()).unwrap(); match &notif.actions[0] { GroupNotificationAction::Ephemeral { expiration, @@ -592,7 +583,7 @@ mod tests { .build(), ]); - let notif = GroupNotification::try_from_node(&node).unwrap(); + let notif = GroupNotification::try_from_node_ref(&node.as_node_ref()).unwrap(); match &notif.actions[0] { GroupNotificationAction::MembershipApprovalMode { enabled } => { assert!(*enabled); @@ -605,7 +596,7 @@ mod tests { fn test_parse_unknown_tag() { let node = make_notification(vec![NodeBuilder::new("some_future_feature").build()]); - let notif = GroupNotification::try_from_node(&node).unwrap(); + let notif = GroupNotification::try_from_node_ref(&node.as_node_ref()).unwrap(); match &notif.actions[0] { GroupNotificationAction::Unknown { tag } => { assert_eq!(tag, "some_future_feature"); @@ -621,6 +612,6 @@ mod tests { .attr("t", "1704067200") .build(); - assert!(GroupNotification::try_from_node(&node).is_none()); + assert!(GroupNotification::try_from_node_ref(&node.as_node_ref()).is_none()); } } diff --git a/wacore/src/stanza/message.rs b/wacore/src/stanza/message.rs index d26c6c780..3b0310bde 100644 --- a/wacore/src/stanza/message.rs +++ b/wacore/src/stanza/message.rs @@ -3,7 +3,7 @@ //! Provides type-safe message stanzas with JID-aware attributes. use crate::ProtocolNode; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; /// Typed 1-to-1 or generic message stanza with JID-safe attributes. /// diff --git a/wacore/src/stanza/notification.rs b/wacore/src/stanza/notification.rs index 93f510146..bd9fec11d 100644 --- a/wacore/src/stanza/notification.rs +++ b/wacore/src/stanza/notification.rs @@ -10,7 +10,7 @@ //! the handler. If more parsing logic is added to the handler in the future, //! it should be extracted here as pure functions. -use wacore_binary::node::Node; +use wacore_binary::Node; /// Extract a notification timestamp from a node's `t` attribute. /// diff --git a/wacore/src/stanza/receipt.rs b/wacore/src/stanza/receipt.rs index 51dbf1608..4fc06a959 100644 --- a/wacore/src/stanza/receipt.rs +++ b/wacore/src/stanza/receipt.rs @@ -4,7 +4,7 @@ //! Orchestration and dispatch remain in `whatsapp-rust/src/receipt.rs`. use crate::types::message::{MessageCategory, MessageInfo}; -use wacore_binary::jid::{JidExt as _, STATUS_BROADCAST_USER}; +use wacore_binary::{JidExt as _, STATUS_BROADCAST_USER}; /// Determines whether a delivery receipt should be sent for this message. /// diff --git a/wacore/src/store/commands.rs b/wacore/src/store/commands.rs index 35e102948..864c6c9e8 100644 --- a/wacore/src/store/commands.rs +++ b/wacore/src/store/commands.rs @@ -1,5 +1,5 @@ use crate::store::Device; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; use waproto::whatsapp as wa; #[derive(Debug, Clone)] diff --git a/wacore/src/store/device.rs b/wacore/src/store/device.rs index d2ff7f780..4f20e10b5 100644 --- a/wacore/src/store/device.rs +++ b/wacore/src/store/device.rs @@ -3,7 +3,7 @@ use once_cell::sync::Lazy; use prost::Message; use serde::{Deserialize, Serialize}; use serde_big_array::BigArray; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; use waproto::whatsapp as wa; /// Protobuf-bytes serde for `AdvSignedDeviceIdentity` (prost types lack `Deserialize`). diff --git a/wacore/src/types/call.rs b/wacore/src/types/call.rs index 438cb2029..543807e96 100644 --- a/wacore/src/types/call.rs +++ b/wacore/src/types/call.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Utc}; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; #[derive(Debug, Clone)] pub struct BasicCallMeta { diff --git a/wacore/src/types/events.rs b/wacore/src/types/events.rs index c78fb4db8..debc0373c 100644 --- a/wacore/src/types/events.rs +++ b/wacore/src/types/events.rs @@ -7,8 +7,9 @@ use prost::Message; use serde::Serialize; use std::fmt; use std::sync::{Arc, OnceLock, RwLock}; -use wacore_binary::jid::{Jid, MessageId}; -use wacore_binary::node::Node; +use wacore_binary::Node; +use wacore_binary::OwnedNodeRef; +use wacore_binary::{Jid, MessageId}; use waproto::whatsapp::{self as wa, HistorySync}; /// Wrapper for large event data that uses Arc for cheap cloning. @@ -382,7 +383,7 @@ pub struct BusinessStatusUpdate { #[derive(Debug, Clone, Serialize)] pub struct DisappearingModeChanged { /// The contact whose setting changed. - pub from: wacore_binary::jid::Jid, + pub from: Jid, /// New duration in seconds (0 = disabled, 86400 = 24h, etc.). pub duration: u32, /// Unix timestamp (seconds) when the setting was changed. @@ -416,7 +417,8 @@ pub enum Event { Message(Box<wa::Message>, MessageInfo), Receipt(Receipt), UndecryptableMessage(UndecryptableMessage), - Notification(Node), + #[serde(skip)] + Notification(Arc<OwnedNodeRef>), ChatPresence(ChatPresenceUpdate), Presence(PresenceUpdate), @@ -469,7 +471,7 @@ pub enum Event { /// Library extension — no WA Web equivalent (WA Web has no raw stanza observer). /// Gated by `Client::set_raw_node_forwarding(true)` to avoid overhead when unused. #[serde(skip)] - RawNode(Arc<Node>), + RawNode(Arc<OwnedNodeRef>), } /// A newsletter live update notification, typically containing updated diff --git a/wacore/src/types/jid.rs b/wacore/src/types/jid.rs index bc6711a9c..f2ce3535a 100644 --- a/wacore/src/types/jid.rs +++ b/wacore/src/types/jid.rs @@ -1,5 +1,5 @@ use crate::libsignal::protocol::ProtocolAddress; -use wacore_binary::jid::Jid; +use wacore_binary::Jid; /// Map server names to WhatsApp Web's internal Signal address format. #[inline] @@ -11,7 +11,7 @@ fn mapped_server(s: &str) -> &str { pub fn write_protocol_address_to(jid: &Jid, buf: &mut String) { use std::fmt::Write; buf.clear(); - let server = mapped_server(&jid.server); + let server = mapped_server(jid.server.as_str()); buf.push_str(&jid.user); if jid.device != 0 { buf.push(':'); @@ -24,8 +24,8 @@ pub fn write_protocol_address_to(jid: &Jid, buf: &mut String) { /// Consistent ordering for deadlock-free multi-lock acquisition. pub fn cmp_for_lock_order(a: &Jid, b: &Jid) -> std::cmp::Ordering { - mapped_server(&a.server) - .cmp(mapped_server(&b.server)) + mapped_server(a.server.as_str()) + .cmp(mapped_server(b.server.as_str())) .then_with(|| a.user.cmp(&b.user)) .then_with(|| a.device.cmp(&b.device)) } @@ -69,7 +69,7 @@ pub trait JidExt { impl JidExt for Jid { fn to_signal_address_string(&self) -> String { use std::fmt::Write; - let server = mapped_server(&self.server); + let server = mapped_server(self.server.as_str()); let mut result = String::with_capacity(self.user.len() + 7 + server.len()); result.push_str(&self.user); if self.device != 0 { diff --git a/wacore/src/types/message.rs b/wacore/src/types/message.rs index 842c672a9..1deefa821 100644 --- a/wacore/src/types/message.rs +++ b/wacore/src/types/message.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use wacore_binary::jid::{Jid, JidExt, MessageId, MessageServerId}; +use wacore_binary::{Jid, JidExt, MessageId, MessageServerId}; use waproto::whatsapp as wa; use crate::StringEnum; diff --git a/wacore/src/types/spam_report.rs b/wacore/src/types/spam_report.rs index 32bd25539..d28a77e8d 100644 --- a/wacore/src/types/spam_report.rs +++ b/wacore/src/types/spam_report.rs @@ -1,9 +1,9 @@ //! Spam report types and node building. use crate::StringEnum; +use wacore_binary::Jid; +use wacore_binary::Node; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::Jid; -use wacore_binary::node::Node; /// The type of spam flow indicating the source of the report. #[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)] diff --git a/wacore/src/usync.rs b/wacore/src/usync.rs index 75c65f8e7..aee5f7e91 100644 --- a/wacore/src/usync.rs +++ b/wacore/src/usync.rs @@ -1,7 +1,7 @@ use anyhow::{Result, anyhow}; +use wacore_binary::Jid; use wacore_binary::builder::NodeBuilder; -use wacore_binary::jid::Jid; -use wacore_binary::node::Node; +use wacore_binary::{Node, NodeRef}; /// A LID mapping learned from usync response #[derive(Debug, Clone)] @@ -81,9 +81,7 @@ pub fn parse_get_user_devices_response_with_phash(resp_node: &Node) -> Result<Ve let key_index_bytes = devices_parent .and_then(|dp| dp.get_optional_child("key-index-list")) .and_then(|ki| match &ki.content { - Some(wacore_binary::node::NodeContent::Bytes(b)) if !b.is_empty() => { - Some(b.clone()) - } + Some(wacore_binary::NodeContent::Bytes(b)) if !b.is_empty() => Some(b.clone()), _ => None, }); @@ -152,9 +150,8 @@ pub fn parse_get_user_devices_response(resp_node: &Node) -> Result<Vec<Jid>> { .collect()) } -/// Parse LID mappings from a usync response. -/// Returns a list of phone -> LID mappings learned from the response. -pub fn parse_lid_mappings_from_response(resp_node: &Node) -> Vec<UsyncLidMapping> { +/// Parse LID mappings from a usync `NodeRef` response (zero-copy path). +pub fn parse_lid_mappings_from_response(resp_node: &NodeRef<'_>) -> Vec<UsyncLidMapping> { let mut mappings = Vec::new(); let list_node = match resp_node.get_optional_child_by_tag(&["usync", "list"]) { @@ -172,27 +169,23 @@ pub fn parse_lid_mappings_from_response(resp_node: &Node) -> Vec<UsyncLidMapping Err(_) => continue, }; - // Only extract mappings for phone number JIDs (not LID JIDs) - if user_jid.server != wacore_binary::jid::DEFAULT_USER_SERVER { + if user_jid.server != wacore_binary::Server::Pn { continue; } - // Look for <lid val="...@lid"> node inside the user node if let Some(lid_node) = user_node.get_optional_child("lid") { let lid_val = match lid_node.attrs().optional_string("val") { Some(v) => v, None => continue, }; - if !lid_val.is_empty() { - // Parse the LID JID to extract just the user part - if let Ok(lid_jid) = lid_val.parse::<Jid>() - && lid_jid.server == wacore_binary::jid::HIDDEN_USER_SERVER - { - mappings.push(UsyncLidMapping { - phone_number: user_jid.user.clone(), - lid: lid_jid.user.clone(), - }); - } + if !lid_val.is_empty() + && let Ok(lid_jid) = lid_val.parse::<Jid>() + && lid_jid.server == wacore_binary::Server::Lid + { + mappings.push(UsyncLidMapping { + phone_number: user_jid.user.clone(), + lid: lid_jid.user.clone(), + }); } } } diff --git a/wacore/src/xml.rs b/wacore/src/xml.rs index 08a9d2fdf..2d4518565 100644 --- a/wacore/src/xml.rs +++ b/wacore/src/xml.rs @@ -1,5 +1,5 @@ use std::fmt::{self, Write as _}; -use wacore_binary::node::{Node, NodeContent, NodeContentRef, NodeRef}; +use wacore_binary::{Node, NodeContent, NodeContentRef, NodeRef}; pub struct DisplayableNode<'a>(pub &'a Node); diff --git a/wacore/tests/binary_protocol_test.rs b/wacore/tests/binary_protocol_test.rs index 9f48dff3e..672724551 100644 --- a/wacore/tests/binary_protocol_test.rs +++ b/wacore/tests/binary_protocol_test.rs @@ -38,7 +38,7 @@ fn test_attr_parser_ref_zero_copy_access() { let marshaled_with_flag = marshal(&original_node).expect("Marshal failed"); let node_ref = unmarshal_ref(&marshaled_with_flag[1..]).expect("unmarshal_ref failed"); - let mut parser = node_ref.attr_parser(); + let mut parser = node_ref.attrs(); assert_eq!(parser.optional_string("xmlns").as_deref(), Some("test")); assert_eq!(parser.optional_string("type").as_deref(), Some("result")); assert!(parser.ok()); @@ -46,7 +46,7 @@ fn test_attr_parser_ref_zero_copy_access() { .finish() .expect("Expected parser to finish without errors"); - let mut parser_with_error = node_ref.attr_parser(); + let mut parser_with_error = node_ref.attrs(); assert!(!parser_with_error.bool("missing")); assert!(parser_with_error.finish().is_err()); } @@ -82,16 +82,14 @@ fn test_unmarshal_ref_known_good_data() { node_ref .get_attr("location") .expect("test data should be valid") - .as_str() - .expect("location should be a string"), + .as_str(), "frc" ); assert_eq!( node_ref .get_attr("props") .expect("test data should be valid") - .as_str() - .expect("props should be a string"), + .as_str(), "27" ); } diff --git a/wacore/tests/jid_test.rs b/wacore/tests/jid_test.rs index edbe93578..e14bcec48 100644 --- a/wacore/tests/jid_test.rs +++ b/wacore/tests/jid_test.rs @@ -40,7 +40,10 @@ fn test_jid_parsing_and_serialization() { fn test_invalid_jid_parsing() { assert!(Jid::from_str("invalidjid").is_err()); - assert!(Jid::from_str("user@server:device").is_ok()); + // Unknown servers are now rejected by the Server enum + assert!(Jid::from_str("user@server:device").is_err()); + // But known servers with device work fine + assert!(Jid::from_str("user@s.whatsapp.net").is_ok()); } #[test]