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
18 changes: 3 additions & 15 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4023,21 +4023,9 @@ impl Client {
}
let device_snapshot = self.persistence_manager.get_device_snapshot().await;
if let Some(own_jid) = &device_snapshot.pn {
let type_str = match receipt_type {
crate::types::presence::ReceiptType::HistorySync => "hist_sync",
crate::types::presence::ReceiptType::Read => "read",
crate::types::presence::ReceiptType::ReadSelf => "read-self",
crate::types::presence::ReceiptType::Delivered => "delivery",
crate::types::presence::ReceiptType::Played => "played",
crate::types::presence::ReceiptType::PlayedSelf => "played-self",
crate::types::presence::ReceiptType::Inactive => "inactive",
crate::types::presence::ReceiptType::PeerMsg => "peer_msg",
crate::types::presence::ReceiptType::Sender => "sender",
crate::types::presence::ReceiptType::ServerError => "server-error",
crate::types::presence::ReceiptType::Retry => "retry",
crate::types::presence::ReceiptType::EncRekeyRetry => "enc_rekey_retry",
crate::types::presence::ReceiptType::Other(ref s) => s.as_str(),
};
// Single source of truth for the wire mapping (ReceiptType::Sent is a derived
// incoming-only state and is never sent by us).
let type_str = receipt_type.as_wire_str();

let node = NodeBuilder::new("receipt")
.attrs([
Expand Down
12 changes: 11 additions & 1 deletion src/receipt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ impl Client {
.unwrap_or_else(wacore::time::now_utc);

let receipt_type = ReceiptType::parse(receipt_type_str);
// WA Web downgrades a delivery ack to "sent" (not delivered) when the receipt carries
// <error reason="lid" type="feature-incapable"> (the LID peer can't receive it).
let receipt_type =
wacore::stanza::receipt::downgrade_for_feature_incapable(nr, receipt_type);
Comment thread
jlucaso1 marked this conversation as resolved.
let is_view = receipt_type_str == "view";
let is_group = from.is_group();
let default_sender = if is_group {
Expand Down Expand Up @@ -189,7 +193,13 @@ impl Client {
// aggregated_by_message: each <user> carries its own type;
// aggregated_by_type: all users share the receipt-level type.
let effective_type = match user.r#type.as_deref() {
Some(t) => ReceiptType::parse(t),
// Apply the receipt-level feature-incapable downgrade to the per-user type
// too, so an aggregated delivery receipt with a feature-incapable LID
// participant doesn't re-emit a delivered tick for it.
Some(t) => wacore::stanza::receipt::downgrade_for_feature_incapable(
nr,
ReceiptType::parse(t),
),
None => receipt_type.clone(),
};
let r = Receipt {
Expand Down
69 changes: 69 additions & 0 deletions wacore/src/stanza/receipt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! Orchestration and dispatch remain in `whatsapp-rust/src/receipt.rs`.

use crate::types::message::{MessageCategory, MessageInfo};
use crate::types::presence::ReceiptType;
use wacore_binary::NodeRef;
use wacore_binary::{Jid, JidExt as _, STATUS_BROADCAST_USER};

Expand Down Expand Up @@ -130,11 +131,79 @@ pub fn should_send_delivery_receipt(info: &MessageInfo) -> bool {
|| info.source.is_self_fanout()
}

/// WA Web's receipt parser downgrades a delivery ack to "sent" (not delivered) when the
/// `<receipt>` carries `<error reason="lid" type="feature-incapable">`: the LID peer is
/// feature-incapable and never received the message. Returns the effective receipt type.
///
/// Scoped to `Delivered` (the only type that carries this error in practice), which also
/// keeps the downgrade from rerouting retry / enc-rekey receipts.
pub fn downgrade_for_feature_incapable(
node: &NodeRef<'_>,
parsed_type: ReceiptType,
) -> ReceiptType {
if parsed_type != ReceiptType::Delivered {
return parsed_type;
}
let Some(err) = node.get_optional_child("error") else {
return parsed_type;
};
let mut a = err.attrs();
let reason = a.optional_string("reason");
let err_type = a.optional_string("type");
if reason.as_deref() == Some("lid") && err_type.as_deref() == Some("feature-incapable") {
ReceiptType::Sent
} else {
parsed_type
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::types::message::{MessageCategory, MessageInfo, MessageSource};

#[test]
fn feature_incapable_error_downgrades_delivery_to_sent() {
use wacore_binary::builder::NodeBuilder;

let with_error = NodeBuilder::new("receipt")
.children([NodeBuilder::new("error")
.attr("reason", "lid")
.attr("type", "feature-incapable")
.build()])
.build();
assert_eq!(
downgrade_for_feature_incapable(&with_error.as_node_ref(), ReceiptType::Delivered),
ReceiptType::Sent,
"lid/feature-incapable error downgrades delivery to sent"
);

// No <error> child: unchanged.
let plain = NodeBuilder::new("receipt").build();
assert_eq!(
downgrade_for_feature_incapable(&plain.as_node_ref(), ReceiptType::Delivered),
ReceiptType::Delivered
);

// Different error type: unchanged.
let other = NodeBuilder::new("receipt")
.children([NodeBuilder::new("error")
.attr("reason", "lid")
.attr("type", "other")
.build()])
.build();
assert_eq!(
downgrade_for_feature_incapable(&other.as_node_ref(), ReceiptType::Delivered),
ReceiptType::Delivered
);

// Non-delivery type with the same error: not downgraded (scoped to Delivered).
assert_eq!(
downgrade_for_feature_incapable(&with_error.as_node_ref(), ReceiptType::Read),
ReceiptType::Read
);
}

#[test]
fn skip_empty_id() {
let info = MessageInfo {
Expand Down
6 changes: 6 additions & 0 deletions wacore/src/types/presence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ pub enum ChatPresenceMedia {
#[serde(from = "String")]
pub enum ReceiptType {
Delivered,
/// Sent but NOT delivered: WA Web downgrades a delivery ack to this when the
/// receipt carries `<error reason="lid" type="feature-incapable">` (the LID peer
/// can't receive the message). Produced by the receipt parser, not sent by us.
Sent,
Sender,
Retry,
/// VoIP call encryption re-keying retry.
Expand Down Expand Up @@ -55,6 +59,7 @@ impl ReceiptType {
fn from_known(s: &str) -> Option<Self> {
Some(match s {
"" | "delivery" => Self::Delivered,
"sent" => Self::Sent,
"sender" => Self::Sender,
"retry" => Self::Retry,
"enc_rekey_retry" => Self::EncRekeyRetry,
Expand All @@ -79,6 +84,7 @@ impl ReceiptType {
pub fn as_wire_str(&self) -> &str {
match self {
Self::Delivered => "delivery",
Self::Sent => "sent",
Self::Sender => "sender",
Self::Retry => "retry",
Self::EncRekeyRetry => "enc_rekey_retry",
Expand Down
Loading