Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
379 changes: 216 additions & 163 deletions src/client/device_registry.rs

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions src/client/lid_pn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2093,10 +2093,7 @@ mod tests {
backend
.update_device_list(DeviceListRecord {
user: pn.to_string(),
devices: vec![DeviceInfo {
device_id: 3,
key_index: None,
}],
devices: vec![DeviceInfo::new(3, None)],
timestamp: wacore::time::now_secs(),
phash: None,
raw_id: None,
Expand Down
11 changes: 1 addition & 10 deletions src/features/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1266,16 +1266,7 @@ mod tests {
client
.update_device_list(DeviceListRecord {
user: recipient.user.to_string(),
devices: vec![
DeviceInfo {
device_id: 0,
key_index: None,
},
DeviceInfo {
device_id: 1,
key_index: None,
},
],
devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(1, None)],
timestamp: wacore::time::now_secs(),
phash: None,
raw_id: None,
Expand Down
6 changes: 3 additions & 3 deletions src/handlers/notification/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,9 +615,9 @@ pub(crate) async fn handle_account_sync_devices(
user: from_jid.user.to_string(),
devices: devices
.iter()
.map(|d| DeviceInfo {
device_id: d.jid.device as u32,
key_index: d.key_index,
.map(|d| {
DeviceInfo::new(d.jid.device as u32, d.key_index)
.with_hosting(wacore_binary::JidExt::is_hosted(&d.jid))
})
.collect(),
timestamp,
Expand Down
10 changes: 2 additions & 8 deletions src/handlers/notification/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -884,10 +884,7 @@ mod tests {
// Pre-populate device registry so clear_device_record has something to clear
let record = wacore::store::traits::DeviceListRecord {
user: "5511999999999".into(),
devices: vec![wacore::store::traits::DeviceInfo {
device_id: 1,
key_index: None,
}],
devices: vec![wacore::store::traits::DeviceInfo::new(1, None)],
timestamp: wacore::time::now_secs(),
phash: None,
raw_id: Some(42),
Expand Down Expand Up @@ -1201,10 +1198,7 @@ mod tests {
"5511666666666".into(),
Arc::new(wacore::store::traits::DeviceListRecord {
user: "5511666666666".into(),
devices: vec![wacore::store::traits::DeviceInfo {
device_id: 1,
key_index: None,
}],
devices: vec![wacore::store::traits::DeviceInfo::new(1, None)],
timestamp: wacore::time::now_secs(),
phash: None,
raw_id: Some(1),
Expand Down
10 changes: 2 additions & 8 deletions src/send/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3032,10 +3032,7 @@ mod tests {
for user in &participant_users {
let record = DeviceListRecord {
user: (*user).into(),
devices: vec![DeviceInfo {
device_id: 0,
key_index: None,
}],
devices: vec![DeviceInfo::new(0, None)],
timestamp: wacore::time::now_secs(),
phash: None,
raw_id: None,
Expand Down Expand Up @@ -4472,10 +4469,7 @@ mod tests {
client
.update_device_list(wacore::store::traits::DeviceListRecord {
user,
devices: vec![wacore::store::traits::DeviceInfo {
device_id: 0,
key_index: None,
}],
devices: vec![wacore::store::traits::DeviceInfo::new(0, None)],
timestamp: wacore::time::now_secs(),
phash: None,
raw_id: None,
Expand Down
128 changes: 73 additions & 55 deletions src/usync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,35 @@
//! Device list IQ specification is defined in `wacore::iq::usync`.

use crate::client::Client;
use crate::request::IqError;
use log::{debug, warn};
use std::collections::HashSet;
use wacore::iq::usync::{DeviceListResponse, DeviceListSpec};
use wacore_binary::Jid;

pub use wacore::iq::usync::{
UsyncAddressingMode, UsyncBotCommand, UsyncBotProfessionalType, UsyncBotProfileResult,
UsyncBotPrompt, UsyncBusinessResult, UsyncContactResult, UsyncContext, UsyncDeviceListResult,
UsyncDeviceResult, UsyncDeviceSyncHint, UsyncDevicesResult, UsyncDisappearingModeResult,
UsyncFeature, UsyncFeatureResult, UsyncKeyIndexResult, UsyncMode, UsyncOutcome, UsyncProtocol,
UsyncProtocolKind, UsyncProtocolResult, UsyncProtocolState, UsyncQuery, UsyncResponse,
UsyncStatusResult, UsyncSubprotocolError, UsyncTextStatusResult, UsyncUser, UsyncUserResult,
UsyncValidationError,
};

impl Client {
/// Executes a typed USync query.
///
/// The client generates the protocol `sid` independently from the IQ ID,
/// matching WhatsApp Web. This neutral operation only returns decoded wire
/// data; cache and persistence effects remain in specialized client APIs.
pub async fn query_usync(&self, query: UsyncQuery) -> Result<UsyncResponse, IqError> {
let sid = self.generate_request_id();
let spec = wacore::iq::usync::UsyncQuerySpec::new(query, sid)
.map_err(|error| IqError::EncodeError(error.into()))?;
self.execute(spec).await
}

#[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.usync.get_user_devices", level = "debug", skip_all, fields(users = jids.len()), err(Debug)))]
pub(crate) async fn get_user_devices(&self, jids: &[Jid]) -> Result<Vec<Jid>, anyhow::Error> {
let mut jids_to_fetch: HashSet<Jid> = HashSet::with_capacity(jids.len());
Expand Down Expand Up @@ -179,15 +202,16 @@ impl Client {
let mut devices: Vec<wacore::store::traits::DeviceInfo> = user_list
.devices
.iter()
.map(|d| wacore::store::traits::DeviceInfo {
device_id: d.device as u32,
.map(|d| {
// Server-returned key_index takes priority over cached
key_index: d.key_index.or_else(|| {
let key_index = d.key_index.or_else(|| {
existing_key_indices
.get(&(d.device as u32))
.copied()
.flatten()
}),
});
wacore::store::traits::DeviceInfo::new(d.device as u32, key_index)
.with_hosting(d.is_hosted)
})
.collect();

Expand All @@ -199,9 +223,7 @@ impl Client {
// Convert filtered DeviceInfo list back to JIDs for return
let user_jid = &user_list.user;
for d in &devices {
let mut jid = user_jid.clone();
jid.device = d.device_id as u16;
fetched_devices.push(jid);
fetched_devices.push(user_jid.with_device_hosting(d.device_id as u16, d.is_hosted));
}

// An empty device list is never valid — WA Web always keeps the primary
Expand Down Expand Up @@ -337,16 +359,7 @@ mod tests {
// Insert a device record into the registry (simulates prior usync/notification)
let record = DeviceListRecord {
user: "1234567890".into(),
devices: vec![
DeviceInfo {
device_id: 0,
key_index: None,
},
DeviceInfo {
device_id: 3,
key_index: Some(10),
},
],
devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(3, Some(10))],
timestamp: wacore::time::now_secs(),
phash: None,
raw_id: None,
Expand All @@ -369,16 +382,7 @@ mod tests {

let record = DeviceListRecord {
user: "100000012345678".into(),
devices: vec![
DeviceInfo {
device_id: 0,
key_index: None,
},
DeviceInfo {
device_id: 39,
key_index: Some(25),
},
],
devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(39, Some(25))],
timestamp: wacore::time::now_secs(),
phash: None,
raw_id: None,
Expand All @@ -401,10 +405,7 @@ mod tests {
// Insert into backend DB via update_device_list
let record = DeviceListRecord {
user: "9876543210".into(),
devices: vec![DeviceInfo {
device_id: 5,
key_index: None,
}],
devices: vec![DeviceInfo::new(5, None)],
timestamp: wacore::time::now_secs(),
phash: None,
raw_id: None,
Expand Down Expand Up @@ -481,16 +482,7 @@ mod tests {
client
.update_device_list(DeviceListRecord {
user: "2222222222".into(),
devices: vec![
DeviceInfo {
device_id: 0,
key_index: None,
},
DeviceInfo {
device_id: 7,
key_index: None,
},
],
devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(7, None)],
timestamp: wacore::time::now_secs(),
phash: Some("2:oldB".to_string()),
raw_id: None,
Expand All @@ -502,10 +494,7 @@ mod tests {
let response = DeviceListResponse {
device_lists: vec![UserDeviceList {
user: "1111111111@s.whatsapp.net".parse().unwrap(),
devices: vec![UsyncDevice {
device: 0,
key_index: None,
}],
devices: vec![UsyncDevice::new(0, None)],
phash: Some("2:a".to_string()),
key_index_bytes: None,
}],
Expand All @@ -531,6 +520,44 @@ mod tests {
);
}

#[tokio::test]
async fn process_response_preserves_hosted_device_addressing() {
use wacore::iq::usync::DeviceListResponse;
use wacore::usync::{UserDeviceList, UsyncDevice};

let client = create_test_client().await;
let user = Jid::pn("1111111111");
let response = DeviceListResponse {
device_lists: vec![UserDeviceList {
user: user.clone(),
devices: vec![
UsyncDevice::new(0, None),
UsyncDevice::new(7, Some(3)).with_hosting(true),
],
phash: Some("2:hosted".to_string()),
key_index_bytes: None,
}],
lid_mappings: Vec::new(),
};

let fetched = client.process_device_list_response(&response).await;
assert!(
fetched
.iter()
.any(|jid| jid.device == 7 && jid.server == wacore_binary::Server::Hosted)
);

let cached = client
.get_devices_from_registry(&user)
.await
.expect("processed devices should be cached");
assert!(
cached
.iter()
.any(|jid| jid.device == 7 && jid.server == wacore_binary::Server::Hosted)
);
}

/// A usync that returns an empty device list for a user is transient or
/// corrupt (WA Web always keeps device 0). `process_device_list_response`
/// must not persist it: a good cached record stays intact instead of being
Expand All @@ -545,16 +572,7 @@ mod tests {
client
.update_device_list(DeviceListRecord {
user: "3333333333".into(),
devices: vec![
DeviceInfo {
device_id: 0,
key_index: None,
},
DeviceInfo {
device_id: 4,
key_index: None,
},
],
devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(4, None)],
timestamp: wacore::time::now_secs(),
phash: Some("3:old".to_string()),
raw_id: None,
Expand Down
27 changes: 3 additions & 24 deletions storages/sqlite-storage/src/sqlite_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4033,16 +4033,7 @@ mod tests {

let record = DeviceListRecord {
user: "1234567890".to_string(),
devices: vec![
DeviceInfo {
device_id: 0,
key_index: None,
},
DeviceInfo {
device_id: 1,
key_index: Some(42),
},
],
devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(1, Some(42))],
timestamp: 1234567890,
phash: Some("2:abcdef".to_string()),
raw_id: None,
Expand All @@ -4069,10 +4060,7 @@ mod tests {

let record1 = DeviceListRecord {
user: "1234567890".to_string(),
devices: vec![DeviceInfo {
device_id: 0,
key_index: None,
}],
devices: vec![DeviceInfo::new(0, None)],
timestamp: 1000,
phash: Some("2:old".to_string()),
raw_id: None,
Expand All @@ -4084,16 +4072,7 @@ mod tests {

let record2 = DeviceListRecord {
user: "1234567890".to_string(),
devices: vec![
DeviceInfo {
device_id: 0,
key_index: None,
},
DeviceInfo {
device_id: 2,
key_index: None,
},
],
devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(2, None)],
timestamp: 2000,
phash: Some("2:new".to_string()),
raw_id: None,
Expand Down
38 changes: 38 additions & 0 deletions wacore/binary/src/jid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,23 @@ impl Jid {
}
}

/// Construct a device JID and select the hosted variant of PN/LID when
/// indicated by a device-list entry.
///
/// Non-user namespaces are left unchanged because they have no hosted
/// counterpart on the wire.
pub fn with_device_hosting(&self, device_id: u16, is_hosted: bool) -> Self {
let mut jid = self.with_device(device_id);
jid.server = match (jid.server, is_hosted) {
(Server::Pn | Server::Hosted, true) => Server::Hosted,
(Server::Pn | Server::Hosted, false) => Server::Pn,
(Server::Lid | Server::HostedLid, true) => Server::HostedLid,
(Server::Lid | Server::HostedLid, false) => Server::Lid,
(server, _) => server,
};
jid
}

pub fn to_non_ad(&self) -> Self {
Self {
user: self.user.clone(),
Expand Down Expand Up @@ -1594,6 +1611,27 @@ mod tests {
assert!(jid.is_ad());
}

#[test]
fn with_device_hosting_preserves_addressing_family() {
let hosted_pn = Jid::pn("1234567890").with_device_hosting(7, true);
assert_eq!(hosted_pn.server, Server::Hosted);
assert_eq!(hosted_pn.device, 7);

let hosted_lid = Jid::lid("100000012345678").with_device_hosting(8, true);
assert_eq!(hosted_lid.server, Server::HostedLid);
assert_eq!(hosted_lid.device, 8);

let regular = Jid::pn("1234567890").with_device_hosting(9, false);
assert_eq!(regular.server, Server::Pn);

let regular_from_hosted =
Jid::new("1234567890", Server::Hosted).with_device_hosting(9, false);
assert_eq!(regular_from_hosted.server, Server::Pn);

let group = Jid::group("123-456").with_device_hosting(10, true);
assert_eq!(group.server, Server::Group);
}

#[test]
fn test_status_broadcast_jid() {
let jid = Jid::status_broadcast();
Expand Down
Loading
Loading