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
1 change: 1 addition & 0 deletions src/features/contacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use wacore_binary::{Jid, JidExt};
// Re-export types from wacore
pub use wacore::iq::contacts::ProfilePicture;
pub use wacore::iq::usync::{IsOnWhatsAppResult, UserInfo};
pub use wacore::stanza::business::VerifiedName;

pub struct Contacts<'a> {
client: &'a Client,
Expand Down
2 changes: 1 addition & 1 deletion src/features/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub use community::{

pub use chatstate::{ChatStateType, Chatstate};

pub use contacts::{Contacts, IsOnWhatsAppResult, ProfilePicture, UserInfo};
pub use contacts::{Contacts, IsOnWhatsAppResult, ProfilePicture, UserInfo, VerifiedName};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

pub use groups::{
BatchGroupResult, CreateGroupResult, GroupCreateOptions, GroupDescription, GroupJoinError,
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ pub use features::{
PictureType, Presence, PresenceError, PresenceStatus, Profile, ProfilePicture, SecretEncKind,
SecretEncrypted, SetProfilePictureResponse, Signal, Status, StatusPrivacySetting,
StatusSendOptions, SyncActionMessageRange, TcToken, UnlinkSubgroupsResult, UserInfo,
group_type, message_key, message_range,
VerifiedName, group_type, message_key, message_range,
};

pub mod bot;
Expand Down
66 changes: 66 additions & 0 deletions wacore/src/iq/usync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
use crate::WireEnum;
use crate::iq::spec::IqSpec;
use crate::request::InfoQuery;
use crate::stanza::business::VerifiedName;
use anyhow::anyhow;
use log::warn;
use std::collections::HashMap;
Expand Down Expand Up @@ -138,6 +139,27 @@ struct ParsedUserFields {
lid: Option<Jid>,
is_business: bool,
status: Option<String>,
verified_name: Option<VerifiedName>,
}

/// Parses the `<business><verified_name>` certificate that usync returns for
/// business accounts (the `name` lives inside the cert protobuf). Mirrors
/// WAWebUsyncBusiness `businessParser`.
fn parse_verified_name(user_node: &NodeRef<'_>) -> Option<VerifiedName> {
let vn_node = user_node
.get_optional_child("business")?
.get_optional_child("verified_name")?;
// Treat an <error> child or an empty marker (no name, no cert) as absent,
// mirroring the status/picture parsers, so `verified_name.is_some()` means a
// real verified name was returned.
if vn_node.get_optional_child("error").is_some() {
return None;
}
let parsed = VerifiedName::try_from_node(vn_node).ok()?;
if parsed.name.is_none() && parsed.certificate.is_none() {
return None;
}
Some(parsed)
}

/// Parse common fields from a usync `<user>` node.
Expand All @@ -163,12 +185,14 @@ fn parse_user_common_fields(user_node: &NodeRef<'_>) -> Option<ParsedUserFields>
});

let is_business = user_node.get_optional_child("business").is_some();
let verified_name = parse_verified_name(user_node);

Some(ParsedUserFields {
jid,
lid,
is_business,
status,
verified_name,
})
}

Expand Down Expand Up @@ -196,6 +220,8 @@ pub struct IsOnWhatsAppResult {
pub pn_jid: Option<Jid>,
pub is_registered: bool,
pub is_business: bool,
/// Verified business name (decoded from `<business><verified_name>`), if any.
pub verified_name: Option<VerifiedName>,
}

/// User information from usync.
Expand All @@ -206,6 +232,8 @@ pub struct UserInfo {
pub status: Option<String>,
pub picture_id: Option<String>,
pub is_business: bool,
/// Verified business name (decoded from `<business><verified_name>`), if any.
pub verified_name: Option<VerifiedName>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -342,13 +370,15 @@ impl IqSpec for IsOnWhatsAppSpec {
};

let is_business = user_node.get_optional_child("business").is_some();
let verified_name = parse_verified_name(user_node);

results.push(IsOnWhatsAppResult {
jid,
lid,
pn_jid,
is_registered,
is_business,
verified_name,
});
}

Expand Down Expand Up @@ -437,6 +467,7 @@ impl IqSpec for UserInfoSpec {
status: fields.status,
picture_id: parse_picture_id_string(user_node),
is_business: fields.is_business,
verified_name: fields.verified_name,
},
);
}
Expand Down Expand Up @@ -1277,4 +1308,39 @@ mod tests {
assert_eq!(result.lid_mappings[0].phone_number, "1234567890");
assert_eq!(result.lid_mappings[0].lid, "100000012345678");
}

#[test]
fn parse_verified_name_skips_error_and_empty() {
let user = |vn: Node| {
NodeBuilder::new("user")
.children([NodeBuilder::new("business").children([vn]).build()])
.build()
};

// <verified_name><error/></verified_name> -> absent
let err = user(
NodeBuilder::new("verified_name")
.children([NodeBuilder::new("error").attr("code", "404").build()])
.build(),
);
assert!(parse_verified_name(&err.as_node_ref()).is_none());

// empty <verified_name/> (no attrs, no cert) -> absent
let empty = user(NodeBuilder::new("verified_name").build());
assert!(parse_verified_name(&empty.as_node_ref()).is_none());

// real name attr -> present
let real = user(
NodeBuilder::new("verified_name")
.attr("name", "Acme")
.build(),
);
assert_eq!(
parse_verified_name(&real.as_node_ref())
.expect("real name")
.name
.as_deref(),
Some("Acme")
);
}
}
53 changes: 51 additions & 2 deletions wacore/src/stanza/business.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! Reference: WhatsApp Web `WAWebHandleBusinessNotification`

use anyhow::{Result, anyhow};
use prost::Message as _;
use serde::Serialize;
use wacore_binary::Jid;
use wacore_binary::NodeRef;
Expand Down Expand Up @@ -58,11 +59,12 @@ impl VerifiedName {
})
});

let serial = node
let mut name = name;
let mut serial = node
.attrs()
.optional_string("serial")
.map(|s| s.into_owned());
let issuer = node
let mut issuer = node
.attrs()
.optional_string("issuer")
.map(|s| s.into_owned());
Expand All @@ -71,6 +73,20 @@ impl VerifiedName {
_ => None,
};

// usync's `<verified_name>` carries no `name`/`serial` attrs; the name
// lives only inside the certificate protobuf (content bytes). Decode it
// to fill the missing fields, matching WAWebCommonParsersVerifiedName.
if let Some(cert_bytes) = certificate.as_deref()
&& let Ok(cert) = waproto::whatsapp::VerifiedNameCertificate::decode(cert_bytes)
&& let Some(details_bytes) = cert.details.as_deref()
&& let Ok(details) =
waproto::whatsapp::verified_name_certificate::Details::decode(details_bytes)
{
name = name.or(details.verified_name);
serial = serial.or_else(|| details.serial.map(|s| s.to_string()));
issuer = issuer.or(details.issuer);
}

Ok(Self {
name,
serial,
Expand Down Expand Up @@ -393,6 +409,39 @@ mod tests {
assert!(parsed.is_business_removed());
}

#[test]
fn verified_name_decodes_certificate_content_bytes() {
// usync's <verified_name> has no name/serial attrs; the name lives inside
// the certificate protobuf carried as content bytes.
let details = waproto::whatsapp::verified_name_certificate::Details {
verified_name: Some("Acme Inc".to_string()),
serial: Some(42),
..Default::default()
};
let cert = waproto::whatsapp::VerifiedNameCertificate {
details: Some(details.encode_to_vec()),
..Default::default()
};
let node = NodeBuilder::new("verified_name")
.bytes(cert.encode_to_vec())
.build();
let vn = VerifiedName::try_from_node(&node.as_node_ref()).expect("parse");
assert_eq!(vn.name.as_deref(), Some("Acme Inc"));
assert_eq!(vn.serial.as_deref(), Some("42"));
assert!(vn.certificate.is_some());
}

#[test]
fn verified_name_prefers_attr_name_over_certificate() {
let node = NodeBuilder::new("verified_name")
.attr("name", "Attr Name")
.attr("serial", "7")
.build();
let vn = VerifiedName::try_from_node(&node.as_node_ref()).expect("parse");
assert_eq!(vn.name.as_deref(), Some("Attr Name"));
assert_eq!(vn.serial.as_deref(), Some("7"));
}

#[test]
fn test_parse_remove_hash_notification() {
let node = NodeBuilder::new("notification")
Expand Down
Loading