use crate::iq::usync::{parse_usync_response, project_device_list_response};
use anyhow::Result;
use wacore_binary::{Jid, Node};
#[derive(Debug, Clone)]
pub struct UsyncLidMapping {
pub phone_number: wacore_binary::CompactString,
pub lid: wacore_binary::CompactString,
}
#[derive(Debug, Clone)]
pub struct UsyncDevice {
pub device: u16,
pub key_index: Option<u32>,
pub is_hosted: bool,
}
impl UsyncDevice {
pub const fn new(device: u16, key_index: Option<u32>) -> Self {
Self {
device,
key_index,
is_hosted: false,
}
}
pub const fn with_hosting(mut self, is_hosted: bool) -> Self {
self.is_hosted = is_hosted;
self
}
}
#[derive(Debug, Clone)]
pub struct UserDeviceList {
pub user: Jid,
pub devices: Vec<UsyncDevice>,
pub phash: Option<String>,
pub key_index_bytes: Option<Vec<u8>>,
}
pub fn parse_get_user_devices_response_with_phash(resp_node: &Node) -> Result<Vec<UserDeviceList>> {
let response = parse_usync_response(&resp_node.as_node_ref())?;
Ok(project_device_list_response(response)?.device_lists)
}
pub fn parse_get_user_devices_response(resp_node: &Node) -> Result<Vec<Jid>> {
Ok(parse_get_user_devices_response_with_phash(resp_node)?
.into_iter()
.flat_map(|u| {
let user_jid = u.user;
u.devices
.into_iter()
.map(move |d| user_jid.with_device_hosting(d.device, d.is_hosted))
})
.collect())
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use wacore_binary::builder::NodeBuilder;
fn build_test_key_index_bytes(device_ids: &[u16]) -> Vec<u8> {
use buffa::Message;
let valid_indexes: Vec<u32> = device_ids.iter().map(|&id| id as u32).collect();
let key_index = waproto::whatsapp::ADVKeyIndexList {
raw_id: Some(1),
timestamp: Some(1000),
current_index: Some(valid_indexes.iter().copied().max().unwrap_or(0)),
valid_indexes,
..Default::default()
};
let signed = waproto::whatsapp::ADVSignedKeyIndexList {
details: Some(key_index.encode_to_vec()),
..Default::default()
};
signed.encode_to_vec()
}
fn build_usync_response(users: Vec<(&str, Vec<u16>, Option<&str>)>) -> Node {
let user_nodes: Vec<Node> = users
.into_iter()
.map(|(jid, device_ids, phash)| {
let device_nodes: Vec<Node> = device_ids
.iter()
.map(|id| NodeBuilder::new("device").attr("id", *id).build())
.collect();
let mut device_list_builder = NodeBuilder::new("device-list");
if let Some(hash) = phash {
device_list_builder = device_list_builder.attr("hash", hash);
}
let device_list = device_list_builder.children(device_nodes).build();
let has_companion = device_ids.iter().any(|&id| id != 0);
let mut devices_children = vec![device_list];
if has_companion {
let ki_bytes = build_test_key_index_bytes(&device_ids);
devices_children.push(
NodeBuilder::new("key-index-list")
.attr("ts", "1000")
.bytes(ki_bytes)
.build(),
);
}
let devices_node = NodeBuilder::new("devices")
.children(devices_children)
.build();
NodeBuilder::new("user")
.attr("jid", jid)
.children([devices_node])
.build()
})
.collect();
let list_node = NodeBuilder::new("list").children(user_nodes).build();
let usync_node = NodeBuilder::new("usync").children([list_node]).build();
NodeBuilder::new("iq").children([usync_node]).build()
}
#[test]
fn test_parse_with_phash_single_user() {
let response = build_usync_response(vec![(
"1234567890@s.whatsapp.net",
vec![0, 1, 2],
Some("2:abcdef123456"),
)]);
let result = parse_get_user_devices_response_with_phash(&response).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].user.user, "1234567890");
assert_eq!(result[0].devices.len(), 3);
assert_eq!(result[0].phash, Some("2:abcdef123456".to_string()));
}
#[test]
fn test_parse_with_phash_multiple_users() {
let response = build_usync_response(vec![
("1111111111@s.whatsapp.net", vec![0, 1], Some("2:hash1")),
("2222222222@s.whatsapp.net", vec![0], Some("2:hash2")),
(
"3333333333@s.whatsapp.net",
vec![0, 1, 2, 3],
Some("2:hash3"),
),
]);
let result = parse_get_user_devices_response_with_phash(&response).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0].user.user, "1111111111");
assert_eq!(result[0].devices.len(), 2);
assert_eq!(result[0].phash, Some("2:hash1".to_string()));
assert_eq!(result[1].user.user, "2222222222");
assert_eq!(result[1].devices.len(), 1);
assert_eq!(result[1].phash, Some("2:hash2".to_string()));
assert_eq!(result[2].user.user, "3333333333");
assert_eq!(result[2].devices.len(), 4);
assert_eq!(result[2].phash, Some("2:hash3".to_string()));
}
#[test]
fn test_parse_without_phash() {
let response = build_usync_response(vec![(
"1234567890@s.whatsapp.net",
vec![0, 1],
None, )]);
let result = parse_get_user_devices_response_with_phash(&response).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].user.user, "1234567890");
assert_eq!(result[0].devices.len(), 2);
assert_eq!(result[0].phash, None);
}
#[test]
fn test_parse_mixed_phash_presence() {
let response = build_usync_response(vec![
("1111111111@s.whatsapp.net", vec![0], Some("2:hasphash")),
("2222222222@s.whatsapp.net", vec![0, 1], None), ]);
let result = parse_get_user_devices_response_with_phash(&response).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].phash, Some("2:hasphash".to_string()));
assert_eq!(result[1].phash, None);
}
#[test]
fn test_parse_device_ids_correct() {
let response = build_usync_response(vec![(
"1234567890@s.whatsapp.net",
vec![0, 5, 10],
Some("2:test"),
)]);
let result = parse_get_user_devices_response_with_phash(&response).unwrap();
assert_eq!(result[0].devices.len(), 3);
assert_eq!(result[0].devices[0].device, 0);
assert_eq!(result[0].devices[1].device, 5);
assert_eq!(result[0].devices[2].device, 10);
}
#[test]
fn test_backward_compat_flat_list() {
let response = build_usync_response(vec![
("1111111111@s.whatsapp.net", vec![0, 1], Some("2:hash1")),
("2222222222@s.whatsapp.net", vec![0], None),
]);
let result = parse_get_user_devices_response(&response).unwrap();
assert_eq!(result.len(), 3); assert_eq!(result[0].user, "1111111111");
assert_eq!(result[0].device, 0);
assert_eq!(result[1].user, "1111111111");
assert_eq!(result[1].device, 1);
assert_eq!(result[2].user, "2222222222");
assert_eq!(result[2].device, 0);
}
#[test]
fn test_user_jid_normalized_to_non_ad() {
let response = build_usync_response(vec![(
"1234567890:5@s.whatsapp.net", vec![0, 1],
Some("2:test"),
)]);
let result = parse_get_user_devices_response_with_phash(&response).unwrap();
assert_eq!(result[0].user.device, 0);
assert_eq!(result[0].user.user, "1234567890");
}
#[test]
fn test_empty_device_list() {
let response = build_usync_response(vec![(
"1234567890@s.whatsapp.net",
vec![], Some("2:empty"),
)]);
let result = parse_get_user_devices_response_with_phash(&response).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].devices.len(), 0);
assert_eq!(result[0].phash, Some("2:empty".to_string()));
}
#[test]
fn test_server_returned_key_index_is_parsed() {
let device_nodes: Vec<Node> = vec![
NodeBuilder::new("device").attr("id", "0").build(),
NodeBuilder::new("device")
.attr("id", "4")
.attr("key-index", "93")
.attr("is_hosted", "true")
.build(),
NodeBuilder::new("device")
.attr("id", "24")
.attr("key-index", "113")
.build(),
];
let ki_bytes = build_test_key_index_bytes(&[0, 4, 24]);
let device_list = NodeBuilder::new("device-list")
.children(device_nodes)
.build();
let devices_node = NodeBuilder::new("devices")
.children(vec![
device_list,
NodeBuilder::new("key-index-list")
.attr("ts", "1000")
.bytes(ki_bytes)
.build(),
])
.build();
let user_node = NodeBuilder::new("user")
.attr("jid", "559900001111@s.whatsapp.net")
.children(vec![devices_node])
.build();
let response = NodeBuilder::new("iq")
.children(vec![
NodeBuilder::new("usync")
.children(vec![
NodeBuilder::new("list").children(vec![user_node]).build(),
])
.build(),
])
.build();
let result = parse_get_user_devices_response_with_phash(&response).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].devices.len(), 3);
assert_eq!(result[0].devices[0].device, 0);
assert_eq!(result[0].devices[0].key_index, None);
assert_eq!(result[0].devices[1].device, 4);
assert_eq!(result[0].devices[1].key_index, Some(93));
assert!(result[0].devices[1].is_hosted);
assert_eq!(result[0].devices[2].device, 24);
assert_eq!(result[0].devices[2].key_index, Some(113));
let flat = parse_get_user_devices_response(&response).unwrap();
assert_eq!(flat[1].server, wacore_binary::Server::Hosted);
}
}