Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
70 changes: 27 additions & 43 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 @@ -1849,11 +1867,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 +3234,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/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
13 changes: 5 additions & 8 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 Down Expand Up @@ -270,7 +267,7 @@ impl<'a> Signal<'a> {
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);
session_mutexes.push(self.client.session_lock_for(key.as_str()).await);
}
let mut _session_guards = Vec::with_capacity(session_mutexes.len());
for mutex in &session_mutexes {
Expand Down
22 changes: 4 additions & 18 deletions src/handlers/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,10 +377,7 @@ 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())
Expand Down Expand Up @@ -894,11 +891,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 +981,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 +2119,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
3 changes: 1 addition & 2 deletions src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,8 +578,7 @@ impl Client {
}

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Preserve per-message flush context without reintroducing allocations.

Line 581 drops message-level correlation in error logs. You can keep zero-allocation behavior and retain debuggability by passing the message ID directly.

Suggested tweak
-        self.flush_signal_cache_logged("message dispatch").await;
+        self.flush_signal_cache_logged(info.id.as_str()).await;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.flush_signal_cache_logged("message dispatch").await;
self.flush_signal_cache_logged(info.id.as_str()).await;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/message.rs` at line 581, The call to
self.flush_signal_cache_logged("message dispatch").await loses per-message
correlation in logs; modify the flush_signal_cache_logged call to accept and
forward the message ID (or reference to the message's correlation id) so logs
include that context without allocating (e.g., add a parameter like message_id:
&str or &MessageId and pass the existing id from the current message before
awaiting). Update the function signature of flush_signal_cache_logged and its
callers (and log statements inside it) to accept this id reference and use it in
error/debug messages while preserving zero-allocation usage.

}

async fn process_session_enc_batch(
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
22 changes: 10 additions & 12 deletions src/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,10 +308,8 @@ impl Client {

for own_jid in jids_to_delete {
use wacore::libsignal::store::sender_key_name::SenderKeyName;
let sk_name = SenderKeyName::new(
group_jid.clone(),
own_jid.to_protocol_address().to_string(),
);
let sk_name =
SenderKeyName::from_jid(&group_jid, &own_jid.to_protocol_address());
self.signal_cache
.delete_sender_key(sk_name.cache_key())
.await;
Expand Down Expand Up @@ -780,12 +778,12 @@ impl Client {
// to identify which group member should resend. For DMs, omit it since the
// "to" address already identifies the sender.
let mut builder = NodeBuilder::new("receipt")
.attr("to", receipt_to.clone())
.attr("to", receipt_to)
.attr("id", info.id.clone())
.attr("type", "retry");

if info.source.is_group {
builder = builder.attr("participant", info.source.sender.clone());
builder = builder.attr("participant", &info.source.sender);
}

// Handle peer vs device sync messages (matches WhatsApp Web's sendRetryReceipt):
Expand All @@ -809,7 +807,7 @@ impl Client {
// Include recipient so the sender can look up the original message.
// Without this, the retry fails silently (getTargetChat returns null).
let recipient = info.source.recipient.as_ref().unwrap_or(&info.source.chat);
builder = builder.attr("recipient", recipient.clone());
builder = builder.attr("recipient", recipient);
}
}
}
Expand Down Expand Up @@ -851,7 +849,7 @@ impl Client {

// WA Web: <enc_rekey call-creator="JID" call-id="..." count="N"/>
let enc_rekey_node = NodeBuilder::new("enc_rekey")
.attr("call-creator", call_creator.clone())
.attr("call-creator", call_creator)
.attr("call-id", call_id)
.attr("count", retry_count.to_string())
.build();
Expand All @@ -861,7 +859,7 @@ impl Client {
.build();

let receipt_node = NodeBuilder::new("receipt")
.attr("to", peer_jid.clone())
.attr("to", peer_jid)
.attr("id", stanza_id)
.attr("type", "enc_rekey_retry")
.children([enc_rekey_node, registration_node])
Expand Down Expand Up @@ -993,12 +991,12 @@ mod tests {
our_lid: &Jid,
) -> wacore_binary::node::Node {
let mut builder = NodeBuilder::new("receipt")
.attr("to", info.source.sender.clone())
.attr("to", &info.source.sender)
.attr("id", info.id.clone())
.attr("type", "retry");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

if info.source.is_group {
builder = builder.attr("participant", info.source.sender.clone());
builder = builder.attr("participant", &info.source.sender);
}

if !info.source.is_group {
Expand All @@ -1010,7 +1008,7 @@ mod tests {
builder = builder.attr("category", MessageCategory::Peer.as_str());
} else {
let recipient = info.source.recipient.as_ref().unwrap_or(&info.source.chat);
builder = builder.attr("recipient", recipient.clone());
builder = builder.attr("recipient", recipient);
}
}
}
Expand Down
Loading
Loading