Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
59 changes: 59 additions & 0 deletions src/receipt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ impl Client {
let participant = attrs.optional_jid("participant");
// participant_pn -> sender_alt so the LID-PN cache warms from receipts too.
let participant_pn = attrs.optional_jid("participant_pn");
// Present when this receipt was drained from the offline queue on reconnect.
let offline = attrs.optional_string("offline").is_some();
let stanza_ts = attrs
.optional_u64("t")
.and_then(|t| i64::try_from(t).ok())
Expand Down Expand Up @@ -309,6 +311,7 @@ impl Client {
},
timestamp: user_ts,
r#type: effective_type,
offline,
};
self.core.event_bus.dispatch(Event::Receipt(r));
}
Expand Down Expand Up @@ -336,6 +339,7 @@ impl Client {
},
timestamp: stanza_ts,
r#type: receipt_type,
offline,
};

if receipt.r#type == ReceiptType::Retry {
Expand Down Expand Up @@ -1648,6 +1652,61 @@ mod tests {
);
}

#[tokio::test]
async fn test_receipt_offline_attr_propagated() {
let (client, collector) = setup_client_with_collector().await;

// Drained from the offline queue: carries the `offline` attr.
client
.handle_receipt(node_to_arc(
NodeBuilder::new("receipt")
.attr("from", "15551234567@s.whatsapp.net")
.attr("id", "OFFLINE-RCPT")
.attr("offline", "1")
.attr("t", "1700000000")
.build(),
))
.await;

// Live delivery: no `offline` attr.
client
.handle_receipt(node_to_arc(
NodeBuilder::new("receipt")
.attr("from", "15551234567@s.whatsapp.net")
.attr("id", "LIVE-RCPT")
.attr("t", "1700000000")
.build(),
))
.await;

let events = collector.events();
let receipts: Vec<_> = events
.iter()
.filter_map(|e| match &**e {
Event::Receipt(r) => Some(r),
_ => None,
})
.collect();

let offline = receipts
.iter()
.find(|r| r.message_ids.iter().any(|id| id == "OFFLINE-RCPT"))
.expect("offline receipt dispatched");
assert!(
offline.offline,
"receipt with the offline attr sets offline=true"
);

let live = receipts
.iter()
.find(|r| r.message_ids.iter().any(|id| id == "LIVE-RCPT"))
.expect("live receipt dispatched");
assert!(
!live.offline,
"receipt without the offline attr sets offline=false"
);
}

/// Missing per-user `t`: the fan-out event's timestamp falls back to
/// the stanza-level `t` rather than collapsing to epoch zero (which
/// was the previous behavior).
Expand Down
2 changes: 2 additions & 0 deletions src/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2511,6 +2511,7 @@ mod tests {
message_ids: vec!["MSG001".to_string()],
timestamp: wacore::time::now_utc(),
r#type: crate::types::presence::ReceiptType::Retry,
offline: false,
}
}

Expand Down Expand Up @@ -2626,6 +2627,7 @@ mod tests {
message_ids: vec!["MSG001".to_string()],
timestamp: wacore::time::now_utc(),
r#type: crate::types::presence::ReceiptType::Retry,
offline: false,
};
let info = resolve_retry_chat_info(&receipt, &node.as_node_ref(), None, None);

Expand Down
4 changes: 4 additions & 0 deletions wacore/src/types/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,10 @@ pub struct Receipt {
pub message_ids: Vec<MessageId>,
pub timestamp: DateTime<Utc>,
pub r#type: ReceiptType,
/// True when the receipt carried the `offline` attribute, i.e. it was drained
/// from the server's offline queue on reconnect rather than delivered live.
/// Mirrors WA Web `incomingMsgReceiptParser` (`offline: maybeAttrString`).
pub offline: bool,
}

#[derive(Debug, Clone, Serialize)]
Expand Down
Loading