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
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
21 changes: 21 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,17 @@ 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> {
user_node
.get_optional_child("business")
.and_then(|business| business.get_optional_child("verified_name"))
.and_then(|vn| VerifiedName::try_from_node(vn).ok())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip empty verified-name nodes

When usync returns a per-user <business><verified_name><error .../></verified_name></business> or an empty marker for a business account without a verified-name cert, try_from_node still returns Ok(VerifiedName { name: None, serial: None, certificate: None }) because it never treats error/empty nodes as absent. Both public result types then expose verified_name: Some(...), so callers using is_some() to decide whether a verified business name exists get a false positive; this should mirror the status/picture parsers and return None unless the node actually carries a usable attr/name/certificate and has no error child.

Useful? React with 👍 / 👎.

}

/// Parse common fields from a usync `<user>` node.
Expand All @@ -163,12 +175,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 +210,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 +222,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 +360,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 +457,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
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