Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 33 additions & 45 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,27 @@ struct NodeWaiter {
tx: futures::channel::oneshot::Sender<Arc<Node>>,
}

struct SentNodeWaiter {
filter: NodeFilter,
tx: futures::channel::oneshot::Sender<Arc<Node>>,
fn resolve_waiters(
waiters_mutex: &std::sync::Mutex<Vec<NodeWaiter>>,
counter: &AtomicUsize,
node: &Arc<Node>,
) {
let mut waiters = waiters_mutex
.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);
counter.fetch_sub(1, Ordering::Release);
} else if waiters[i].filter.matches(node) {
let w = waiters.swap_remove(i);
counter.fetch_sub(1, Ordering::Release);
let _ = w.tx.send(Arc::clone(node));
} else {
i += 1;
}
}
}

use async_lock::Mutex;
Expand Down Expand Up @@ -295,7 +313,7 @@ pub struct Client {
node_waiters: std::sync::Mutex<Vec<NodeWaiter>>,
node_waiter_count: AtomicUsize,
/// Waiters for raw outgoing nodes before encryption.
sent_node_waiters: std::sync::Mutex<Vec<SentNodeWaiter>>,
sent_node_waiters: std::sync::Mutex<Vec<NodeWaiter>>,
sent_node_waiter_count: AtomicUsize,

pub(crate) unique_id: String,
Expand Down Expand Up @@ -1338,9 +1356,13 @@ impl Client {
}

/// [`flush_signal_cache`](Self::flush_signal_cache) with error logging instead of propagation.
pub(crate) async fn flush_signal_cache_logged(&self, context: &str) {
pub(crate) async fn flush_signal_cache_logged(&self, context: &str, id: Option<&str>) {
if let Err(e) = self.flush_signal_cache().await {
log::error!("Failed to flush signal cache ({context}): {e:?}");
if let Some(id) = id {
log::error!("Failed to flush signal cache ({context} {id}): {e:?}");
} else {
log::error!("Failed to flush signal cache ({context}): {e:?}");
}
}
}

Expand Down Expand Up @@ -1849,11 +1871,11 @@ impl Client {
let id = self.generate_request_id();

let stanza = wacore_binary::builder::NodeBuilder::new("call")
.attr("to", call_from.clone())
.attr("to", call_from)
.attr("id", id)
.children([wacore_binary::builder::NodeBuilder::new("reject")
.attr("call-id", call_id)
.attr("call-creator", call_from.clone())
.attr("call-creator", call_from)
.attr("count", "0")
.build()])
.build();
Expand Down Expand Up @@ -3216,52 +3238,18 @@ impl Client {
.sent_node_waiters
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
waiters.push(SentNodeWaiter { filter, tx });
waiters.push(NodeWaiter { 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<Node>) {
let mut waiters = self
.node_waiters
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let mut i = 0;
while i < waiters.len() {
if waiters[i].tx.is_canceled() {
// Receiver dropped — clean up
waiters.swap_remove(i);
self.node_waiter_count.fetch_sub(1, Ordering::Release);
} else if waiters[i].filter.matches(node) {
// Match found — remove and send
let w = waiters.swap_remove(i);
self.node_waiter_count.fetch_sub(1, Ordering::Release);
let _ = w.tx.send(Arc::clone(node));
} else {
i += 1;
}
}
resolve_waiters(&self.node_waiters, &self.node_waiter_count, node);
}

fn resolve_sent_node_waiters(&self, node: &Arc<Node>) {
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(node) {
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;
}
}
resolve_waiters(&self.sent_node_waiters, &self.sent_node_waiter_count, node);
}

fn clear_sent_node_waiters(&self) {
Expand Down
2 changes: 1 addition & 1 deletion src/client/device_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ impl Client {
}
}
}
self.flush_signal_cache_logged("delete_sessions_for_devices")
self.flush_signal_cache_logged("delete_sessions_for_devices", None)
.await;
}

Expand Down
2 changes: 1 addition & 1 deletion src/features/chatstate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ impl<'a> Chatstate<'a> {
};

NodeBuilder::new("chatstate")
.attr("to", to.clone())
.attr("to", to)
.children([child])
.build()
}
Expand Down
2 changes: 1 addition & 1 deletion src/features/newsletter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ impl<'a> Newsletter<'a> {
let request_id = self.client.generate_message_id().await;

let stanza = NodeBuilder::new("message")
.attr("to", jid.clone())
.attr("to", jid)
.attr("type", "reaction")
.attr("id", &request_id)
.attr("server_id", server_id.to_string())
Expand Down
4 changes: 2 additions & 2 deletions src/features/presence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl<'a> Presence<'a> {
async fn build_subscription_node(&self, jid: &Jid) -> Node {
let mut builder = NodeBuilder::new("presence")
.attr("type", "subscribe")
.attr("to", jid.clone());
.attr("to", jid);

// Include tctoken if available (no t attribute, matching WhatsApp Web)
if let Some(token) = self.client.lookup_tc_token_for_jid(jid).await {
Expand All @@ -60,7 +60,7 @@ impl<'a> Presence<'a> {
fn build_unsubscription_node(&self, jid: &Jid) -> Node {
NodeBuilder::new("presence")
.attr("type", "unsubscribe")
.attr("to", jid.clone())
.attr("to", jid)
.build()
}

Expand Down
21 changes: 10 additions & 11 deletions src/features/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ impl<'a> Signal<'a> {
// Resolve PN→LID to use the correct Signal session (matches send path)
let encryption_jid = self.client.resolve_encryption_jid(jid).await;
let signal_addr = encryption_jid.to_protocol_address();
let signal_addr_str = signal_addr.to_string();

let lock = self.client.session_lock_for(&signal_addr_str).await;
let lock = self.client.session_lock_for(signal_addr.as_str()).await;
let _guard = lock.lock().await;
let mut adapter = self.client.signal_adapter().await;

Expand Down Expand Up @@ -93,9 +92,8 @@ impl<'a> Signal<'a> {

let encryption_jid = self.client.resolve_encryption_jid(jid).await;
let signal_addr = encryption_jid.to_protocol_address();
let signal_addr_str = signal_addr.to_string();

let lock = self.client.session_lock_for(&signal_addr_str).await;
let lock = self.client.session_lock_for(signal_addr.as_str()).await;
let _guard = lock.lock().await;
let mut adapter = self.client.signal_adapter().await;
let mut rng = rand::make_rng::<rand::rngs::StdRng>();
Expand Down Expand Up @@ -135,7 +133,7 @@ impl<'a> Signal<'a> {
) -> Result<(Option<Vec<u8>>, Vec<u8>)> {
let own_jid = self.client.get_own_jid_for_group(group_jid).await?;
let sender_addr = own_jid.to_protocol_address();
let sender_key_name = SenderKeyName::new(group_jid.to_string(), sender_addr.to_string());
let sender_key_name = SenderKeyName::from_jid(group_jid, &sender_addr);

// Only create SKDM when no sender key exists (matches WA Web behavior)
let device_store = self.client.persistence_manager.get_device_arc().await;
Expand Down Expand Up @@ -238,9 +236,8 @@ impl<'a> Signal<'a> {
for jid in jids {
let resolved = self.client.resolve_encryption_jid(jid).await;
let addr = resolved.to_protocol_address();
let addr_str = addr.to_string();

let lock = self.client.session_lock_for(&addr_str).await;
let lock = self.client.session_lock_for(addr.as_str()).await;
let _guard = lock.lock().await;

// WA Web removes session + identity together (deleteRemoteSession)
Expand All @@ -267,10 +264,12 @@ impl<'a> Signal<'a> {
self.client.ensure_e2e_sessions(&device_jids).await?;

// Acquire per-device session locks before encrypting (matches DM send path)
let lock_keys = self.client.build_session_lock_keys(&device_jids).await;
let mut session_mutexes = Vec::with_capacity(lock_keys.len());
for key in &lock_keys {
session_mutexes.push(self.client.session_lock_for(key).await);
let lock_jids = self.client.build_session_lock_keys(&device_jids).await;
let mut session_mutexes = Vec::with_capacity(lock_jids.len());
let mut lock_buf = String::with_capacity(64);
for jid in &lock_jids {
wacore::types::jid::write_protocol_address_to(jid, &mut lock_buf);
session_mutexes.push(self.client.session_lock_for(&lock_buf).await);
}
let mut _session_guards = Vec::with_capacity(session_mutexes.len());
for mutex in &session_mutexes {
Expand Down
26 changes: 7 additions & 19 deletions src/handlers/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,17 +377,16 @@ async fn handle_identity_change(client: &Arc<Client>, node: &Node) {

let status_group = "status@broadcast";
for own_jid in device_snapshot.pn.iter().chain(device_snapshot.lid.iter()) {
let sk_name = SenderKeyName::new(
status_group.to_string(),
own_jid.to_protocol_address().to_string(),
);
let sk_name = SenderKeyName::from_jid(&status_group, &own_jid.to_protocol_address());
client
.signal_cache
.delete_sender_key(sk_name.cache_key())
.await;
}

client.flush_signal_cache_logged("identity change").await;
client
.flush_signal_cache_logged("identity change", None)
.await;
}

// Force fresh usync on next send
Expand Down Expand Up @@ -894,11 +893,7 @@ fn handle_picture_notification(client: &Arc<Client>, node: &Node) {
}
};

let timestamp = node
.attrs()
.optional_u64("t")
.map(|t| chrono::DateTime::from_timestamp(t as i64, 0).unwrap_or_else(chrono::Utc::now))
.unwrap_or_else(chrono::Utc::now);
let timestamp = notification_timestamp(node);

Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Look for <set>, <delete>, or <request> child to determine the action.
// WhatsApp Web has two formats:
Expand Down Expand Up @@ -988,11 +983,7 @@ fn handle_status_notification(client: &Arc<Client>, node: &Node) {
}
};

let timestamp = node
.attrs()
.optional_u64("t")
.map(|t| chrono::DateTime::from_timestamp(t as i64, 0).unwrap_or_else(chrono::Utc::now))
.unwrap_or_else(chrono::Utc::now);
let timestamp = notification_timestamp(node);

if let Some(set_node) = node.get_optional_child("set") {
let status_text = match &set_node.content {
Expand Down Expand Up @@ -2130,10 +2121,7 @@ mod tests {
.await;

// Pre-populate a sender key for status@broadcast
let sk_name = SenderKeyName::new(
"status@broadcast".to_string(),
own_jid.to_protocol_address().to_string(),
);
let sk_name = SenderKeyName::from_jid(&"status@broadcast", &own_jid.to_protocol_address());
let sk_record = wacore::libsignal::protocol::SenderKeyRecord::new_empty();
client
.signal_cache
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/presence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl StanzaHandler for PresenceHandler {
let last_seen = node
.attrs
.get("last")
.and_then(|v| v.to_string().parse::<i64>().ok())
.and_then(|v| v.as_str().parse::<i64>().ok())
.and_then(|ts| chrono::DateTime::from_timestamp(ts, 0));

debug!(
Expand Down
2 changes: 1 addition & 1 deletion src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ impl Client {
}

// Flush cached Signal state to DB (matches WA Web's flushBufferToDiskIfNotMemOnlyMode)
self.flush_signal_cache_logged(&format!("message {}", info.id))
self.flush_signal_cache_logged("message", Some(&info.id))
.await;
}

Expand Down
8 changes: 4 additions & 4 deletions src/receipt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ impl Client {

let mut builder = NodeBuilder::new("receipt")
.attr("id", &info.id)
.attr("to", info.source.chat.clone());
.attr("to", &info.source.chat);

// WA Web: peer device messages (category="peer") use type="peer_msg".
// Normal delivery receipts omit the type attribute (DROP_ATTR).
Expand All @@ -140,7 +140,7 @@ impl Client {

// For group messages, the 'participant' attribute is required to identify the sender.
if info.source.is_group {
builder = builder.attr("participant", info.source.sender.clone());
builder = builder.attr("participant", &info.source.sender);
}

let receipt_node = builder.build();
Expand Down Expand Up @@ -172,13 +172,13 @@ impl Client {
let timestamp = (wacore::time::now_secs() as u64).to_string();

let mut builder = NodeBuilder::new("receipt")
.attr("to", chat.clone())
.attr("to", chat)
.attr("type", "read")
.attr("id", &message_ids[0])
.attr("t", &timestamp);

if let Some(sender) = sender {
builder = builder.attr("participant", sender.clone());
builder = builder.attr("participant", sender);
}

// Additional message IDs go into <list><item id="..."/></list>
Expand Down
Loading
Loading