mod accessors;
mod adapters;
mod app_state;
pub(crate) use app_state::SyncSettles;
mod builder;
mod context_impl;
mod device_registry;
pub(crate) mod device_topology;
#[cfg(feature = "client-lifecycle")]
mod extension_lifecycle;
mod iq_ops;
mod lid_pn;
mod lifecycle;
mod messaging;
mod node_io;
pub(crate) mod offline_resume;
mod sender_keys;
mod sessions;
mod voip;
use builder::{ClientAssembly, ClientExtensions};
pub use builder::{ClientBuild, ClientBuilder, ClientBuilderError};
#[cfg(feature = "client-lifecycle")]
use extension_lifecycle::LifecycleRegistration;
#[cfg(feature = "client-lifecycle")]
#[cfg_attr(docsrs, doc(cfg(feature = "client-lifecycle")))]
pub use extension_lifecycle::{ClientLifecycle, ConnectionScope, ConnectionScopeState};
pub use voip::{CallError, Voip};
use crate::cache::Cache;
use crate::cache_store::TypedCache;
use crate::handshake;
use crate::lid_pn_cache::LidPnCache;
use crate::pair;
use anyhow::Result;
use futures::FutureExt;
#[cfg(test)]
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64;
use wacore::xml::{DisplayableNode, DisplayableNodeRef};
use wacore_binary::JidExt;
use wacore_binary::Node;
use wacore_binary::builder::NodeBuilder;
#[cfg(test)]
use wacore_binary::{Attrs, NodeValue};
use crate::appstate_sync::AppStateProcessor;
use crate::handlers::chatstate::ChatStateEvent;
use crate::jid_utils::server_jid;
use crate::store::{commands::DeviceCommand, persistence_manager::PersistenceManager};
use crate::types::enc_handler::EncHandler;
use crate::types::events::{ConnectFailureReason, Event};
use log::{debug, error, info, trace, warn};
use rand::{Rng, RngExt};
use scopeguard;
use wacore_binary::Jid;
use portable_atomic::{AtomicI64, AtomicU64};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
#[must_use = "dropping the lease immediately releases raw-node forwarding"]
pub struct RawNodeLease {
client: std::sync::Weak<Client>,
}
impl Drop for RawNodeLease {
fn drop(&mut self) {
let Some(client) = self.client.upgrade() else {
return;
};
let previous = client.raw_node_forwarding.fetch_sub(1, Ordering::Relaxed);
debug_assert!(previous > 0, "raw-node forwarding lease underflow");
}
}
#[derive(Debug, Clone)]
pub struct NodeFilter {
tag: String,
attrs: Vec<(String, String)>,
}
impl NodeFilter {
pub fn tag(tag: impl Into<String>) -> Self {
Self {
tag: tag.into(),
attrs: Vec::new(),
}
}
pub fn attr(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attrs.push((key.into(), value.into()));
self
}
pub fn from_jid(self, jid: &Jid) -> Self {
self.attr("from", jid.to_string())
}
fn matches(&self, node: &wacore_binary::NodeRef<'_>) -> bool {
node.tag == self.tag.as_str()
&& self.attrs.iter().all(|(k, v)| {
node.get_attr(k.as_str())
.is_some_and(|attr| attr == v.as_str())
})
}
}
struct NodeWaiter {
filter: NodeFilter,
tx: futures::channel::oneshot::Sender<Arc<wacore_binary::OwnedNodeRef>>,
}
struct SentNodeWaiter {
filter: NodeFilter,
tx: futures::channel::oneshot::Sender<Arc<Node>>,
}
fn resolve_waiters(
waiters_mutex: &std::sync::Mutex<Vec<NodeWaiter>>,
counter: &AtomicUsize,
node: &Arc<wacore_binary::OwnedNodeRef>,
) {
let nr = node.get();
let mut waiters = waiters_mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let mut i = 0;
while i < waiters.len() {
if waiters[i].tx.is_canceled() {
waiters.swap_remove(i);
counter.fetch_sub(1, Ordering::Release);
} else if waiters[i].filter.matches(nr) {
let w = waiters.swap_remove(i);
counter.fetch_sub(1, Ordering::Release);
let _ = w.tx.send(Arc::clone(node));
} else {
i += 1;
}
}
}
use async_lock::Mutex;
use async_lock::RwLock;
use std::time::Duration;
use thiserror::Error;
use wacore::appstate::patch_decode::WAPatchName;
use wacore::client::context::GroupInfo;
type GroupCache = TypedCache<Jid, Arc<GroupInfo>>;
pub(crate) type SkdmWarmMemoEntry = (
std::sync::Weak<wacore::send::ResolvedGroupDevices>,
std::sync::Weak<crate::sender_key_device_cache::SenderKeyDeviceMap>,
u64,
Jid,
Vec<Jid>,
);
use wacore::runtime::timeout as rt_timeout;
use waproto::whatsapp as wa;
use crate::cache_config::CacheConfig;
use crate::socket::{NoiseSocket, SocketError, error::EncryptSendError};
use crate::sync_task::MajorSyncTask;
use wacore::runtime::Runtime;
type ChatStateHandler = Arc<dyn Fn(ChatStateEvent) + Send + Sync>;
#[derive(Clone)]
pub(crate) struct ChatLane {
pub enqueue_lock: Arc<Mutex<()>>,
pub queue_tx: async_channel::Sender<QueuedChatMessage>,
}
impl ChatLane {
pub(crate) fn try_enqueue(
&self,
node: Arc<wacore_binary::OwnedNodeRef>,
) -> Result<(), async_channel::TrySendError<QueuedChatMessage>> {
self.queue_tx.try_send(QueuedChatMessage {
node,
lane_liveness: Arc::clone(&self.enqueue_lock),
})
}
}
pub(crate) struct QueuedChatMessage {
pub node: Arc<wacore_binary::OwnedNodeRef>,
pub lane_liveness: Arc<Mutex<()>>,
}
const APP_STATE_RETRY_MAX_ATTEMPTS: u32 = 6;
const TRANSPORT_CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
pub use wacore::stats::{
AllocSnapshot, CollectionStats, HttpResourceReport, StatsSnapshot, StorageResourceReport,
TransportResourceReport,
};
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct MemoryReport {
pub group_cache: CollectionStats,
pub device_registry_cache: CollectionStats,
pub lid_pn_lid_entries: CollectionStats,
pub lid_pn_pn_entries: CollectionStats,
pub recent_messages: CollectionStats,
pub sender_key_device_cache: CollectionStats,
pub group_devices_memo: CollectionStats,
pub dm_devices_memo: CollectionStats,
pub message_retry_counts: u64,
pub undecryptable_dispatched: u64,
pub pdo_pending_requests: u64,
pub pdo_requested: u64,
pub history_sync_tasks: CollectionStats,
pub history_sync_tasks_peak: u64,
pub history_sync_payload_bytes_peak: u64,
pub session_locks: u64,
pub chat_lanes: u64,
pub group_distribution_locks: u64,
pub group_distribution_lock_evictions: u64,
pub group_distribution_lock_eviction_blocks: u64,
pub resend_rate_limiter_chats: u64,
pub transport_ack_queue: usize,
pub delivery_receipt_queue: usize,
pub response_waiters: usize,
pub node_waiters: usize,
pub pending_retries: usize,
pub presence_subscriptions: usize,
pub app_state_key_requests: usize,
pub app_state_syncing: usize,
pub signal_sessions: CollectionStats,
pub signal_identities: CollectionStats,
pub signal_sender_keys: CollectionStats,
#[cfg(feature = "voip-runtime")]
pub pending_call_link_updates: CollectionStats,
#[cfg(feature = "voip-runtime")]
pub active_calls: CollectionStats,
#[cfg(feature = "plugins")]
pub plugins: u64,
#[cfg(feature = "plugins")]
pub plugin_install_tasks: u64,
#[cfg(feature = "plugins")]
pub plugin_connection_tasks: u64,
#[cfg(feature = "plugins")]
pub plugin_connection_generations: u64,
#[cfg(feature = "plugins")]
pub plugin_core_event_subscriptions: u64,
#[cfg(feature = "plugins")]
pub plugin_event_endpoints: u64,
#[cfg(feature = "plugins")]
pub plugin_event_endpoint_capacity: u64,
#[cfg(feature = "plugins")]
pub plugin_event_queue: CollectionStats,
pub chatstate_handlers: usize,
pub custom_enc_handlers: usize,
}
impl MemoryReport {
fn collections(&self) -> [(&'static str, &CollectionStats); 12] {
[
("group_cache:", &self.group_cache),
("device_registry_cache:", &self.device_registry_cache),
("lid_pn (lid):", &self.lid_pn_lid_entries),
("lid_pn (pn):", &self.lid_pn_pn_entries),
("recent_messages:", &self.recent_messages),
("sk_device_cache:", &self.sender_key_device_cache),
("group_devices_memo:", &self.group_devices_memo),
("dm_devices_memo:", &self.dm_devices_memo),
("signal_sessions:", &self.signal_sessions),
("signal_identities:", &self.signal_identities),
("signal_sender_keys:", &self.signal_sender_keys),
("history_sync_tasks:", &self.history_sync_tasks),
]
}
pub fn total_estimated_bytes(&self) -> u64 {
let total: u64 = self.collections().iter().map(|(_, c)| c.bytes).sum();
#[cfg(feature = "voip-runtime")]
let total = total
.saturating_add(self.pending_call_link_updates.bytes)
.saturating_add(self.active_calls.bytes);
#[cfg(feature = "plugins")]
let total = total.saturating_add(self.plugin_event_queue.bytes);
total
}
}
impl std::fmt::Display for MemoryReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn line(
f: &mut std::fmt::Formatter<'_>,
name: &str,
c: &CollectionStats,
) -> std::fmt::Result {
writeln!(f, " {name:<22} {:>7} entries {:>10} B", c.entries, c.bytes)
}
const TTL_BOUNDED: usize = 8;
const SIGNAL_CACHES: usize = 3;
let collections = self.collections();
writeln!(f, "=== Memory Report ===")?;
writeln!(f, "--- TTL-bounded caches ---")?;
for (name, c) in &collections[..TTL_BOUNDED] {
line(f, name, c)?;
}
writeln!(f, " message_retry_counts: {}", self.message_retry_counts)?;
writeln!(
f,
" undec_dispatched: {}",
self.undecryptable_dispatched
)?;
writeln!(f, " pdo_pending_requests: {}", self.pdo_pending_requests)?;
writeln!(f, " pdo_requested: {}", self.pdo_requested)?;
writeln!(f, "--- Capacity-only caches ---")?;
writeln!(f, " session_locks: {}", self.session_locks)?;
writeln!(f, " chat_lanes: {}", self.chat_lanes)?;
writeln!(
f,
" group_dist_locks: {} (evicted: {}, blocked: {})",
self.group_distribution_locks,
self.group_distribution_lock_evictions,
self.group_distribution_lock_eviction_blocks
)?;
writeln!(
f,
" resend_rl_chats: {}",
self.resend_rate_limiter_chats
)?;
writeln!(f, "--- Unbounded collections ---")?;
writeln!(f, " transport_ack_queue: {}", self.transport_ack_queue)?;
writeln!(
f,
" delivery_receipt_queue: {}",
self.delivery_receipt_queue
)?;
writeln!(f, " response_waiters: {}", self.response_waiters)?;
writeln!(f, " node_waiters: {}", self.node_waiters)?;
writeln!(f, " pending_retries: {}", self.pending_retries)?;
writeln!(
f,
" presence_subscriptions: {}",
self.presence_subscriptions
)?;
writeln!(
f,
" app_state_key_requests: {}",
self.app_state_key_requests
)?;
writeln!(f, " app_state_syncing: {}", self.app_state_syncing)?;
writeln!(f, "--- Signal store caches ---")?;
for (name, c) in &collections[TTL_BOUNDED..TTL_BOUNDED + SIGNAL_CACHES] {
line(f, name, c)?;
}
#[cfg(feature = "voip-runtime")]
{
writeln!(f, "--- VoIP state ---")?;
line(f, "pending_link_updates:", &self.pending_call_link_updates)?;
line(f, "active_calls:", &self.active_calls)?;
}
writeln!(f, "--- In-flight history sync ---")?;
line(
f,
collections[TTL_BOUNDED + SIGNAL_CACHES].0,
&self.history_sync_tasks,
)?;
writeln!(
f,
" peak tasks: {}",
self.history_sync_tasks_peak
)?;
writeln!(
f,
" peak payload storage: {} B",
self.history_sync_payload_bytes_peak
)?;
#[cfg(feature = "plugins")]
{
writeln!(f, "--- Plugins ---")?;
writeln!(f, " installed: {}", self.plugins)?;
writeln!(f, " install tasks: {}", self.plugin_install_tasks)?;
writeln!(
f,
" connection tasks: {} (generations: {})",
self.plugin_connection_tasks, self.plugin_connection_generations
)?;
writeln!(
f,
" core subscriptions: {}",
self.plugin_core_event_subscriptions
)?;
writeln!(
f,
" event endpoints: {} (capacity: {})",
self.plugin_event_endpoints, self.plugin_event_endpoint_capacity
)?;
line(f, "event_queue:", &self.plugin_event_queue)?;
}
writeln!(f, "--- Misc ---")?;
writeln!(f, " chatstate_handlers: {}", self.chatstate_handlers)?;
writeln!(f, " custom_enc_handlers: {}", self.custom_enc_handlers)?;
writeln!(
f,
" total estimated: {} B",
self.total_estimated_bytes()
)?;
Ok(())
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct ResourceReport {
pub client: MemoryReport,
pub storage: StorageResourceReport,
pub transport: Option<TransportResourceReport>,
pub http: Option<HttpResourceReport>,
pub alloc: Option<AllocSnapshot>,
}
impl ResourceReport {
pub fn total_estimated_bytes(&self) -> u64 {
self.client
.total_estimated_bytes()
.saturating_add(self.storage.total_bytes())
.saturating_add(self.transport.map_or(0, |t| t.total_bytes()))
.saturating_add(self.http.map_or(0, |h| h.total_bytes()))
}
}
impl std::fmt::Display for ResourceReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "=== Resource Report ===")?;
writeln!(
f,
" client collections: {:>10} B",
self.client.total_estimated_bytes()
)?;
writeln!(
f,
" storage backend: {:>10} B (pages: {:?})",
self.storage.total_bytes(),
self.storage.pages
)?;
writeln!(
f,
" transport: {:>10} B",
self.transport.map_or(0, |t| t.total_bytes())
)?;
writeln!(
f,
" http client: {:>10} B",
self.http.map_or(0, |h| h.total_bytes())
)?;
if let Some(alloc) = self.alloc {
writeln!(
f,
" alloc churn: {:>10} B allocated / {:>10} B freed ({} allocs)",
alloc.allocated_bytes, alloc.freed_bytes, alloc.allocations
)?;
}
writeln!(
f,
" total retained (lower bound): {} B",
self.total_estimated_bytes()
)?;
Ok(())
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ClientError {
#[error("client is not connected")]
NotConnected,
#[error("socket error: {0}")]
Socket(#[from] SocketError),
#[error("encrypt/send error: {0}")]
EncryptSend(#[from] EncryptSendError),
#[error("client is not logged in")]
NotLoggedIn,
#[error("IQ request failed: {0}")]
Iq(#[from] crate::request::IqError),
#[error("{0}")]
Internal(#[from] anyhow::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConnectStage {
VersionFetch,
Transport,
Socket,
Ready,
}
impl std::fmt::Display for ConnectStage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let stage = match self {
ConnectStage::VersionFetch => "version fetch",
ConnectStage::Transport => "transport connect",
ConnectStage::Socket => "socket wait",
ConnectStage::Ready => "connection wait",
};
f.write_str(stage)
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ConnectError {
#[error("client is already connected")]
AlreadyConnected,
#[error("client construction did not activate")]
NotActivated,
#[error("{stage} timed out after {timeout:?}")]
Timeout {
stage: ConnectStage,
timeout: Duration,
},
#[error("failed to resolve app version")]
Version(#[source] anyhow::Error),
#[error("failed to open transport")]
Transport(#[source] anyhow::Error),
#[error("{0}")]
Handshake(#[from] handshake::HandshakeError),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum SignalMaintenanceError {
#[error("corrupt signed pre-key material: {0}")]
CorruptKey(String),
#[error("signal storage failure: {0}")]
Storage(#[source] anyhow::Error),
#[error("IQ request failed: {0}")]
Iq(#[from] crate::request::IqError),
#[error("{0}")]
Signal(#[from] wacore::libsignal::protocol::SignalProtocolError),
#[error(
"inbound drain batch commit failed; Signal cache left unflushed so the server redelivers"
)]
DrainCommitFailed,
#[error("client dropping while inbound drain is active; skipping Signal flush")]
DrainShuttingDown,
}
impl ConnectError {
pub fn is_timeout(&self) -> bool {
match self {
ConnectError::Timeout { .. } => true,
ConnectError::Handshake(handshake) => handshake.is_timeout(),
ConnectError::AlreadyConnected
| ConnectError::NotActivated
| ConnectError::Version(_)
| ConnectError::Transport(_) => false,
}
}
}
impl ClientError {
pub fn is_transport_unavailable(&self) -> bool {
match self {
ClientError::NotConnected => true,
ClientError::EncryptSend(e) => e.is_transport_unavailable(),
ClientError::Iq(e) => e.is_transport_unavailable(),
_ => false,
}
}
}
use wacore::types::message::ChatMessageId;
#[derive(Debug)]
pub(crate) struct OfflineSyncMetrics {
pub active: AtomicBool,
pub total_messages: AtomicUsize,
pub processed_messages: AtomicUsize,
pub start_time: std::sync::Mutex<Option<wacore::time::Instant>>,
}
type ResponseWaiterSender = futures::channel::oneshot::Sender<Arc<wacore_binary::OwnedNodeRef>>;
pub(crate) enum ResponseWaiter {
Iq(ResponseWaiterSender),
Phash(PhashWaiter),
}
pub(crate) struct PhashWaiter {
pub(crate) expected: wacore_binary::CompactString,
pub(crate) jid: Jid,
pub(crate) invalidate_group_cache: bool,
pub(crate) registered_epoch: u64,
}
struct ResponseWaiterEntry {
generation: NonZeroU64,
waiter: ResponseWaiter,
}
#[derive(Default)]
pub(crate) struct ResponseWaiterMap {
entries: HashMap<String, ResponseWaiterEntry>,
last_generation: u64,
sweep_epoch: u64,
}
impl ResponseWaiterMap {
fn next_generation(&mut self) -> NonZeroU64 {
loop {
self.last_generation = self.last_generation.wrapping_add(1);
if let Some(generation) = NonZeroU64::new(self.last_generation) {
return generation;
}
}
}
pub(crate) fn try_insert_guarded(
&mut self,
request_id: String,
waiter: ResponseWaiter,
) -> Option<NonZeroU64> {
use std::collections::hash_map::Entry;
let generation = self.next_generation();
match self.entries.entry(request_id) {
Entry::Vacant(entry) => {
entry.insert(ResponseWaiterEntry { generation, waiter });
Some(generation)
}
Entry::Occupied(_) => None,
}
}
pub(crate) fn insert(
&mut self,
request_id: String,
waiter: ResponseWaiter,
) -> Option<ResponseWaiter> {
let generation = self.next_generation();
self.entries
.insert(request_id, ResponseWaiterEntry { generation, waiter })
.map(|entry| entry.waiter)
}
pub(crate) fn remove(&mut self, request_id: &str) -> Option<ResponseWaiter> {
self.entries.remove(request_id).map(|entry| entry.waiter)
}
pub(crate) fn current_epoch(&self) -> u64 {
self.sweep_epoch
}
pub(crate) fn drop_expired_phash(&mut self) {
let epoch = self.sweep_epoch;
self.entries.retain(|_, entry| match &entry.waiter {
ResponseWaiter::Phash(waiter) => waiter.registered_epoch >= epoch,
ResponseWaiter::Iq(_) => true,
});
self.sweep_epoch = self.sweep_epoch.wrapping_add(1);
}
pub(crate) fn remove_guarded(&mut self, request_id: &str, cleanup_generation: NonZeroU64) {
if self
.entries
.get(request_id)
.is_some_and(|entry| entry.generation == cleanup_generation)
{
self.entries.remove(request_id);
}
}
pub(crate) fn clear(&mut self) {
self.entries = HashMap::new();
}
#[cfg(test)]
pub(crate) fn contains_key(&self, request_id: &str) -> bool {
self.entries.contains_key(request_id)
}
pub(crate) fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub(crate) fn len(&self) -> usize {
self.entries.len()
}
}
pub struct Client {
pub(crate) runtime: Arc<dyn Runtime>,
pub(crate) core: wacore::client::CoreClient,
pub(crate) persistence_manager: Arc<PersistenceManager>,
pub(crate) msg_secret_buffer: Arc<crate::msg_secret_buffer::MsgSecretWriteBuffer>,
pub(crate) inbound_commit_batch: crate::message::commit_batch::InboundCommitBatcher,
pub(crate) media_conn: Arc<RwLock<Option<crate::mediaconn::MediaConn>>>,
pub(crate) is_logged_in: Arc<AtomicBool>,
#[cfg(feature = "client-lifecycle")]
pub(crate) login_transition: std::sync::Mutex<()>,
pub(crate) is_connecting: Arc<AtomicBool>,
pub(crate) is_running: Arc<AtomicBool>,
is_connected: Arc<AtomicBool>,
send_active_receipts: AtomicU32,
pub(crate) ik_handshake_failures: Arc<AtomicU32>,
pub(crate) shutdown_notifier: wacore::runtime::ShutdownNotifier,
pub(crate) connection_shutdown: std::sync::Mutex<wacore::runtime::ShutdownNotifier>,
#[cfg(feature = "client-lifecycle")]
lifecycle: Option<Arc<LifecycleRegistration>>,
#[cfg(feature = "plugins")]
pub(crate) plugin_host: Option<Arc<crate::plugins::PluginHost>>,
pub(crate) stats: Arc<wacore::stats::SessionStats>,
pub(crate) transport: Arc<Mutex<Option<Arc<dyn crate::transport::Transport>>>>,
pub(crate) transport_events:
Arc<Mutex<Option<async_channel::Receiver<crate::transport::TransportEvent>>>>,
pub(crate) transport_factory: Arc<dyn crate::transport::TransportFactory>,
pub(crate) noise_socket: Arc<Mutex<Option<Arc<NoiseSocket>>>>,
pub(crate) response_waiters: Arc<std::sync::Mutex<ResponseWaiterMap>>,
node_waiters: std::sync::Mutex<Vec<NodeWaiter>>,
node_waiter_count: AtomicUsize,
sent_node_waiters: std::sync::Mutex<Vec<SentNodeWaiter>>,
sent_node_waiter_count: AtomicUsize,
pub(crate) unique_id: String,
pub(crate) id_counter: Arc<AtomicU64>,
pub(crate) unified_session: crate::unified_session::UnifiedSessionManager,
pub(crate) signal_cache: Arc<crate::store::signal_cache::SignalStoreCache>,
pub(crate) message_processing_semaphore: std::sync::Mutex<Arc<async_lock::Semaphore>>,
pub(crate) message_semaphore_generation: Arc<AtomicU64>,
pub(crate) session_locks: Cache<String, Arc<Mutex<()>>>,
pub(crate) chat_lanes: Cache<Jid, ChatLane>,
pub(crate) lid_pn_cache: Arc<LidPnCache>,
pub(crate) ab_props: Arc<wacore::store::ab_props::AbPropsCache>,
pub group_cache: Mutex<Option<Arc<GroupCache>>>,
pub(crate) expected_disconnect: Arc<AtomicBool>,
pub(crate) intentional_reconnect: AtomicBool,
pub(crate) connection_generation: Arc<AtomicU64>,
pub(crate) recent_messages: Cache<ChatMessageId, Arc<Vec<u8>>>,
pub(crate) sender_key_device_cache: crate::sender_key_device_cache::SenderKeyDeviceCache,
pub(crate) pending_device_sync: crate::pending_device_sync::PendingDeviceSync,
pub(crate) pending_retries: Arc<std::sync::Mutex<HashSet<String>>>,
pub(crate) message_retry_counts:
Cache<String, (u8, Option<wacore::protocol::retry::RetryReason>)>,
pub(crate) session_recreate_history: Cache<Jid, wacore::time::Instant>,
pub(crate) resend_rate_limiter: crate::resend_rate_limiter::ResendRateLimiter,
pub(crate) undecryptable_dispatched: Cache<ChatMessageId, ()>,
pub enable_auto_reconnect: Arc<AtomicBool>,
pub(crate) auto_reconnect_errors: Arc<AtomicU32>,
pub(crate) connected_at_ms: Arc<AtomicI64>,
pub(crate) backoff_reset_suppressed: Arc<AtomicBool>,
pub(crate) needs_initial_full_sync: Arc<app_state::BootstrapGate>,
pub(crate) app_state_processor: Mutex<Option<Arc<AppStateProcessor>>>,
pub(crate) app_state_key_requests: Arc<Mutex<HashMap<Vec<u8>, wacore::time::Instant>>>,
pub(crate) app_state_syncing: Arc<app_state::SyncInFlight>,
pub(crate) app_state_send_lock: Arc<Mutex<()>>,
pub(crate) initial_keys_synced_notifier: Arc<event_listener::Event>,
pub(crate) initial_app_state_keys_received: Arc<AtomicBool>,
pub(crate) prekey_upload_lock: Arc<Mutex<()>>,
pub(crate) signed_pre_key_rotation_lock: Arc<Mutex<()>>,
pub(crate) offline_sync_notifier: Arc<event_listener::Event>,
pub(crate) offline_sync_completed: Arc<AtomicBool>,
pub(crate) offline_sync_finish_started: Arc<AtomicBool>,
pub(crate) offline_receipt_buffer:
std::sync::Mutex<Vec<Arc<crate::types::message::MessageInfo>>>,
pub(crate) history_sync_activity: Arc<crate::sync_task::HistorySyncActivity>,
pub(crate) outbound_flush: Arc<crate::flush_scope::FlushScope>,
pub(crate) delivery_receipt_queue: std::sync::OnceLock<
async_channel::Sender<(
Arc<crate::types::message::MessageInfo>,
crate::flush_scope::FlushGuard,
)>,
>,
pub(crate) transport_ack_queue: std::sync::OnceLock<
async_channel::Sender<(
Arc<wacore_binary::OwnedNodeRef>,
crate::flush_scope::FlushGuard,
)>,
>,
pub(crate) presence_subscriptions: Arc<Mutex<HashSet<Jid>>>,
pub(crate) offline_sync_metrics: Arc<OfflineSyncMetrics>,
pub(crate) offline_batch: Arc<offline_resume::OfflineBatchCoordinator>,
pub(crate) socket_ready_notifier: Arc<event_listener::Event>,
pub(crate) is_ready: Arc<AtomicBool>,
pub(crate) connected_notifier: Arc<event_listener::Event>,
pub(crate) authenticated_generation: Arc<AtomicU64>,
pub(crate) session_state_notifier: Arc<event_listener::Event>,
pub(crate) major_sync_task_sender: async_channel::Sender<MajorSyncTask>,
pub(crate) pairing_cancellation_tx: Arc<Mutex<Option<async_channel::Sender<()>>>>,
pub(crate) pairing_qr_refresh_tx: Arc<Mutex<Option<async_channel::Sender<()>>>>,
pub(crate) pair_code_state: Arc<Mutex<wacore::pair_code::PairCodeState>>,
pub(crate) passkey_state: Arc<Mutex<crate::passkey::flow::PasskeyFlowState>>,
pub(crate) passkey_opening: AtomicBool,
pub custom_enc_handlers: std::sync::OnceLock<HashMap<String, Arc<dyn EncHandler>>>,
pub(crate) inbound_durability_hook:
std::sync::OnceLock<Arc<dyn crate::types::durability_hook::InboundDurabilityHook>>,
pub(crate) retry_admission:
std::sync::OnceLock<Arc<dyn crate::types::retry_admission::RetryAdmission>>,
pub(crate) chatstate_handlers: Arc<RwLock<Vec<ChatStateHandler>>>,
pub(crate) pdo_pending_requests: Cache<ChatMessageId, crate::pdo::PendingPdoRequest>,
pub(crate) pdo_requested: Cache<ChatMessageId, ()>,
pub(crate) device_registry_cache: device_topology::DeviceRegistryCache,
pub(crate) device_topology: Arc<device_topology::DeviceTopology>,
pub(crate) device_memos_enabled: bool,
pub(crate) group_devices_memo: Cache<Jid, Arc<device_registry::GroupDevicesMemo>>,
pub(crate) dm_devices_memo: Cache<Jid, Arc<device_registry::DmDevicesMemo>>,
#[cfg(test)]
pub(crate) dm_devices_memo_recomputes: AtomicU64,
pub(crate) group_distribution_locks: Cache<Jid, Arc<Mutex<()>>>,
pub(crate) skdm_warm_memo: Cache<Jid, SkdmWarmMemoEntry>,
pub(crate) stanza_router: crate::handlers::router::StanzaRouter,
pub(crate) synchronous_ack: bool,
pub http_client: Arc<dyn crate::http::HttpClient>,
pub(crate) override_version: Option<(u32, u32, u32)>,
pub(crate) skip_history_sync: AtomicBool,
pub(crate) wanted_pre_key_count: AtomicUsize,
pub(crate) cache_config: CacheConfig,
pub(crate) self_weak: std::sync::OnceLock<std::sync::Weak<Client>>,
pub(crate) signal_flush_state: AtomicU64,
pub(crate) signal_flush_lifecycle: Mutex<()>,
#[cfg(test)]
pub(crate) signal_flush_test_failures: AtomicU32,
#[cfg(test)]
pub(crate) signal_flush_test_block: AtomicBool,
#[cfg(test)]
pub(crate) signal_flush_test_in_attempt: AtomicU32,
#[cfg(test)]
pub(crate) app_state_key_share_prepare_test_failures: AtomicU32,
pub(crate) saver_handle: std::sync::OnceLock<wacore::runtime::AbortHandle>,
pub(crate) alloc_meter: std::sync::OnceLock<Arc<wacore::stats::AllocMeter>>,
raw_node_forwarding: AtomicUsize,
#[cfg(feature = "voip-runtime")]
pub(crate) call_registry: Arc<wacore::voip::CallRegistry>,
#[cfg(feature = "voip-runtime")]
pending_call_link_joins: Arc<std::sync::Mutex<voip::PendingCallLinkJoins>>,
#[cfg(feature = "voip-runtime")]
pending_call_link_join_lane: Arc<Mutex<()>>,
#[cfg(feature = "voip-runtime")]
pub(crate) answer_transition_locks: [Arc<Mutex<()>>; 16],
#[cfg(feature = "voip-runtime")]
pub(crate) pending_outgoing_calls:
Arc<std::sync::Mutex<HashMap<String, crate::voip::facade::PendingOutgoing>>>,
}
fn build_pong(to: String, id: Option<&str>) -> Node {
let mut builder = NodeBuilder::new("iq").attr("to", to).attr("type", "result");
if let Some(id) = id {
builder = builder.attr("id", id);
}
builder.build()
}
#[inline]
fn value_refs_display_equal(
left: &wacore_binary::node::ValueRef<'_>,
right: &wacore_binary::node::ValueRef<'_>,
) -> bool {
use wacore_binary::node::ValueRef;
match (left, right) {
(ValueRef::String(left), ValueRef::String(right)) => left == right,
(ValueRef::Jid(left), ValueRef::Jid(right)) => left.display_eq_jid(right),
(ValueRef::String(left), ValueRef::Jid(right)) => right.display_eq(left),
(ValueRef::Jid(left), ValueRef::String(right)) => left.display_eq(right),
}
}
#[derive(Clone, Copy)]
enum AckParticipantPolicy {
Preserve,
OmitReceiptDestinationDuplicate,
}
#[inline]
fn ack_participant<'node, 'data>(
node: &'node wacore_binary::NodeRef<'data>,
from: &wacore_binary::node::ValueRef<'data>,
policy: AckParticipantPolicy,
) -> Option<&'node wacore_binary::node::ValueRef<'data>> {
node.get_attr("participant")
.filter(|participant| match policy {
AckParticipantPolicy::Preserve => true,
AckParticipantPolicy::OmitReceiptDestinationDuplicate => {
node.tag != "receipt" || !value_refs_display_equal(participant, from)
}
})
}
fn encode_ack_bytes(
node: &wacore_binary::NodeRef<'_>,
own_device_pn: Option<&Jid>,
participant_policy: AckParticipantPolicy,
) -> Result<Vec<u8>, crate::features::StanzaResponseError> {
use wacore_binary::encoder::{ByteWriter, EncodeNode, Encoder};
let id_val = crate::features::required_stanza_attr(node, "id")?;
let from_val = crate::features::required_stanza_attr(node, "from")?;
let tag = node.tag.as_ref();
let participant_val = ack_participant(node, from_val, participant_policy);
let recipient_val = node.get_attr("recipient");
let typ_val = if !is_encrypt_identity_notification(node) {
node.get_attr("type")
} else {
None
};
let own_device_pn = if tag == "message" || tag == "status" {
Some(own_device_pn.ok_or(crate::features::StanzaResponseError::MissingLocalIdentity)?)
} else {
None
};
let attr_count = 3
+ usize::from(own_device_pn.is_some())
+ usize::from(participant_val.is_some())
+ usize::from(recipient_val.is_some())
+ usize::from(typ_val.is_some());
struct AckNode<'a> {
id: &'a wacore_binary::node::ValueRef<'a>,
from: &'a wacore_binary::node::ValueRef<'a>,
participant: Option<&'a wacore_binary::node::ValueRef<'a>>,
recipient: Option<&'a wacore_binary::node::ValueRef<'a>>,
typ: Option<&'a wacore_binary::node::ValueRef<'a>>,
own_pn: Option<&'a Jid>,
tag_str: &'a str,
attr_count: usize,
}
impl EncodeNode for AckNode<'_> {
fn tag(&self) -> &str {
"ack"
}
fn attrs_len(&self) -> usize {
self.attr_count
}
fn has_content(&self) -> bool {
false
}
fn encode_attrs<'a, W: ByteWriter>(
&self,
enc: &mut Encoder<'a, W>,
) -> wacore_binary::Result<()> {
enc.write_string("class")?;
enc.write_string(self.tag_str)?;
enc.write_string("id")?;
self.id.encode_value(enc)?;
enc.write_string("to")?;
self.from.encode_value(enc)?;
if let Some(pn) = self.own_pn {
enc.write_string("from")?;
enc.write_jid_owned(pn)?;
}
if let Some(p) = self.participant {
enc.write_string("participant")?;
p.encode_value(enc)?;
}
if let Some(r) = self.recipient {
enc.write_string("recipient")?;
r.encode_value(enc)?;
}
if let Some(t) = self.typ {
enc.write_string("type")?;
t.encode_value(enc)?;
}
Ok(())
}
fn encode_content<'a, W: ByteWriter>(
&self,
_enc: &mut Encoder<'a, W>,
) -> wacore_binary::Result<()> {
Ok(())
}
}
let ack = AckNode {
id: id_val,
from: from_val,
participant: participant_val,
recipient: recipient_val,
typ: typ_val,
own_pn: own_device_pn,
tag_str: tag,
attr_count,
};
let mut buf = Vec::with_capacity(64);
let mut encoder = Encoder::new_vec(&mut buf)?;
encoder.write_node(&ack)?;
Ok(buf)
}
fn message_ack_source_node(info: &crate::types::message::MessageInfo) -> Node {
let from = if info.source.is_group {
&info.source.chat
} else {
&info.source.sender
};
let mut builder = NodeBuilder::new("message")
.attr("id", &info.id)
.attr("from", from);
if let Some(recipient) = &info.source.recipient {
builder = builder.attr("recipient", recipient);
}
if info.source.is_group {
builder = builder.attr("participant", &info.source.sender);
}
builder.build()
}
#[cfg(test)]
fn build_ack_node(node: &wacore_binary::NodeRef<'_>, own_device_pn: Option<&Jid>) -> Option<Node> {
let id = node.get_attr("id")?.to_node_value();
let from_ref = node.get_attr("from")?;
let from = from_ref.to_node_value();
let tag = node.tag.as_ref();
let participant = ack_participant(
node,
from_ref,
AckParticipantPolicy::OmitReceiptDestinationDuplicate,
)
.map(|value| value.to_node_value());
let recipient = node.get_attr("recipient").map(|v| v.to_node_value());
let typ = if !is_encrypt_identity_notification(node) {
node.get_attr("type").map(|v| v.to_node_value())
} else {
None
};
let mut attrs = Attrs::with_capacity(7);
attrs.insert("class", NodeValue::from(tag));
attrs.insert("id", id);
attrs.insert("to", from);
if tag == "message"
&& let Some(own_device_pn) = own_device_pn
{
attrs.insert("from", NodeValue::Jid(own_device_pn.clone()));
}
if let Some(p) = participant {
attrs.insert("participant", p);
}
if let Some(r) = recipient {
attrs.insert("recipient", r);
}
if let Some(t) = typ {
attrs.insert("type", t);
}
Some(Node {
tag: Cow::Borrowed("ack"),
attrs,
content: None,
})
}
fn is_encrypt_identity_notification(node: &wacore_binary::NodeRef<'_>) -> bool {
node.tag == "notification"
&& node
.get_attr("type")
.is_some_and(|value| value == "encrypt")
&& node.get_optional_child("identity").is_some()
}
pub(crate) fn should_reset_backoff(
connected_at_ms: i64,
now_ms: i64,
penalty_pending: bool,
) -> bool {
!penalty_pending
&& connected_at_ms != 0
&& now_ms.saturating_sub(connected_at_ms) >= Client::STABLE_CONNECTION_RESET_MS
}
fn fibonacci_backoff(attempt: u32) -> Duration {
const MAX_MS: u64 = 900_000;
let mut a: u64 = 1000;
let mut b: u64 = 1000;
for _ in 0..attempt {
let next = a.saturating_add(b).min(MAX_MS);
a = b;
b = next;
}
let base = a.min(MAX_MS);
let jitter_range = base / 10;
let jitter = if jitter_range > 0 {
rand::make_rng::<rand::rngs::StdRng>().random_range(0..=(jitter_range * 2)) as i64
- jitter_range as i64
} else {
0
};
let ms = (base as i64 + jitter).max(0) as u64;
Duration::from_millis(ms)
}
#[cfg(test)]
mod tests;