use crate::client::Client;
use crate::lid_pn_cache::LearningSource;
use crate::types::events::Event;
use log::{debug, info, warn};
use std::sync::Arc;
use wacore::stanza::devices::DeviceNotification;
use wacore::store::traits::{DeviceInfo, DeviceListRecord};
use wacore::types::events::{DeviceListUpdate, DeviceNotificationInfo};
use wacore_binary::NodeRef;
use wacore_binary::{Jid, JidExt};
pub(crate) async fn handle_encrypt_notification(client: &Arc<Client>, nr: &NodeRef<'_>) {
if nr.get_optional_child("identity").is_some() {
handle_identity_change(client, nr).await;
} else if nr
.get_attr("from")
.is_some_and(|v| v == wacore_binary::SERVER_JID)
{
let first_child_tag = nr
.children()
.and_then(|c| c.first().map(|n| n.tag.as_ref()));
match first_child_tag {
Some("count") => handle_prekey_low(client).await,
Some("digest") => handle_digest_key(client),
other => warn!("Unhandled encrypt notification child: {:?}", other),
}
}
}
pub(crate) async fn handle_account_sync_notification(client: &Arc<Client>, nr: &NodeRef<'_>) {
if let Some(new_push_name) = nr.attrs().optional_string("pushname") {
client
.clone()
.update_push_name_and_notify(new_push_name.to_string())
.await;
}
if let Some(devices_node) = nr.get_optional_child_by_tag(&["devices"]) {
handle_account_sync_devices(client, nr, devices_node).await;
}
}
pub(crate) async fn handle_prekey_low(client: &Arc<Client>) {
client
.persistence_manager
.modify_device(|d| d.server_has_prekeys = false)
.await;
let client_clone = client.clone();
client
.runtime
.spawn(Box::pin(async move {
client_clone.wait_for_offline_delivery_end().await;
if !client_clone
.is_logged_in
.load(std::sync::atomic::Ordering::Relaxed)
{
debug!("Pre-key upload skipped: disconnected during offline delivery wait");
return;
}
let _guard = client_clone.prekey_upload_lock.lock().await;
if client_clone
.persistence_manager
.get_device_snapshot()
.server_has_prekeys
{
debug!("Pre-key upload already completed by another task, skipping");
return;
}
if let Err(e) = client_clone.upload_pre_keys_with_retry(true).await {
warn!(
"Failed to upload pre-keys after prekey_low notification: {:?}",
e
);
}
}))
.detach();
}
pub(crate) fn handle_digest_key(client: &Arc<Client>) {
let client_clone = client.clone();
client
.runtime
.spawn(Box::pin(async move {
if let Err(e) = client_clone.validate_digest_key().await {
warn!("Digest key validation failed: {:?}", e);
}
}))
.detach();
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(name = "wa.notif.identity_change", level = "debug", skip_all)
)]
pub(crate) async fn handle_identity_change(client: &Arc<Client>, node: &NodeRef<'_>) {
let from_jid = crate::require_from_jid!(node, "Identity change notification");
if from_jid.device != 0 {
debug!(
"Ignoring identity change from companion device {}",
from_jid.observe()
);
return;
}
let device_snapshot = client.persistence_manager.get_device_snapshot();
let is_me = device_snapshot
.pn
.as_ref()
.is_some_and(|pn| pn.user == from_jid.user)
|| device_snapshot
.lid
.as_ref()
.is_some_and(|lid| lid.user == from_jid.user);
if is_me {
debug!("Ignoring self-primary identity change");
return;
}
use wacore::libsignal::store::sender_key_name::SenderKeyName;
use wacore::types::jid::JidExt;
if let Some(record) = client.load_device_record(&from_jid.user).await {
client
.clear_device_record(&from_jid.user, from_jid.server.as_str(), &record)
.await;
}
client.invalidate_device_cache(&from_jid.user).await;
let resolved = client.resolve_encryption_jid(&from_jid).await;
let stanza_lid = node.attrs().optional_jid("lid");
let backend = client.persistence_manager.backend();
let mut reset_addrs = vec![resolved.to_protocol_address()];
for candidate in [Some(from_jid.clone()), stanza_lid.clone()]
.into_iter()
.flatten()
{
let cand_addr = candidate.to_protocol_address();
if !reset_addrs.contains(&cand_addr) {
reset_addrs.push(cand_addr);
}
}
let mut had_prior_identity = false;
for cand in &reset_addrs {
match client
.signal_cache
.get_identity(cand, backend.as_ref())
.await
{
Ok(Some(_)) => {
had_prior_identity = true;
break;
}
Ok(None) => {}
Err(e) => {
warn!(
"Identity change: failed reading stored identity for {}: {e}; proceeding with reset",
wacore::types::jid::observe_protocol_address(cand)
);
had_prior_identity = true;
break;
}
}
}
if !had_prior_identity {
info!(
"Identity change for {} (had_prior_identity=false): device record cleared, skipping session reset",
from_jid.user
);
return;
}
wacore::telemetry::identity_change();
info!(
"Identity change for {} (had_prior_identity=true): resetting session",
from_jid.user
);
{
for cand in &reset_addrs {
let lock = client.session_lock_for(cand.as_str()).await;
let _guard = lock.lock().await;
client.signal_cache.delete_session(cand).await;
client.signal_cache.delete_identity(cand).await;
}
let status_jid = Jid::status_broadcast();
let distribution_guard = client.group_distribution_lock(&status_jid).await;
let status_group = "status@broadcast";
for own_jid in device_snapshot.pn.iter().chain(device_snapshot.lid.iter()) {
let sk_name =
SenderKeyName::from_parts(status_group, own_jid.to_protocol_address().as_str());
client
.signal_cache
.delete_sender_key(sk_name.cache_key())
.await;
}
drop(distribution_guard);
client
.flush_signal_cache_batch_safe_logged("identity change", None)
.await;
}
if !from_jid.is_bot() && !from_jid.is_status_broadcast() {
let tc_client = client.clone();
let tc_jid = from_jid.clone();
client
.runtime
.spawn(Box::pin(async move {
tc_client
.reissue_tc_token_after_identity_change(&tc_jid)
.await;
}))
.detach();
}
client.core.event_bus.dispatch(Event::IdentityChange(
crate::types::events::IdentityChange::builder()
.user(from_jid.clone())
.maybe_lid_user(stanza_lid)
.implicit(false)
.build(),
));
let arrived_during_resume = node.attrs().optional_string("offline").is_some()
&& !client
.offline_sync_completed
.load(std::sync::atomic::Ordering::Relaxed);
if arrived_during_resume {
debug!(
"Identity change for {} arrived during offline resume; deferring session re-establishment to next send",
from_jid.user
);
} else {
let client_clone = client.clone();
let session_jid = from_jid;
client
.runtime
.spawn(Box::pin(async move {
if let Err(e) = client_clone.ensure_e2e_sessions(&[session_jid]).await {
warn!("Identity change: failed to re-establish session: {e}");
}
}))
.detach();
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.notif.local_identity_change", level = "debug", skip_all, fields(sender = %sender.observe())))]
pub(crate) async fn handle_local_identity_change(client: &Arc<Client>, sender: Jid) {
if sender.device != 0 {
return;
}
let device_snapshot = client.persistence_manager.get_device_snapshot();
let is_me = device_snapshot
.pn
.as_ref()
.is_some_and(|pn| pn.user == sender.user)
|| device_snapshot
.lid
.as_ref()
.is_some_and(|lid| lid.user == sender.user);
if is_me {
return;
}
info!(
"Local identity change detected for {}: clearing device record",
sender.user
);
if let Some(record) = client.load_device_record(&sender.user).await {
client
.clear_device_record(&sender.user, sender.server.as_str(), &record)
.await;
}
client.invalidate_device_cache(&sender.user).await;
if !sender.is_bot() && !sender.is_status_broadcast() {
client.reissue_tc_token_after_identity_change(&sender).await;
}
client.core.event_bus.dispatch(Event::IdentityChange(
crate::types::events::IdentityChange::builder()
.user(sender)
.maybe_lid_user(None)
.implicit(true)
.build(),
));
}
async fn sync_hashed_contact(client: &Arc<Client>, wire_hash: Option<&str>) {
let Some(hash) = wire_hash.and_then(wacore::crypto::parse_contact_notification_hash) else {
debug!("Device update with unusable contact hash {wire_hash:?}");
return;
};
let Some(lid) = client.lid_pn_cache.lid_for_contact_hash(hash).await else {
debug!("Device update for an unknown contact hash {wire_hash:?}");
return;
};
let is_offline = client
.offline_sync_metrics
.active
.load(std::sync::atomic::Ordering::Acquire);
let jid = Jid::new(lid.as_ref(), wacore_binary::Server::Lid);
client.schedule_unknown_device_sync(jid, is_offline).await;
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(name = "wa.notif.devices", level = "debug", skip_all)
)]
pub(crate) async fn handle_devices_notification(client: &Arc<Client>, node: &NodeRef<'_>) {
let notification = match DeviceNotification::try_parse(node) {
Ok(n) => n,
Err(e) => {
warn!("Failed to parse device notification: {e}");
return;
}
};
if let Some((lid, pn)) = notification.lid_pn_mapping()
&& let Err(e) = client
.add_lid_pn_mapping(lid, pn, LearningSource::DeviceNotification)
.await
{
warn!("Failed to add LID-PN mapping from device notification: {e}");
}
let op = ¬ification.operation;
debug!(
"Device notification: user={}, type={:?}, devices={:?}",
notification.user(),
op.operation_type,
op.device_ids()
);
match op.operation_type {
wacore::stanza::devices::DeviceNotificationType::Add => {
for device in &op.devices {
client
.patch_device_add(notification.user(), device, op.key_index.as_ref())
.await;
}
}
wacore::stanza::devices::DeviceNotificationType::Remove => {
for device in &op.devices {
client
.patch_device_remove(notification.user(), device.device_id())
.await;
}
}
wacore::stanza::devices::DeviceNotificationType::Update => {
if op.devices.is_empty() {
sync_hashed_contact(client, op.contact_hash.as_deref()).await;
} else {
for device in &op.devices {
client
.patch_device_update(notification.user(), device)
.await;
}
}
}
}
let event = Event::DeviceListUpdate(
DeviceListUpdate::builder()
.user(notification.from.clone())
.maybe_lid_user(notification.lid_user.clone())
.update_type(op.operation_type.into())
.devices(
op.devices
.iter()
.map(|d| {
DeviceNotificationInfo::builder()
.device_id(d.device_id())
.maybe_key_index(d.key_index)
.build()
})
.collect(),
)
.maybe_key_index(op.key_index.clone())
.maybe_contact_hash(op.contact_hash.clone())
.build(),
);
client.core.event_bus.dispatch(event);
}
pub(crate) struct AccountSyncDevice {
pub(crate) jid: Jid,
pub(crate) key_index: Option<u32>,
}
pub(crate) fn parse_account_sync_device_list(devices_node: &NodeRef<'_>) -> Vec<AccountSyncDevice> {
let Some(children) = devices_node.children() else {
return Vec::new();
};
children
.iter()
.filter(|n| n.tag == "device")
.filter_map(|n| {
let jid = n.attrs().optional_jid("jid")?;
let key_index = n.attrs().optional_u64("key-index").map(|v| v as u32);
Some(AccountSyncDevice { jid, key_index })
})
.collect()
}
pub(crate) async fn handle_account_sync_devices(
client: &Arc<Client>,
node: &NodeRef<'_>,
devices_node: &NodeRef<'_>,
) {
let from_jid = crate::require_from_jid!(
node,
target: "Client/AccountSync",
"account_sync devices"
);
let device_snapshot = client.persistence_manager.get_device_snapshot();
let own_pn = device_snapshot.pn.as_ref();
let own_lid = device_snapshot.lid.as_ref();
let is_own_account = own_pn.is_some_and(|pn| pn.is_same_user_as(&from_jid))
|| own_lid.is_some_and(|lid| lid.is_same_user_as(&from_jid));
if !is_own_account {
warn!(
target: "Client/AccountSync",
"Received account_sync devices for non-self user: {} (our PN: {:?}, LID: {:?})",
from_jid.observe(),
own_pn.map(|j| j.user.as_str()),
own_lid.map(|j| j.user.as_str())
);
return;
}
let devices = parse_account_sync_device_list(devices_node);
if devices.is_empty() {
debug!(target: "Client/AccountSync", "account_sync devices list is empty");
return;
}
let dhash = devices_node
.attrs()
.optional_string("dhash")
.map(|s| s.into_owned());
let timestamp = node
.attrs()
.optional_u64("t")
.map(|v| v as i64)
.unwrap_or_else(wacore::time::now_secs);
let existing_raw_id = client
.load_device_record(&from_jid.user)
.await
.and_then(|r| r.raw_id);
let device_list = DeviceListRecord {
user: from_jid.user.to_string(),
devices: devices
.iter()
.map(|d| {
DeviceInfo::new(d.jid.device as u32, d.key_index)
.with_hosting(JidExt::is_hosted(&d.jid))
})
.collect(),
timestamp,
phash: dhash,
raw_id: existing_raw_id,
};
if let Err(e) = client.update_device_list(device_list).await {
warn!(
target: "Client/AccountSync",
"Failed to update device list from account_sync: {}",
e
);
return;
}
info!(
target: "Client/AccountSync",
"Updated own device list from account_sync: {} devices (user: {})",
devices.len(),
from_jid.user
);
for device in &devices {
debug!(
target: "Client/AccountSync",
" Device: {} (key-index: {:?})",
device.jid.observe(),
device.key_index
);
}
}