diff --git a/src/features/contacts.rs b/src/features/contacts.rs index 4f5681796..95d3b32f9 100644 --- a/src/features/contacts.rs +++ b/src/features/contacts.rs @@ -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, diff --git a/src/features/mod.rs b/src/features/mod.rs index d76c0bc2d..394cbb169 100644 --- a/src/features/mod.rs +++ b/src/features/mod.rs @@ -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}; pub use groups::{ BatchGroupResult, CreateGroupResult, GroupCreateOptions, GroupDescription, GroupJoinError, diff --git a/src/lib.rs b/src/lib.rs index 3c71c0330..a4d3c4fe2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/wacore/src/iq/usync.rs b/wacore/src/iq/usync.rs index 31a103dd6..9133edfbe 100644 --- a/wacore/src/iq/usync.rs +++ b/wacore/src/iq/usync.rs @@ -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; @@ -138,6 +139,27 @@ struct ParsedUserFields { lid: Option, is_business: bool, status: Option, + verified_name: Option, +} + +/// Parses the `` 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 { + let vn_node = user_node + .get_optional_child("business")? + .get_optional_child("verified_name")?; + // Treat an 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 `` node. @@ -163,12 +185,14 @@ fn parse_user_common_fields(user_node: &NodeRef<'_>) -> Option }); 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, }) } @@ -196,6 +220,8 @@ pub struct IsOnWhatsAppResult { pub pn_jid: Option, pub is_registered: bool, pub is_business: bool, + /// Verified business name (decoded from ``), if any. + pub verified_name: Option, } /// User information from usync. @@ -206,6 +232,8 @@ pub struct UserInfo { pub status: Option, pub picture_id: Option, pub is_business: bool, + /// Verified business name (decoded from ``), if any. + pub verified_name: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -342,6 +370,7 @@ 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, @@ -349,6 +378,7 @@ impl IqSpec for IsOnWhatsAppSpec { pn_jid, is_registered, is_business, + verified_name, }); } @@ -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, }, ); } @@ -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() + }; + + // -> 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 (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") + ); + } } diff --git a/wacore/src/stanza/business.rs b/wacore/src/stanza/business.rs index 44e96417a..259c0e73f 100644 --- a/wacore/src/stanza/business.rs +++ b/wacore/src/stanza/business.rs @@ -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; @@ -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()); @@ -71,6 +73,20 @@ impl VerifiedName { _ => None, }; + // usync's `` 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, @@ -393,6 +409,39 @@ mod tests { assert!(parsed.is_business_removed()); } + #[test] + fn verified_name_decodes_certificate_content_bytes() { + // usync's 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")