From d6f2bda12ec4ab16e2010d0c5c4a70219f85b350 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 17:27:29 +0000 Subject: [PATCH 1/3] refactor(api): rustdoc for entry types, must_use, root re-exports, and constructor consistency Ergonomics pass over the public surface, no behavior changes. - Document Client and Bot, the two entry points, including how to build them and where run/send/events live. - Mark BotHandle, BotBuilder and ClientBuilder #[must_use]; dropping a BotHandle silently aborts the bot, and a dropped builder does nothing. - Re-export Polls, PollOptionResult, PollVoteCiphertext, MexGraphQLError and EncType from the crate root, like their feature siblings. - Drop the get_ prefix from Client::get_push_name/get_pn/get_lid so they match the surrounding accessors. - Take &Jid/&[Jid] in AcceptGroupInviteV4Iq, BatchGetGroupInfoIq and GetGroupProfilePicturesIq constructors, per the IqSpec convention. - Seal SendOptions and EditOptions with #[non_exhaustive] and add with_* setters so fields can be added without breaking consumers. - Explain when to use Groups::query_info (cached, send oriented) versus Groups::get_metadata (full, always over the network). --- src/bot.rs | 37 ++++++++++++ src/client.rs | 37 ++++++++++++ src/client/accessors.rs | 11 ++-- src/client/builder.rs | 1 + src/client/messaging.rs | 4 +- src/features/comments.rs | 4 +- src/features/events.rs | 4 +- src/features/groups.rs | 35 ++++++++--- src/features/polls.rs | 6 +- src/features/reaction.rs | 4 +- src/handlers/call.rs | 4 +- src/history_sync.rs | 2 +- src/lib.rs | 15 ++--- src/message/msg_secret.rs | 34 +++++------ src/message/receive.rs | 2 +- src/message/tests.rs | 2 +- src/send/mod.rs | 65 ++++++++++++++++++++- src/voip/facade.rs | 30 ++++------ tests/e2e/src/lib.rs | 10 ++-- tests/e2e/tests/app_state.rs | 50 +++++----------- tests/e2e/tests/chat_actions.rs | 78 +++++-------------------- tests/e2e/tests/community.rs | 12 ++-- tests/e2e/tests/groups.rs | 2 +- tests/e2e/tests/lid_sessions.rs | 20 +++---- tests/e2e/tests/media.rs | 54 +++-------------- tests/e2e/tests/memory_soak.rs | 20 +++---- tests/e2e/tests/prekey_sessions.rs | 2 +- tests/e2e/tests/privacy_tokens.rs | 66 ++++++++------------- tests/e2e/tests/profile.rs | 22 +++---- tests/e2e/tests/profile_picture.rs | 31 ++-------- tests/e2e/tests/receipts.rs | 5 +- tests/e2e/tests/retry_dm_multidevice.rs | 5 +- tests/e2e/tests/session_reuse.rs | 10 ++-- tests/e2e/tests/status.rs | 2 +- wacore/src/iq/groups.rs | 33 +++++------ wacore/src/stanza/call.rs | 2 +- 36 files changed, 358 insertions(+), 363 deletions(-) diff --git a/src/bot.rs b/src/bot.rs index 1fa37cedf..4bc18be7e 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -391,6 +391,7 @@ impl EventHandler for CallbackBusAdapter { /// /// Dropping the handle aborts the bot task. Keep it alive for as long as the /// bot should run, and prefer [`BotHandle::shutdown`] to stop it. +#[must_use = "dropping the handle aborts the bot; bind it and await it, or call .shutdown()"] pub struct BotHandle { client: Arc, done_rx: futures::channel::oneshot::Receiver<()>, @@ -446,6 +447,41 @@ async fn run_metered>( } } +/// A configured WhatsApp session with its event handlers already wired, +/// ready to be started. +/// +/// This is the high-level entry point and what most applications should use. +/// Build one with [`Bot::builder`]: the typestate [`BotBuilder`] takes the +/// storage backend (the only required dependency with the default cargo +/// features), the pairing callbacks, and the message/event handlers, then +/// hands back a `Bot`. +/// +/// Starting it is a single call. [`Bot::run`] drives the session on the +/// current task until logout or shutdown; [`Bot::spawn`] starts it on the +/// runtime instead and returns a [`BotHandle`] you can await, shut down +/// gracefully, or abort. Both consume the `Bot`, so handlers are registered +/// at build time, not afterwards. +/// +/// ```no_run +/// # use whatsapp_rust::prelude::*; +/// # async fn example() -> Result<(), Box> { +/// let bot = Bot::builder() +/// .with_backend(SqliteStore::new("whatsapp.db").await?) +/// .on_message(|ctx| async move { +/// let _ = ctx.reply("pong").await; +/// }) +/// .build() +/// .await?; +/// +/// bot.run().await; +/// # Ok(()) +/// # } +/// ``` +/// +/// Handlers registered through the builder (`on_message`, `on_event`, …) +/// receive typed [`Event`](wacore::types::events::Event) payloads. Anything the +/// builder does not expose is reachable on the underlying client via +/// [`Bot::client`], which stays valid after the bot is started. pub struct Bot { client: Arc, sync_task_receiver: Option>, @@ -607,6 +643,7 @@ impl Bot { /// into compile-time errors. With the default cargo features, transport, HTTP /// client and runtime start [`Provided`] (Tokio WebSocket, ureq, Tokio), so /// only the backend is required. +#[must_use = "call .build() to produce the Bot; the builder does nothing on its own"] pub struct BotBuilder< B = MissingBackend, T = MissingTransport, diff --git a/src/client.rs b/src/client.rs index 33584c98e..64bcc1089 100644 --- a/src/client.rs +++ b/src/client.rs @@ -690,6 +690,43 @@ impl ResponseWaiterMap { } } +/// A single WhatsApp session: the connection, the Signal state, and every +/// protocol operation built on top of them. +/// +/// This is the low-level entry point. Build one with +/// [`ClientBuilder`](crate::client::ClientBuilder), which +/// takes the four platform dependencies (storage backend, transport factory, +/// HTTP client, async runtime) and validates them at runtime. Most applications +/// should use [`Bot`](crate::bot::Bot) instead and reach the client through +/// [`Bot::client`](crate::bot::Bot::client); `Client` is what remains when you +/// need to drive the lifecycle yourself, from an FFI host, or from a wrapper +/// that cannot express typestate generics. +/// +/// The client is always used behind an `Arc` (most methods take `self: &Arc`) +/// and is cheap to clone and share across tasks. +/// +/// # Lifecycle +/// +/// [`Client::run`] owns the session: it connects, keeps the socket alive, and +/// reconnects with backoff until [`Client::disconnect`] is called or the device +/// is logged out. [`Client::connect`] performs a single connection attempt +/// without the supervision loop, for hosts that manage retries themselves. +/// +/// # Events +/// +/// Everything the server reports (messages, receipts, pairing progress, +/// connection state) is delivered as an [`Event`](wacore::types::events::Event) +/// on the event bus. Register a handler with [`Client::subscribe`] (explicit +/// [`EventInterest`](wacore::types::events::EventInterest) filter) or +/// [`Client::subscribe_handler`]. +/// +/// # Sending +/// +/// [`Client::send_message`] covers the common path; +/// [`Client::send_message_with_options`] takes a [`SendOptions`](crate::send::SendOptions) +/// for message-id pinning, ephemeral expiration, and cache freshness. Domain +/// operations hang off accessors such as [`Client::groups`], [`Client::contacts`], +/// and [`Client::presence`]. pub struct Client { pub(crate) runtime: Arc, pub(crate) core: wacore::client::CoreClient, diff --git a/src/client/accessors.rs b/src/client/accessors.rs index ec168b83f..f5826c93e 100644 --- a/src/client/accessors.rs +++ b/src/client/accessors.rs @@ -325,18 +325,21 @@ impl Client { // itself is an Arc refcount bump (no lock against writers). Callers that // only need a borrow can hold `persistence_manager().get_device_snapshot()` // and read fields directly. - pub fn get_push_name(&self) -> String { + /// This device's push name (the display name peers see). + pub fn push_name(&self) -> String { self.persistence_manager .get_device_snapshot() .push_name .clone() } - pub fn get_pn(&self) -> Option { + /// This device's phone-number JID, or `None` before pairing completes. + pub fn pn(&self) -> Option { self.persistence_manager.get_device_snapshot().pn.clone() } - pub fn get_lid(&self) -> Option { + /// This device's LID JID, or `None` before pairing completes. + pub fn lid(&self) -> Option { self.persistence_manager.get_device_snapshot().lid.clone() } @@ -369,7 +372,7 @@ impl Client { } pub(crate) fn require_pn(&self) -> Result { - self.get_pn().ok_or(ClientError::NotLoggedIn.into()) + self.pn().ok_or(ClientError::NotLoggedIn.into()) } /// Resolve our own JID for a group, respecting its addressing mode. diff --git a/src/client/builder.rs b/src/client/builder.rs index fd63f0541..4091077e5 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -103,6 +103,7 @@ pub enum ClientBuilderError { /// Unlike [`crate::bot::BotBuilder`], this builder deliberately does not use /// typestate. FFI and embedded hosts can populate dependencies dynamically and /// receive a typed error without encoding Rust generic state in their wrapper. +#[must_use = "call .build() to produce the Client; the builder does nothing on its own"] pub struct ClientBuilder { runtime: Option>, persistence_manager: Option>, diff --git a/src/client/messaging.rs b/src/client/messaging.rs index 5b5622b7a..22110260e 100644 --- a/src/client/messaging.rs +++ b/src/client/messaging.rs @@ -119,7 +119,7 @@ impl Client { .to_string(), ) } else { - if self.get_pn().is_none() { + if self.pn().is_none() { return Err(crate::send::SendError::NotLoggedIn); } None @@ -213,7 +213,7 @@ impl Client { .map_err(SendError::from_anyhow)? .to_non_ad() } else { - self.get_pn().ok_or(SendError::NotLoggedIn)?.to_non_ad() + self.pn().ok_or(SendError::NotLoggedIn)?.to_non_ad() }; let participant = if to.is_group() { Some(self_jid.to_string()) diff --git a/src/features/comments.rs b/src/features/comments.rs index 16bf40c40..175f82913 100644 --- a/src/features/comments.rs +++ b/src/features/comments.rs @@ -69,8 +69,8 @@ impl<'a> Comments<'a> { // WA Web comments are authored under the LID identity // (getMeLidUserOrThrow); fall back to PN only when no LID is known. let commenter = client - .get_lid() - .or_else(|| client.get_pn()) + .lid() + .or_else(|| client.pn()) .map(|j| j.to_non_ad()) .ok_or(SendError::NotLoggedIn)?; diff --git a/src/features/events.rs b/src/features/events.rs index 5d79d19a3..49f02d6f2 100644 --- a/src/features/events.rs +++ b/src/features/events.rs @@ -88,7 +88,7 @@ impl<'a> Events<'a> { message_secret.len() ))); } - let my_jid = self.client.get_pn().ok_or(SendError::NotLoggedIn)?; + let my_jid = self.client.pn().ok_or(SendError::NotLoggedIn)?; let my_base = my_jid.to_non_ad(); let responder = self @@ -142,7 +142,7 @@ impl<'a> Events<'a> { if !event_creator_jid.is_lid() { return own_pn.clone(); } - match self.client.get_lid() { + match self.client.lid() { Some(lid) => lid.to_non_ad(), None => own_pn.clone(), } diff --git a/src/features/groups.rs b/src/features/groups.rs index 149e2dec9..a9049588b 100644 --- a/src/features/groups.rs +++ b/src/features/groups.rs @@ -367,6 +367,19 @@ impl<'a> Groups<'a> { Self { client } } + /// Query the cached, send-oriented view of a group. + /// + /// Returns the slim [`GroupInfo`] the encryption path needs: participant + /// JIDs, the group's addressing mode, the LID/PN mapping, and whether it is + /// a community announcement group. Results are shared through the group + /// cache and refreshed with the persisted participant phash, so a repeated + /// call is usually free and a stale entry costs a `not-modified` round trip + /// instead of a full metadata download. + /// + /// This is the right call for routing and encrypting a message. For the + /// user-facing fields (subject, description, admin roles, group settings) + /// use [`Groups::get_metadata`], and to control staleness explicitly use + /// [`Groups::query_info_with_freshness`]. pub async fn query_info(&self, jid: &Jid) -> Result, GroupError> { self.query_info_with_freshness(jid, crate::cache::Freshness::CachePreferred) .await @@ -609,6 +622,17 @@ impl<'a> Groups<'a> { Ok(result) } + /// Fetch the complete, user-facing metadata of a group. + /// + /// Returns an owned [`GroupMetadata`]: subject, description, creator, + /// per-participant admin roles, ephemeral and membership settings, plus the + /// participants' phone numbers resolved from their LIDs. The query always + /// hits the network (no phash is sent, so the server never answers + /// `not-modified`) and the result does not populate the group cache. + /// + /// This is the right call for displaying or auditing a group. When you only + /// need the participant list to send a message, prefer the cached + /// [`Groups::query_info`]. pub async fn get_metadata(&self, jid: &Jid) -> Result { // No phash is sent, so the server always returns the full group. match self.client.execute(GroupQueryIq::new(jid)).await? { @@ -944,10 +968,7 @@ impl<'a> Groups<'a> { Ok(self .client .execute(AcceptGroupInviteV4Iq::new( - group_jid.clone(), - code.to_string(), - expiration, - admin_jid.clone(), + group_jid, code, expiration, admin_jid, )) .await?) } @@ -1137,7 +1158,7 @@ impl<'a> Groups<'a> { wacore::iq::groups::BATCH_GROUP_INFO_LIMIT, ))); } - let raw = self.client.execute(BatchGetGroupInfoIq::new(jids)).await?; + let raw = self.client.execute(BatchGetGroupInfoIq::new(&jids)).await?; Ok(raw .into_iter() .map(|r| match r { @@ -1164,13 +1185,13 @@ impl<'a> Groups<'a> { wacore::iq::groups::BATCH_PROFILE_PICTURES_LIMIT, ))); } - let groups = group_jids + let groups: Vec<(Jid, PictureType)> = group_jids .into_iter() .map(|jid| (jid, picture_type)) .collect(); Ok(self .client - .execute(GetGroupProfilePicturesIq::with_type(groups)) + .execute(GetGroupProfilePicturesIq::with_type(&groups)) .await?) } diff --git a/src/features/polls.rs b/src/features/polls.rs index 236ce3347..3378fb359 100644 --- a/src/features/polls.rs +++ b/src/features/polls.rs @@ -126,7 +126,7 @@ impl<'a> Polls<'a> { option_names: &[String], ) -> Result { let chat_jid = &chat_jid.into(); - let my_jid = self.client.get_pn().ok_or(PollError::NotLoggedIn)?; + let my_jid = self.client.pn().ok_or(PollError::NotLoggedIn)?; let my_base = my_jid.to_non_ad(); let voter_jid = self @@ -193,7 +193,7 @@ impl<'a> Polls<'a> { if !poll_creator_jid.is_lid() { return own_pn.clone(); } - match self.client.get_lid() { + match self.client.lid() { Some(lid) => lid.to_non_ad(), None => { log::warn!( @@ -536,7 +536,7 @@ mod tests { #[tokio::test] async fn voter_falls_back_to_pn_when_own_lid_unknown() { let client: Arc = create_test_client().await; - // No SetLid, so get_lid() is None. + // No SetLid, so lid() is None. let own_pn = Jid::pn("5511999999999"); let creator = Jid::lid("111000111000111"); diff --git a/src/features/reaction.rs b/src/features/reaction.rs index 09d82241d..13cde0150 100644 --- a/src/features/reaction.rs +++ b/src/features/reaction.rs @@ -82,8 +82,8 @@ impl Client { // CAG is our LID identity regardless of the parent author's namespace; // mirror the comment path (WA Web authors CAG addons under LID). let reactor = self - .get_lid() - .or_else(|| self.get_pn()) + .lid() + .or_else(|| self.pn()) .map(|j| j.to_non_ad()) .ok_or(SendError::NotLoggedIn)?; diff --git a/src/handlers/call.rs b/src/handlers/call.rs index 780b34550..fe826b4ea 100644 --- a/src/handlers/call.rs +++ b/src/handlers/call.rs @@ -354,8 +354,8 @@ impl StanzaHandler for CallHandler { #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.recv.call_offer_ack", level = "debug", skip_all, fields(peer = %call.from.observe()), err(Debug)))] async fn send_offer_ack_receipt(client: &Client, call: &IncomingCall) -> anyhow::Result<()> { let own_from = match call.from.server { - Server::Lid => client.get_lid(), - _ => client.get_pn(), + Server::Lid => client.lid(), + _ => client.pn(), }; let Some(receipt) = build_offer_ack_receipt(call, own_from.as_ref()) else { diff --git a/src/history_sync.rs b/src/history_sync.rs index 4e7969bf6..ae1296357 100644 --- a/src/history_sync.rs +++ b/src/history_sync.rs @@ -558,7 +558,7 @@ impl Client { media_key: &[u8], ) -> Result<(), anyhow::Error> { let own_jid = self - .get_pn() + .pn() .ok_or(crate::client::ClientError::NotLoggedIn)? .to_non_ad(); let (ciphertext, iv) = diff --git a/src/lib.rs b/src/lib.rs index 167a236bf..6d5c086fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -166,19 +166,20 @@ pub use features::{ AppStateError, BatchGroupResult, Blocking, BlockingError, BlocklistEntry, ChatActions, ChatStateError, ChatStateType, Chatstate, Comments, Community, CommunityError, CommunitySubgroup, ContactError, Contacts, CreateCommunityOptions, CreateCommunityResult, - CreateGroupResult, EncryptedEdit, EventCreationParams, EventResponseType, Events, + CreateGroupResult, EncType, EncryptedEdit, EventCreationParams, EventResponseType, Events, GroupAppealStatus, GroupCreateOptions, GroupDescription, GroupEphemeralSettings, GroupError, GroupJoinError, GroupMetadata, GroupParticipant, GroupParticipantDetails, GroupParticipantOptions, GroupProfilePicture, GroupSubject, GroupType, Groups, GrowthLockInfo, InviteInfoError, IsOnWhatsAppResult, JoinGroupResult, Labels, LinkSubgroupsResult, MediaRetryResult, MediaReupload, MediaReuploadError, MediaReuploadRequest, MemberAddMode, MemberLinkMode, MemberShareHistoryMode, MembershipApprovalMode, MembershipRequest, - MessageRetransmission, Mex, MexError, MexErrorExtensions, MexRequest, MexResponse, NackReason, - Newsletter, NewsletterError, NewsletterMessage, NewsletterMessageType, NewsletterMetadata, - NewsletterReactionCount, NewsletterRole, NewsletterState, NewsletterVerification, - ParticipantChangeResponse, ParticipantType, PictureType, PollError, Presence, PresenceError, - PresenceStatus, Profile, ProfileError, ProfilePicture, ReachoutTimelock, RetryReason, - RetryRequestError, RetryRequestOptions, RetryRequestOutcome, SecretEncKind, SecretEncrypted, + MessageRetransmission, Mex, MexError, MexErrorExtensions, MexGraphQLError, MexRequest, + MexResponse, NackReason, Newsletter, NewsletterError, NewsletterMessage, NewsletterMessageType, + NewsletterMetadata, NewsletterReactionCount, NewsletterRole, NewsletterState, + NewsletterVerification, ParticipantChangeResponse, ParticipantType, PictureType, PollError, + PollOptionResult, PollVoteCiphertext, Polls, Presence, PresenceError, PresenceStatus, Profile, + ProfileError, ProfilePicture, ReachoutTimelock, RetryReason, RetryRequestError, + RetryRequestOptions, RetryRequestOutcome, SecretEncKind, SecretEncrypted, SetProfilePictureResponse, Signal, SignalError, SignalSessionInfo, SignalSessionMigration, StanzaRejection, StanzaResponseError, Status, StatusPrivacySetting, StatusSendOptions, SyncActionMessageRange, TcToken, TcTokenError, UnlinkSubgroupsResult, UserInfo, diff --git a/src/message/msg_secret.rs b/src/message/msg_secret.rs index 4b93b6272..1d4916f77 100644 --- a/src/message/msg_secret.rs +++ b/src/message/msg_secret.rs @@ -120,23 +120,21 @@ impl Client { } match info.source.addressing_mode { - Some(AddressingMode::Lid) => match self.get_lid() { + Some(AddressingMode::Lid) => match self.lid() { Some(jid) => Some(jid), - None => self.get_pn(), + None => self.pn(), }, - Some(AddressingMode::Pn) => match self.get_pn() { + Some(AddressingMode::Pn) => match self.pn() { Some(jid) => Some(jid), - None => self.get_lid(), + None => self.lid(), }, - None if info.source.sender.is_lid() || info.source.chat.is_lid() => { - match self.get_lid() { - Some(jid) => Some(jid), - None => self.get_pn(), - } - } - None => match self.get_pn() { + None if info.source.sender.is_lid() || info.source.chat.is_lid() => match self.lid() { + Some(jid) => Some(jid), + None => self.pn(), + }, + None => match self.pn() { Some(jid) => Some(jid), - None => self.get_lid(), + None => self.lid(), }, } } @@ -712,9 +710,9 @@ impl Client { return Some(ts.clone()); } if info.source.sender.server == wacore_binary::Server::Bot { - self.get_lid() + self.lid() } else { - self.get_pn() + self.pn() } } @@ -878,9 +876,9 @@ impl Client { } }; return if lid_mode { - self.get_lid().or_else(|| self.get_pn()) + self.lid().or_else(|| self.pn()) } else { - self.get_pn().or_else(|| self.get_lid()) + self.pn().or_else(|| self.lid()) } .map(|j| j.to_non_ad()); } @@ -891,9 +889,9 @@ impl Client { /// chat addressing): LID identities key the HKDF of LID-addressed addons. pub(crate) fn addon_self_jid(&self, reference: &Jid) -> Option { if reference.is_lid() { - self.get_lid().or_else(|| self.get_pn()) + self.lid().or_else(|| self.pn()) } else { - self.get_pn().or_else(|| self.get_lid()) + self.pn().or_else(|| self.lid()) } .map(|j| j.to_non_ad()) } diff --git a/src/message/receive.rs b/src/message/receive.rs index fd4c4b6cd..d79711836 100644 --- a/src/message/receive.rs +++ b/src/message/receive.rs @@ -148,7 +148,7 @@ impl Client { let own_jid = nr .get_optional_child("participants") - .and_then(|_| self.get_pn()); + .and_then(|_| self.pn()); let mut all_enc_nodes: Vec<&NodeRef<'_>> = Vec::with_capacity(4); all_enc_nodes.extend(message_enc_nodes_for_device(nr, own_jid.as_ref())); diff --git a/src/message/tests.rs b/src/message/tests.rs index 1d70f77df..77b160e31 100644 --- a/src/message/tests.rs +++ b/src/message/tests.rs @@ -5491,7 +5491,7 @@ fn test_undecryptable_event_has_no_pending_pdo_hint() { ); } -/// Seed `device.pn` so `send_nack` clears its `get_pn()` guard. +/// Seed `device.pn` so `send_nack` clears its `pn()` guard. async fn seed_test_pn(client: &Arc) { use crate::store::commands::DeviceCommand; client diff --git a/src/send/mod.rs b/src/send/mod.rs index e749c408b..c2e0b6308 100644 --- a/src/send/mod.rs +++ b/src/send/mod.rs @@ -257,7 +257,17 @@ impl SendBranchOutput { } /// Options for [`Client::send_message_with_options`]. +/// +/// Start from [`SendOptions::default`] and chain the `with_*` setters; the +/// struct is `#[non_exhaustive]` so new knobs can be added without breaking +/// consumers. +/// +/// ``` +/// # use whatsapp_rust::send::SendOptions; +/// let options = SendOptions::default().with_message_id("3EB0ABCDEF"); +/// ``` #[derive(Debug, Clone, Default)] +#[non_exhaustive] pub struct SendOptions { /// Override the auto-generated message ID. /// Useful for resending a failed message with the same ID or idempotency. @@ -277,8 +287,51 @@ pub struct SendOptions { pub device_freshness: crate::cache::Freshness, } +impl SendOptions { + /// See [`SendOptions::message_id`]. + pub fn with_message_id(mut self, message_id: impl Into) -> Self { + self.message_id = Some(message_id.into()); + self + } + + /// See [`SendOptions::extra_stanza_nodes`]. + pub fn with_extra_stanza_nodes(mut self, nodes: Vec) -> Self { + self.extra_stanza_nodes = nodes; + self + } + + /// See [`SendOptions::ephemeral_expiration`]. + pub fn with_ephemeral_expiration(mut self, seconds: u32) -> Self { + self.ephemeral_expiration = Some(seconds); + self + } + + /// See [`SendOptions::stanza_type_override`]. + pub fn with_stanza_type_override(mut self, stanza_type: StanzaType) -> Self { + self.stanza_type_override = Some(stanza_type); + self + } + + /// See [`SendOptions::group_metadata_freshness`]. + pub fn with_group_metadata_freshness(mut self, freshness: crate::cache::Freshness) -> Self { + self.group_metadata_freshness = freshness; + self + } + + /// See [`SendOptions::device_freshness`]. + pub fn with_device_freshness(mut self, freshness: crate::cache::Freshness) -> Self { + self.device_freshness = freshness; + self + } +} + /// Options for [`Client::edit_message_with_options`]. +/// +/// Start from [`EditOptions::default`] and chain the `with_*` setters; the +/// struct is `#[non_exhaustive]` so new knobs can be added without breaking +/// consumers. #[derive(Debug, Clone, Default)] +#[non_exhaustive] pub struct EditOptions { /// Override the outer stanza id (default: a fresh id, like /// [`Client::edit_message`]). Pinning it to an **existing** message's id is @@ -292,6 +345,14 @@ pub struct EditOptions { pub stanza_id: Option, } +impl EditOptions { + /// See [`EditOptions::stanza_id`]. + pub fn with_stanza_id(mut self, stanza_id: impl Into) -> Self { + self.stanza_id = Some(stanza_id.into()); + self + } +} + #[derive(Default)] pub(crate) struct SendPipelineOptions { pub(crate) request_id: Option, @@ -2461,9 +2522,9 @@ impl Client { /// `PreparedGroupStanza.sender_identity` directly instead of this. pub(crate) async fn dm_sender_identity_for(&self, to: &Jid) -> Option { if to.server == wacore_binary::Server::Bot { - self.get_lid() + self.lid() } else { - self.get_pn() + self.pn() } } diff --git a/src/voip/facade.rs b/src/voip/facade.rs index 0107464a2..5316de533 100644 --- a/src/voip/facade.rs +++ b/src/voip/facade.rs @@ -215,10 +215,7 @@ impl<'a> AcceptCall<'a> { // Our own device LID: used both to pick the callKey enc for THIS device (a multi-device // offer lists one per ``) and as the send-side SRTP participant id. - let own_lid = self - .client - .get_lid() - .ok_or(CallError::Media("no own LID"))?; + let own_lid = self.client.lid().ok_or(CallError::Media("no own LID"))?; let enc = media .enc_for(Some(&own_lid)) .ok_or(CallError::Media("offer carried no callKey for this device"))?; @@ -379,10 +376,7 @@ impl<'a> OutgoingCall<'a> { let call_id = gen_call_id(); // Our own LID is the send-side SRTP participant id; required for E2E key derivation. - let own_lid = self - .client - .get_lid() - .ok_or(CallError::Media("no own LID"))?; + let own_lid = self.client.lid().ok_or(CallError::Media("no own LID"))?; // The media keys (SFrame/SRTP) derive from the peer's LID, so a PN callee must be resolved to // its LID first; without a known LID we would derive non-matching keys, so reject. The offer @@ -2364,7 +2358,7 @@ mod tests { ) -> (Arc, Arc) { use wacore::handshake::NoiseCipher; let pm = PersistenceManager::new(backend).await.expect("pm"); - // Set our own LID so get_lid() resolves (the send-side participant id). + // Set our own LID so lid() resolves (the send-side participant id). pm.process_command(crate::store::commands::DeviceCommand::SetLid(Some( Jid::new("111111111111111", Server::Lid), ))) @@ -2431,7 +2425,7 @@ mod tests { let device = peer_lid(); seed_peer_session(&client, &device).await; - let own_lid = client.get_lid().expect("own lid"); + let own_lid = client.lid().expect("own lid"); let (_mic_tx, mic_rx) = async_channel::unbounded::>(); let (spk_tx, _spk_rx) = async_channel::unbounded::>(); @@ -2555,7 +2549,7 @@ mod tests { client .signal_flush_test_block .store(true, Ordering::Release); - let own_lid = client.get_lid().expect("own lid"); + let own_lid = client.lid().expect("own lid"); let (_mic_tx, mic_rx) = async_channel::unbounded::>(); let (spk_tx, _spk_rx) = async_channel::unbounded::>(); let _handle = place_call( @@ -2588,7 +2582,7 @@ mod tests { let peer_user = Jid::new("333333333333333", Server::Lid); let device = peer_lid(); seed_peer_session(&client, &device).await; - let own_lid = client.get_lid().expect("own lid"); + let own_lid = client.lid().expect("own lid"); let waiter = client.wait_for_sent_node(crate::client::NodeFilter::tag("call")); let handle = place_call( @@ -2657,7 +2651,7 @@ mod tests { .await .unwrap(); - let own_lid = client.get_lid().expect("own lid"); + let own_lid = client.lid().expect("own lid"); let (_mic_tx, mic_rx) = async_channel::unbounded::>(); let (spk_tx, _spk_rx) = async_channel::unbounded::>(); @@ -2711,7 +2705,7 @@ mod tests { seed_peer_session(&client, &good0).await; seed_peer_session(&client, &good1).await; - let own_lid = client.get_lid().expect("own lid"); + let own_lid = client.lid().expect("own lid"); let (_mic_tx, mic_rx) = async_channel::unbounded::>(); let (spk_tx, _spk_rx) = async_channel::unbounded::>(); let waiter = client.wait_for_sent_node(crate::client::NodeFilter::tag("call")); @@ -2784,7 +2778,7 @@ mod tests { let peer_user = Jid::new("333333333333333", Server::Lid); // No session seeded for the device, so its encrypt errors and it is skipped. let device = peer_lid(); - let own_lid = client.get_lid().expect("own lid"); + let own_lid = client.lid().expect("own lid"); let (_mic_tx, mic_rx) = async_channel::unbounded::>(); let (spk_tx, _spk_rx) = async_channel::unbounded::>(); @@ -2833,7 +2827,7 @@ mod tests { let peer_user = Jid::new("333333333333333", Server::Lid); let device = peer_lid(); seed_peer_session(client, &device).await; - let own_lid = client.get_lid().expect("own lid"); + let own_lid = client.lid().expect("own lid"); let (_mic_tx, mic_rx) = async_channel::unbounded::>(); let (spk_tx, _spk_rx) = async_channel::unbounded::>(); let call_id = "00abcdef0123456789abcdef0123beef".to_string(); @@ -3362,7 +3356,7 @@ mod tests { let peer_user = Jid::new("333333333333333", Server::Lid); let device = peer_lid(); seed_peer_session(&client, &device).await; - let own_lid = client.get_lid().expect("own lid"); + let own_lid = client.lid().expect("own lid"); let (_mic_tx, mic_rx) = async_channel::unbounded::>(); let (spk_tx, _spk_rx) = async_channel::unbounded::>(); let call_id = "00abcdef0123456789abcdef0123dead".to_string(); @@ -3530,7 +3524,7 @@ mod tests { let peer_user = Jid::new("333333333333333", Server::Lid); let device = peer_lid(); seed_peer_session(&client, &device).await; - let own_lid = client.get_lid().expect("own lid"); + let own_lid = client.lid().expect("own lid"); let (_mic_tx, mic_rx) = async_channel::unbounded::>(); let (spk_tx, _spk_rx) = async_channel::unbounded::>(); let call_id = "00abcdef0123456789abcdef0123feed".to_string(); diff --git a/tests/e2e/src/lib.rs b/tests/e2e/src/lib.rs index af5761580..5dc587ef4 100644 --- a/tests/e2e/src/lib.rs +++ b/tests/e2e/src/lib.rs @@ -168,7 +168,7 @@ async fn collect_connect_diagnostics( impl TestClient { /// Create a client, connect to the mock server, and wait for PairSuccess + Connected. - /// Returns the connected TestClient with its JID available via `client.get_pn()`. + /// Returns the connected TestClient with its JID available via `client.pn()`. pub async fn connect(prefix: &str) -> anyhow::Result { Self::connect_inner(prefix, Some(unique_push_name(prefix))).await } @@ -276,7 +276,7 @@ impl TestClient { /// Get this client's phone number JID (non-AD format). pub async fn jid(&self) -> Jid { self.client - .get_pn() + .pn() .expect("Client should have a JID after connect") .to_non_ad() } @@ -286,12 +286,12 @@ impl TestClient { /// Notification handling stores tcTokens under the sender's LID when it is /// available, otherwise it falls back to the phone-number user part. pub async fn tc_token_key(&self) -> anyhow::Result { - if let Some(lid) = self.client.get_lid() { + if let Some(lid) = self.client.lid() { return Ok(lid.user.to_string()); } self.client - .get_pn() + .pn() .map(|jid| jid.user.to_string()) .ok_or_else(|| anyhow::anyhow!("Client should have a JID after connect")) } @@ -454,7 +454,7 @@ impl TestClient { /// Wait for initial app state sync to complete (keys become available). pub async fn wait_for_app_state_sync(&mut self) -> anyhow::Result<()> { - let push_name = self.client.get_push_name(); + let push_name = self.client.push_name(); if !push_name.is_empty() { return Ok(()); } diff --git a/tests/e2e/tests/app_state.rs b/tests/e2e/tests/app_state.rs index 7f731a591..e74b6b251 100644 --- a/tests/e2e/tests/app_state.rs +++ b/tests/e2e/tests/app_state.rs @@ -19,7 +19,7 @@ async fn test_initial_sync_delivers_push_name() -> anyhow::Result<()> { let mut client = TestClient::connect_without_push_name("e2e_as_init_sync").await?; client.wait_for_app_state_sync().await?; - let push_name = client.client.get_push_name(); + let push_name = client.client.push_name(); assert!( !push_name.is_empty(), "Push name should be set from initial critical_block sync (got empty — app state keys may be broken)" @@ -42,12 +42,12 @@ async fn test_push_name_survives_reconnect() -> anyhow::Result<()> { let name = "ReconnectTest"; client.client.profile().set_push_name(name).await?; - assert_eq!(client.client.get_push_name(), name); + assert_eq!(client.client.push_name(), name); info!("Push name set to '{name}'"); client.reconnect_and_wait().await?; - let after = client.client.get_push_name(); + let after = client.client.push_name(); assert_eq!(after, name, "Push name should survive reconnect"); info!("Push name after reconnect: '{after}'"); @@ -65,11 +65,7 @@ async fn test_mutation_works_after_reconnect() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_as_mut_recon_a").await?; let client_b = TestClient::connect("e2e_as_mut_recon_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -96,11 +92,7 @@ async fn test_undo_mutation_after_reconnect() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_as_undo_recon_a").await?; let client_b = TestClient::connect("e2e_as_undo_recon_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -132,11 +124,7 @@ async fn test_cross_collection_mutations() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_as_cross_coll_a").await?; let client_b = TestClient::connect("e2e_as_cross_coll_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -192,16 +180,8 @@ async fn test_star_received_message() -> anyhow::Result<()> { let client_a = TestClient::connect("e2e_as_star_recv_a").await?; let mut client_b = TestClient::connect("e2e_as_star_recv_b").await?; - let jid_a = client_a - .client - .get_pn() - .expect("A should have JID") - .to_non_ad(); - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_a = client_a.client.pn().expect("A should have JID").to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_b.wait_for_app_state_sync().await?; @@ -260,8 +240,8 @@ async fn test_multi_device_app_state_sync() -> anyhow::Result<()> { let mut client_a2 = TestClient::connect_as("e2e_multidev_a2", &push_name).await?; // Verify both devices got the same phone number - let phone_a1 = client_a1.client.get_pn().expect("A1 should have JID"); - let phone_a2 = client_a2.client.get_pn().expect("A2 should have JID"); + let phone_a1 = client_a1.client.pn().expect("A1 should have JID"); + let phone_a2 = client_a2.client.pn().expect("A2 should have JID"); assert_eq!( phone_a1.user, phone_a2.user, "Both devices should share the same phone number" @@ -331,7 +311,7 @@ async fn test_missing_key_request_rebuilds_primary_session() -> anyhow::Result<( let primary = client_a.jid().await; let sibling = client_b .client - .get_pn() + .pn() .ok_or_else(|| anyhow::anyhow!("sibling PN missing after connect"))?; assert_ne!( sibling.device, 0, @@ -359,7 +339,7 @@ async fn test_missing_key_request_rebuilds_primary_session() -> anyhow::Result<( ); let requester_lid = client_a .client - .get_lid() + .lid() .ok_or_else(|| anyhow::anyhow!("requester LID missing after connect"))?; let sibling_share = client_b.client.wait_for_sent_node( NodeFilter::tag("message") @@ -420,11 +400,7 @@ async fn test_rapid_successive_mutations() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_as_rapid_a").await?; let client_b = TestClient::connect("e2e_as_rapid_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; diff --git a/tests/e2e/tests/chat_actions.rs b/tests/e2e/tests/chat_actions.rs index 143c65b5e..f5b312f6d 100644 --- a/tests/e2e/tests/chat_actions.rs +++ b/tests/e2e/tests/chat_actions.rs @@ -17,11 +17,7 @@ async fn test_archive_chat() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_archive_a").await?; let client_b = TestClient::connect("e2e_archive_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -44,11 +40,7 @@ async fn test_unarchive_chat() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_unarchive_a").await?; let client_b = TestClient::connect("e2e_unarchive_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -81,11 +73,7 @@ async fn test_pin_chat() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_pin_a").await?; let client_b = TestClient::connect("e2e_pin_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -104,11 +92,7 @@ async fn test_unpin_chat() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_unpin_a").await?; let client_b = TestClient::connect("e2e_unpin_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -132,11 +116,7 @@ async fn test_mute_chat_indefinite() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_mute_indef_a").await?; let client_b = TestClient::connect("e2e_mute_indef_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -155,11 +135,7 @@ async fn test_mute_chat_with_expiry() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_mute_expiry_a").await?; let client_b = TestClient::connect("e2e_mute_expiry_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -185,11 +161,7 @@ async fn test_unmute_chat() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_unmute_a").await?; let client_b = TestClient::connect("e2e_unmute_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -214,11 +186,7 @@ async fn test_star_message() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_star_a").await?; let client_b = TestClient::connect("e2e_star_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -256,11 +224,7 @@ async fn test_unstar_message() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_unstar_a").await?; let client_b = TestClient::connect("e2e_unstar_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -305,11 +269,7 @@ async fn test_multiple_chat_actions() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_multi_actions_a").await?; let client_b = TestClient::connect("e2e_multi_actions_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -355,11 +315,7 @@ async fn test_mark_chat_as_read() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_mark_read_a").await?; let client_b = TestClient::connect("e2e_mark_read_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -389,11 +345,7 @@ async fn test_delete_chat() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_delete_chat_a").await?; let client_b = TestClient::connect("e2e_delete_chat_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; @@ -416,11 +368,7 @@ async fn test_delete_message_for_me() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_del_msg_me_a").await?; let client_b = TestClient::connect("e2e_del_msg_me_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); client_a.wait_for_app_state_sync().await?; diff --git a/tests/e2e/tests/community.rs b/tests/e2e/tests/community.rs index 03a0c3ee5..43e90bd47 100644 --- a/tests/e2e/tests/community.rs +++ b/tests/e2e/tests/community.rs @@ -380,12 +380,12 @@ async fn test_community_join_subgroup() -> anyhow::Result<()> { let jid_b_pn = client_b .client - .get_pn() + .pn() .expect("Client B should have a PN JID") .to_non_ad(); let jid_b_lid = client_b .client - .get_lid() + .lid() .expect("Client B should have a LID JID") .to_non_ad(); @@ -487,14 +487,10 @@ async fn test_community_get_linked_groups_participants() -> anyhow::Result<()> { .get_linked_groups_participants(&community.metadata.id) .await?; - let own_pn = client - .client - .get_pn() - .expect("should have PN JID") - .to_non_ad(); + let own_pn = client.client.pn().expect("should have PN JID").to_non_ad(); let own_lid = client .client - .get_lid() + .lid() .expect("should have LID JID") .to_non_ad(); diff --git a/tests/e2e/tests/groups.rs b/tests/e2e/tests/groups.rs index 199b7f84d..9e8e1c258 100644 --- a/tests/e2e/tests/groups.rs +++ b/tests/e2e/tests/groups.rs @@ -744,7 +744,7 @@ async fn test_query_info_populates_lid_pn_cache_for_participants() -> anyhow::Re let jid_b_pn = client_b.jid().await; let jid_b_lid = client_b .client - .get_lid() + .lid() .expect("B must have a LID after pairing") .to_non_ad(); info!("B pn={jid_b_pn} lid={jid_b_lid}"); diff --git a/tests/e2e/tests/lid_sessions.rs b/tests/e2e/tests/lid_sessions.rs index f35f47ab4..03041bb0c 100644 --- a/tests/e2e/tests/lid_sessions.rs +++ b/tests/e2e/tests/lid_sessions.rs @@ -104,8 +104,8 @@ async fn test_sessions_stored_under_lid_not_pn() -> anyhow::Result<()> { let jid_a = client_a.jid().await; let jid_b = client_b.jid().await; - let lid_a = client_a.client.get_lid().expect("A should have LID"); - let lid_b = client_b.client.get_lid().expect("B should have LID"); + let lid_a = client_a.client.lid().expect("A should have LID"); + let lid_b = client_b.client.lid().expect("B should have LID"); // Roundtrip to establish sessions in both directions send_and_expect_text( @@ -169,7 +169,7 @@ async fn test_multiple_sends_stay_lid_only() -> anyhow::Result<()> { let mut client_b = TestClient::connect("e2e_lid_multi_send_b").await?; let jid_b = client_b.jid().await; - let lid_b = client_b.client.get_lid().expect("B should have LID"); + let lid_b = client_b.client.lid().expect("B should have LID"); // Send 5 messages sequentially for i in 1..=5 { @@ -216,7 +216,7 @@ async fn test_stale_pn_session_does_not_break_lid_messaging() -> anyhow::Result< let jid_a = client_a.jid().await; let jid_b = client_b.jid().await; - let lid_b = client_b.client.get_lid().expect("B should have LID"); + let lid_b = client_b.client.lid().expect("B should have LID"); // First, establish a normal LID session via roundtrip send_and_expect_text( @@ -324,7 +324,7 @@ async fn test_lid_session_survives_reconnect() -> anyhow::Result<()> { let jid_a = client_a.jid().await; let jid_b = client_b.jid().await; - let lid_b = client_b.client.get_lid().expect("B should have LID"); + let lid_b = client_b.client.lid().expect("B should have LID"); // Establish sessions with a roundtrip send_and_expect_text( @@ -401,11 +401,11 @@ async fn test_own_device_0_has_lid_session_after_login() -> anyhow::Result<()> { let own_pn = client .client - .get_pn() + .pn() .expect("Client should have PN after connect"); let own_lid = client .client - .get_lid() + .lid() .expect("Client should have LID after connect"); let backend = client.client.persistence_manager().backend(); @@ -507,7 +507,7 @@ async fn test_pn_only_session_causes_undecryptable_on_lid_lookup() -> anyhow::Re let jid_a = client_a.jid().await; let jid_b = client_b.jid().await; - let lid_b = client_b.client.get_lid().expect("B should have LID"); + let lid_b = client_b.client.lid().expect("B should have LID"); // Step 1: Establish sessions via roundtrip send_and_expect_text(&client_a.client, &mut client_b, &jid_b, "Setup A->B", 30).await?; @@ -626,7 +626,7 @@ async fn test_inbound_1x1_session_keyed_at_companion_device() -> anyhow::Result< let mut client_b = TestClient::connect("e2e_lid_dev_b").await?; let jid_a = client_a.jid().await; let jid_b = client_b.jid().await; - let lid_b = client_b.client.get_lid().expect("B should have LID"); + let lid_b = client_b.client.lid().expect("B should have LID"); send_and_expect_text(&client_a.client, &mut client_b, &jid_b, "hi", 30).await?; send_and_expect_text(&client_b.client, &mut client_a, &jid_a, "reply", 30).await?; @@ -660,7 +660,7 @@ async fn test_pn_migration_is_durable_across_followup_messages() -> anyhow::Resu let mut client_b = TestClient::connect("e2e_lid_dur_b").await?; let jid_a = client_a.jid().await; let jid_b = client_b.jid().await; - let lid_b = client_b.client.get_lid().expect("B should have LID"); + let lid_b = client_b.client.lid().expect("B should have LID"); send_and_expect_text(&client_a.client, &mut client_b, &jid_b, "setup", 30).await?; send_and_expect_text(&client_b.client, &mut client_a, &jid_a, "setup reply", 30).await?; diff --git a/tests/e2e/tests/media.rs b/tests/e2e/tests/media.rs index 44b44bd84..11827024d 100644 --- a/tests/e2e/tests/media.rs +++ b/tests/e2e/tests/media.rs @@ -387,11 +387,7 @@ async fn test_send_image_message() -> anyhow::Result<()> { let client_a = TestClient::connect("e2e_send_img_a").await?; let mut client_b = TestClient::connect("e2e_send_img_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); // A uploads an image let original = b"Image bytes sent from A to B".to_vec(); @@ -450,11 +446,7 @@ async fn test_send_video_message() -> anyhow::Result<()> { let client_a = TestClient::connect("e2e_send_vid_a").await?; let mut client_b = TestClient::connect("e2e_send_vid_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); let original = vec![0xBB; 64]; let upload = client_a @@ -496,11 +488,7 @@ async fn test_send_document_message() -> anyhow::Result<()> { let client_a = TestClient::connect("e2e_send_doc_a").await?; let mut client_b = TestClient::connect("e2e_send_doc_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); let original = b"Important document content".to_vec(); let upload = client_a @@ -544,11 +532,7 @@ async fn test_send_audio_message() -> anyhow::Result<()> { let client_a = TestClient::connect("e2e_send_aud_a").await?; let mut client_b = TestClient::connect("e2e_send_aud_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); let original = vec![0xCC; 64]; let upload = client_a @@ -589,11 +573,7 @@ async fn test_send_ptt_voice_message() -> anyhow::Result<()> { let client_a = TestClient::connect("e2e_send_ptt_a").await?; let mut client_b = TestClient::connect("e2e_send_ptt_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); let original = vec![0xDD; 64]; let upload = client_a @@ -637,16 +617,8 @@ async fn test_send_image_bidirectional() -> anyhow::Result<()> { let mut client_a = TestClient::connect("e2e_bidir_img_a").await?; let mut client_b = TestClient::connect("e2e_bidir_img_b").await?; - let jid_a = client_a - .client - .get_pn() - .expect("A should have JID") - .to_non_ad(); - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_a = client_a.client.pn().expect("A should have JID").to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); // A -> B: send image let data_a = b"Image from A to B".to_vec(); @@ -708,11 +680,7 @@ async fn test_send_multiple_media_types() -> anyhow::Result<()> { let client_a = TestClient::connect("e2e_multi_media_a").await?; let mut client_b = TestClient::connect("e2e_multi_media_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); // Send image let img_data = b"image data for multi-type test".to_vec(); @@ -865,11 +833,7 @@ async fn test_send_image_no_caption() -> anyhow::Result<()> { let client_a = TestClient::connect("e2e_img_nocap_a").await?; let mut client_b = TestClient::connect("e2e_img_nocap_b").await?; - let jid_b = client_b - .client - .get_pn() - .expect("B should have JID") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have JID").to_non_ad(); let original = b"Image without caption".to_vec(); let upload = client_a diff --git a/tests/e2e/tests/memory_soak.rs b/tests/e2e/tests/memory_soak.rs index e2e87b2dd..418220e9d 100644 --- a/tests/e2e/tests/memory_soak.rs +++ b/tests/e2e/tests/memory_soak.rs @@ -327,8 +327,8 @@ async fn test_heavy_dm_soak() -> anyhow::Result<()> { let mut client_a = TestClient::connect("soak2_dm_a").await?; let mut client_b = TestClient::connect("soak2_dm_b").await?; - let jid_a = client_a.client.get_pn().expect("A JID").to_non_ad(); - let jid_b = client_b.client.get_pn().expect("B JID").to_non_ad(); + let jid_a = client_a.client.pn().expect("A JID").to_non_ad(); + let jid_b = client_b.client.pn().expect("B JID").to_non_ad(); // Warm-up for i in 0..5 { @@ -389,8 +389,8 @@ async fn test_heavy_group_soak() -> anyhow::Result<()> { let mut client_b = TestClient::connect("soak2_grp_b").await?; let mut client_c = TestClient::connect("soak2_grp_c").await?; - let jid_b = client_b.client.get_pn().expect("B JID").to_non_ad(); - let jid_c = client_c.client.get_pn().expect("C JID").to_non_ad(); + let jid_b = client_b.client.pn().expect("B JID").to_non_ad(); + let jid_c = client_c.client.pn().expect("C JID").to_non_ad(); // Create group 1: A + B + C let g1 = client_a @@ -533,9 +533,9 @@ async fn test_heavy_mixed_soak() -> anyhow::Result<()> { let mut client_b = TestClient::connect("soak2_mix_b").await?; let mut client_c = TestClient::connect("soak2_mix_c").await?; - let jid_a = client_a.client.get_pn().expect("A JID").to_non_ad(); - let jid_b = client_b.client.get_pn().expect("B JID").to_non_ad(); - let jid_c = client_c.client.get_pn().expect("C JID").to_non_ad(); + let jid_a = client_a.client.pn().expect("A JID").to_non_ad(); + let jid_b = client_b.client.pn().expect("B JID").to_non_ad(); + let jid_c = client_c.client.pn().expect("C JID").to_non_ad(); // Create group let group_jid = client_a @@ -679,7 +679,7 @@ async fn test_many_peers_soak() -> anyhow::Result<()> { let mut peer_jids: Vec = Vec::new(); for i in 0..num_peers { let peer = TestClient::connect(&format!("soak2_peers_p{i}")).await?; - let jid = peer.client.get_pn().expect("peer JID").to_non_ad(); + let jid = peer.client.pn().expect("peer JID").to_non_ad(); peer_jids.push(jid); peers.push(peer); } @@ -747,8 +747,8 @@ async fn test_heavy_reconnect_soak() -> anyhow::Result<()> { let mut client_a = TestClient::connect("soak2_recon_a").await?; let mut client_b = TestClient::connect("soak2_recon_b").await?; - let jid_a = client_a.client.get_pn().expect("A JID").to_non_ad(); - let jid_b = client_b.client.get_pn().expect("B JID").to_non_ad(); + let jid_a = client_a.client.pn().expect("A JID").to_non_ad(); + let jid_b = client_b.client.pn().expect("B JID").to_non_ad(); let mut snaps: Vec = Vec::new(); snaps.push(snapshot("A-recon", 0, &client_a.client).await); diff --git a/tests/e2e/tests/prekey_sessions.rs b/tests/e2e/tests/prekey_sessions.rs index 9c033126e..e28454722 100644 --- a/tests/e2e/tests/prekey_sessions.rs +++ b/tests/e2e/tests/prekey_sessions.rs @@ -41,7 +41,7 @@ async fn test_prekey_collision_regression() -> anyhow::Result<()> { let mut recipient = TestClient::connect("e2e_pkcol_recv").await?; let recipient_jid = recipient .client - .get_pn() + .pn() .expect("Recipient should have a JID") .to_non_ad(); info!("Recipient JID: {recipient_jid}"); diff --git a/tests/e2e/tests/privacy_tokens.rs b/tests/e2e/tests/privacy_tokens.rs index 22b00eed0..1ec2d0acb 100644 --- a/tests/e2e/tests/privacy_tokens.rs +++ b/tests/e2e/tests/privacy_tokens.rs @@ -55,10 +55,7 @@ async fn send_message_and_expect_463_with_id( .send_message_with_options( recipient_jid.clone(), text_msg(text), - SendOptions { - message_id: Some(msg_id.clone()), - ..Default::default() - }, + SendOptions::default().with_message_id(msg_id.clone()), ) .await? .message_id; @@ -126,7 +123,7 @@ async fn test_issue_tokens_api_delivers_notification_and_updates_index() -> anyh let jid_b_lid = client_b .client - .get_lid() + .lid() .expect("B should have LID after connect"); let issued = client_a .client @@ -239,8 +236,8 @@ async fn test_tc_token_notification_reaches_all_connected_devices() -> anyhow::R let mut client_b1 = TestClient::connect_as("e2e_tctok_multi_b1", &shared_b_name).await?; let client_b2 = TestClient::connect_as("e2e_tctok_multi_b2", &shared_b_name).await?; - let phone_b1 = client_b1.client.get_pn().expect("B1 should have JID"); - let phone_b2 = client_b2.client.get_pn().expect("B2 should have JID"); + let phone_b1 = client_b1.client.pn().expect("B1 should have JID"); + let phone_b2 = client_b2.client.pn().expect("B2 should have JID"); assert_eq!( phone_b1.user, phone_b2.user, "B devices should share a phone" @@ -398,7 +395,7 @@ async fn test_only_nct_send_ab_without_salt_still_receives_463() -> anyhow::Resu let jid_a_lid = client_a .client - .get_lid() + .lid() .expect("restricted recipient should have a LID"); let msg_id = format!("E2ECSNEG1{}", uuid::Uuid::new_v4().simple()); let sent_msg_id = msg_id.clone(); @@ -416,7 +413,7 @@ async fn test_only_nct_send_ab_without_salt_still_receives_463() -> anyhow::Resu .map_err(|_| anyhow::anyhow!("Timed out waiting for sent message node"))? .map_err(|_| anyhow::anyhow!("sent message waiter was canceled"))?; // A 1:1 `to` carries the recipient's BARE LID (device-stripped); the device is - // addressed per-recipient in the enc fan-out, never on `to`. `get_lid()` returns + // addressed per-recipient in the enc fan-out, never on `to`. `lid()` returns // the account's own device-suffixed LID (e.g. `:33`, matching the real WA // ``), so compare against the non-AD form. assert_eq!( @@ -461,7 +458,7 @@ async fn test_send_and_syncd_ab_without_delivery_still_receives_463() -> anyhow: let jid_a_lid = client_a .client - .get_lid() + .lid() .expect("restricted recipient should have a LID"); let msg_id = format!("E2ECSNEG2{}", uuid::Uuid::new_v4().simple()); let sent_msg_id = msg_id.clone(); @@ -479,7 +476,7 @@ async fn test_send_and_syncd_ab_without_delivery_still_receives_463() -> anyhow: .map_err(|_| anyhow::anyhow!("Timed out waiting for sent message node"))? .map_err(|_| anyhow::anyhow!("sent message waiter was canceled"))?; // A 1:1 `to` carries the recipient's BARE LID (device-stripped); the device is - // addressed per-recipient in the enc fan-out, never on `to`. `get_lid()` returns + // addressed per-recipient in the enc fan-out, never on `to`. `lid()` returns // the account's own device-suffixed LID (e.g. `:33`, matching the real WA // ``), so compare against the non-AD form. assert_eq!( @@ -525,7 +522,7 @@ async fn test_history_sync_nct_salt_enables_cstoken_first_contact() -> anyhow::R let jid_a_lid = client_a .client - .get_lid() + .lid() .expect("restricted recipient should have a LID"); let sent_waiter = client_b.next_sent_message_waiter(); client_b @@ -533,10 +530,8 @@ async fn test_history_sync_nct_salt_enables_cstoken_first_contact() -> anyhow::R .send_message_with_options( jid_a_lid, text_msg("history-sync cstoken first contact"), - SendOptions { - message_id: Some(format!("E2ECSHIST{}", uuid::Uuid::new_v4().simple())), - ..Default::default() - }, + SendOptions::default() + .with_message_id(format!("E2ECSHIST{}", uuid::Uuid::new_v4().simple())), ) .await?; let sent = tokio::time::timeout(tokio::time::Duration::from_secs(10), sent_waiter) @@ -591,7 +586,7 @@ async fn test_cstoken_only_first_contact_succeeds_when_tctoken_disabled() -> any let jid_a_lid = client_a .client - .get_lid() + .lid() .expect("restricted recipient should have a LID"); let sent_waiter = client_b.next_sent_message_waiter(); client_b @@ -599,10 +594,8 @@ async fn test_cstoken_only_first_contact_succeeds_when_tctoken_disabled() -> any .send_message_with_options( jid_a_lid, text_msg("cstoken-only first contact"), - SendOptions { - message_id: Some(format!("E2ECSONLY{}", uuid::Uuid::new_v4().simple())), - ..Default::default() - }, + SendOptions::default() + .with_message_id(format!("E2ECSONLY{}", uuid::Uuid::new_v4().simple())), ) .await?; let sent = tokio::time::timeout(tokio::time::Duration::from_secs(10), sent_waiter) @@ -651,7 +644,7 @@ async fn test_syncd_nct_salt_enables_cstoken_first_contact() -> anyhow::Result<( let jid_a_lid = client_a .client - .get_lid() + .lid() .expect("restricted recipient should have a LID"); let sent_waiter = client_b.next_sent_message_waiter(); client_b @@ -659,10 +652,8 @@ async fn test_syncd_nct_salt_enables_cstoken_first_contact() -> anyhow::Result<( .send_message_with_options( jid_a_lid, text_msg("syncd cstoken first contact"), - SendOptions { - message_id: Some(format!("E2ECSSYN{}", uuid::Uuid::new_v4().simple())), - ..Default::default() - }, + SendOptions::default() + .with_message_id(format!("E2ECSSYN{}", uuid::Uuid::new_v4().simple())), ) .await?; let sent = tokio::time::timeout(tokio::time::Duration::from_secs(10), sent_waiter) @@ -708,7 +699,7 @@ async fn test_clearing_nct_salt_locally_makes_first_contact_fail_again() -> anyh let jid_a_lid = client_a .client - .get_lid() + .lid() .expect("restricted recipient should have a LID"); client_b .client @@ -737,7 +728,7 @@ async fn test_clearing_nct_salt_locally_makes_first_contact_fail_again() -> anyh let mut client_c = TestClient::connect_as("e2e_cstok_remove_c", &restricted_name_c).await?; let jid_c_lid = client_c .client - .get_lid() + .lid() .expect("restricted recipient should have a LID"); send_first_message_and_expect_463( &client_b, @@ -789,10 +780,8 @@ async fn test_tctoken_only_reply_succeeds_when_cstoken_disabled() -> anyhow::Res .send_message_with_options( jid_a.clone(), text_msg("tctoken-only reply"), - SendOptions { - message_id: Some(format!("E2ETCONLY{}", uuid::Uuid::new_v4().simple())), - ..Default::default() - }, + SendOptions::default() + .with_message_id(format!("E2ETCONLY{}", uuid::Uuid::new_v4().simple())), ) .await?; let sent = tokio::time::timeout(tokio::time::Duration::from_secs(10), sent_waiter) @@ -846,7 +835,7 @@ async fn test_nct_salt_survives_reconnect_and_still_allows_first_contact() -> an let jid_a_lid = client_a .client - .get_lid() + .lid() .expect("restricted recipient should have a LID"); let sent_waiter = client_b.next_sent_message_waiter(); client_b @@ -854,10 +843,8 @@ async fn test_nct_salt_survives_reconnect_and_still_allows_first_contact() -> an .send_message_with_options( jid_a_lid, text_msg("reconnect cstoken first contact"), - SendOptions { - message_id: Some(format!("E2ECSRECON{}", uuid::Uuid::new_v4().simple())), - ..Default::default() - }, + SendOptions::default() + .with_message_id(format!("E2ECSRECON{}", uuid::Uuid::new_v4().simple())), ) .await?; let sent = tokio::time::timeout(tokio::time::Duration::from_secs(10), sent_waiter) @@ -928,10 +915,7 @@ async fn test_pn_target_first_contact_uses_cstoken_after_lid_resolution() -> any .send_message_with_options( jid_a_pn.clone(), text_msg("pn-target cstoken first contact"), - SendOptions { - message_id: Some(msg_id), - ..Default::default() - }, + SendOptions::default().with_message_id(msg_id), ) .await?; let sent = tokio::time::timeout(tokio::time::Duration::from_secs(10), sent_waiter) diff --git a/tests/e2e/tests/profile.rs b/tests/e2e/tests/profile.rs index dadbffe32..e092f4df2 100644 --- a/tests/e2e/tests/profile.rs +++ b/tests/e2e/tests/profile.rs @@ -40,7 +40,7 @@ async fn test_set_push_name() -> anyhow::Result<()> { // The push name mutation requires encryption keys from the critical_block sync. client.wait_for_app_state_sync().await?; - let old_name = client.client.get_push_name(); + let old_name = client.client.push_name(); info!("Current push name: '{}'", old_name); let new_name = "TestBot 🤖"; @@ -48,7 +48,7 @@ async fn test_set_push_name() -> anyhow::Result<()> { client.client.profile().set_push_name(new_name).await?; // Verify it was updated locally - let updated_name = client.client.get_push_name(); + let updated_name = client.client.push_name(); assert_eq!( updated_name, new_name, "Push name should be updated locally" @@ -60,7 +60,7 @@ async fn test_set_push_name() -> anyhow::Result<()> { info!("Setting push name again to '{}'...", second_name); client.client.profile().set_push_name(second_name).await?; - let final_name = client.client.get_push_name(); + let final_name = client.client.push_name(); assert_eq!( final_name, second_name, "Push name should be updated to second value" @@ -166,7 +166,7 @@ async fn test_set_push_name_special_characters() -> anyhow::Result<()> { let name_emoji = "Bot 🤖🦀"; info!("Setting push name with emoji: '{}'...", name_emoji); client.client.profile().set_push_name(name_emoji).await?; - let result = client.client.get_push_name(); + let result = client.client.push_name(); assert_eq!(result, name_emoji, "Push name should support emoji"); info!("Emoji push name set successfully"); @@ -174,7 +174,7 @@ async fn test_set_push_name_special_characters() -> anyhow::Result<()> { let name_russian = "Тест"; info!("Setting push name with Russian: '{}'...", name_russian); client.client.profile().set_push_name(name_russian).await?; - let result = client.client.get_push_name(); + let result = client.client.push_name(); assert_eq!(result, name_russian, "Push name should support Cyrillic"); info!("Russian push name set successfully"); @@ -182,7 +182,7 @@ async fn test_set_push_name_special_characters() -> anyhow::Result<()> { let name_mixed = "Test™ User©"; info!("Setting push name with special chars: '{}'...", name_mixed); client.client.profile().set_push_name(name_mixed).await?; - let result = client.client.get_push_name(); + let result = client.client.push_name(); assert_eq!( result, name_mixed, "Push name should support special characters" @@ -206,7 +206,7 @@ async fn test_set_push_name_long() -> anyhow::Result<()> { let long_name = "A".repeat(25); info!("Setting push name with {} characters...", long_name.len()); client.client.profile().set_push_name(&long_name).await?; - let result = client.client.get_push_name(); + let result = client.client.push_name(); assert_eq!(result, long_name, "Push name should support 25 characters"); info!("Long push name set successfully"); @@ -233,7 +233,7 @@ async fn test_set_push_name_whitespace_only() -> anyhow::Result<()> { .profile() .set_push_name(whitespace_name) .await?; - let result = client.client.get_push_name(); + let result = client.client.push_name(); assert_eq!( result, whitespace_name, "Whitespace-only push name should be accepted (only empty is rejected)" @@ -264,7 +264,7 @@ async fn test_status_text_notification_received() -> anyhow::Result<()> { let jid_a = client_a .client - .get_pn() + .pn() .expect("Client A should have a JID") .to_non_ad(); @@ -304,7 +304,7 @@ async fn test_set_push_name_persists_across_operations() -> anyhow::Result<()> { let push_name = "PersistBot"; info!("Setting push name to '{}'...", push_name); client.client.profile().set_push_name(push_name).await?; - let result = client.client.get_push_name(); + let result = client.client.push_name(); assert_eq!(result, push_name); info!("Push name set successfully"); @@ -318,7 +318,7 @@ async fn test_set_push_name_persists_across_operations() -> anyhow::Result<()> { info!("Status text set successfully"); // Verify push name is still correct - let after_status = client.client.get_push_name(); + let after_status = client.client.push_name(); assert_eq!( after_status, push_name, "Push name should persist after setting status text" diff --git a/tests/e2e/tests/profile_picture.rs b/tests/e2e/tests/profile_picture.rs index 55fba402f..abacdbd1c 100644 --- a/tests/e2e/tests/profile_picture.rs +++ b/tests/e2e/tests/profile_picture.rs @@ -20,10 +20,7 @@ async fn test_set_profile_picture() -> anyhow::Result<()> { info!("Profile picture set successfully, id={}", response.id); // Verify the picture can be retrieved - let own_jid = client - .client - .get_pn() - .expect("should have PN after pairing"); + let own_jid = client.client.pn().expect("should have PN after pairing"); info!("Fetching profile picture for own JID: {}", own_jid); let pic = client .client @@ -92,10 +89,7 @@ async fn test_set_profile_picture_then_update() -> anyhow::Result<()> { assert_ne!(resp1.id, resp2.id, "Picture IDs should differ after update"); // Verify the updated picture is returned - let own_jid = client - .client - .get_pn() - .expect("should have PN after pairing"); + let own_jid = client.client.pn().expect("should have PN after pairing"); let pic = client .client .contacts() @@ -134,10 +128,7 @@ async fn test_remove_profile_picture() -> anyhow::Result<()> { info!("Profile picture removed successfully"); // Verify we receive a PictureUpdate event with removed=true - let own_jid = client - .client - .get_pn() - .expect("should have PN after pairing"); + let own_jid = client.client.pn().expect("should have PN after pairing"); let expected_jid = own_jid.to_non_ad(); let event = client .wait_for_event( @@ -176,10 +167,7 @@ async fn test_get_nonexistent_profile_picture() -> anyhow::Result<()> { let client = TestClient::connect("e2e_get_no_ppic").await?; // Query own picture without ever setting one — should return None - let own_jid = client - .client - .get_pn() - .expect("should have PN after pairing"); + let own_jid = client.client.pn().expect("should have PN after pairing"); let pic = client .client .contacts() @@ -210,11 +198,7 @@ async fn test_get_contact_profile_picture() -> anyhow::Result<()> { .await?; // Client A fetches Client B's profile picture - let jid_b = client_b - .client - .get_pn() - .expect("B should have PN") - .to_non_ad(); + let jid_b = client_b.client.pn().expect("B should have PN").to_non_ad(); let pic = client_a .client .contacts() @@ -249,10 +233,7 @@ async fn test_get_profile_picture_preview_and_full() -> anyhow::Result<()> { .wait_for_event(10, |e| matches!(e, Event::PictureUpdate(_))) .await?; - let own_jid = client - .client - .get_pn() - .expect("should have PN after pairing"); + let own_jid = client.client.pn().expect("should have PN after pairing"); // Fetch preview let preview = client diff --git a/tests/e2e/tests/receipts.rs b/tests/e2e/tests/receipts.rs index 6406a7305..a4bdfb4ef 100644 --- a/tests/e2e/tests/receipts.rs +++ b/tests/e2e/tests/receipts.rs @@ -383,10 +383,7 @@ async fn test_delivery_receipts_flushed_on_disconnect() -> anyhow::Result<()> { .send_message_with_options( jid_b.clone(), text_msg(&text), - SendOptions { - message_id: Some(id.clone()), - ..Default::default() - }, + SendOptions::default().with_message_id(id.clone()), ) .await? .message_id; diff --git a/tests/e2e/tests/retry_dm_multidevice.rs b/tests/e2e/tests/retry_dm_multidevice.rs index 14889f802..67434b33c 100644 --- a/tests/e2e/tests/retry_dm_multidevice.rs +++ b/tests/e2e/tests/retry_dm_multidevice.rs @@ -78,10 +78,7 @@ async fn test_dm_retry_recovers_after_session_deletion() -> anyhow::Result<()> { .send_message_with_options( jid_b.clone(), text_msg("retry-recover"), - SendOptions { - message_id: Some(message_id.clone()), - ..Default::default() - }, + SendOptions::default().with_message_id(message_id.clone()), ) .await?; diff --git a/tests/e2e/tests/session_reuse.rs b/tests/e2e/tests/session_reuse.rs index 5e07dcf45..6da929f02 100644 --- a/tests/e2e/tests/session_reuse.rs +++ b/tests/e2e/tests/session_reuse.rs @@ -77,7 +77,7 @@ async fn test_durable_resume_position_always_covers_spent_counters() -> anyhow:: let client_a = TestClient::connect("e2e_sig_durable_a").await?; let client_b = TestClient::connect("e2e_sig_durable_b").await?; let jid_b = client_b.jid().await; - let lid_b = client_b.client.get_lid(); + let lid_b = client_b.client.lid(); // The first send raises the lease, so its flush is synchronous: the // durable resume position must already be past counter 0 the moment @@ -364,7 +364,7 @@ async fn test_session_state_after_roundtrip() -> anyhow::Result<()> { // LID sessions: the active sessions used by encrypt_for_devices let mut lid_sessions = Vec::new(); - if let Some(lid) = client_b.client.get_lid() { + if let Some(lid) = client_b.client.lid() { lid_sessions = scan_sessions(&*backend, &lid.user, "lid").await?; for (addr, pending) in &lid_sessions { info!("LID session {addr}: pending_pre_key={pending}"); @@ -447,7 +447,7 @@ async fn test_session_persistence() -> anyhow::Result<()> { // PN→LID mapping was resolved before encryption. let mut post_send = scan_sessions(&*backend, &jid_b.user, "c.us").await?; if post_send.is_empty() - && let Some(lid_b) = client_b.client.get_lid() + && let Some(lid_b) = client_b.client.lid() { post_send = scan_sessions(&*backend, &lid_b.user, "lid").await?; } @@ -602,7 +602,7 @@ async fn test_message_info_fields() -> anyhow::Result<()> { // A 1:1 message is LID-addressed on the wire (compliant), so B sees A's LID // as the sender (with the PN carried in sender_pn). Accept either identity. let sender_user = info.source.sender.user.as_str(); - let a_lid = client_a.client.get_lid(); + let a_lid = client_a.client.lid(); assert!( sender_user == jid_a.user.as_str() || a_lid @@ -640,7 +640,7 @@ async fn test_message_info_fields() -> anyhow::Result<()> { assert!(!info.source.is_group); // LID-addressed 1:1 → A sees B's LID as the sender; accept PN or LID. let sender_user = info.source.sender.user.as_str(); - let b_lid = client_b.client.get_lid(); + let b_lid = client_b.client.lid(); assert!( sender_user == jid_b.user.as_str() || b_lid diff --git a/tests/e2e/tests/status.rs b/tests/e2e/tests/status.rs index 00aab97ce..6333608ad 100644 --- a/tests/e2e/tests/status.rs +++ b/tests/e2e/tests/status.rs @@ -31,7 +31,7 @@ async fn status_broadcast_send_is_wa_web_compliant() -> anyhow::Result<()> { let client_b = TestClient::connect("e2e_status_ok_b").await?; let recipient = client_b .client - .get_lid() + .lid() .expect("recipient should have a LID after connect"); let sent_waiter = client_a.next_sent_message_waiter(); diff --git a/wacore/src/iq/groups.rs b/wacore/src/iq/groups.rs index b269fc94f..f71b2cd95 100644 --- a/wacore/src/iq/groups.rs +++ b/wacore/src/iq/groups.rs @@ -2968,12 +2968,12 @@ pub struct AcceptGroupInviteV4Iq { } impl AcceptGroupInviteV4Iq { - pub fn new(group_jid: Jid, code: String, expiration: i64, admin_jid: Jid) -> Self { + pub fn new(group_jid: &Jid, code: &str, expiration: i64, admin_jid: &Jid) -> Self { Self { - group_jid, - code, + group_jid: group_jid.clone(), + code: code.to_string(), expiration, - admin_jid, + admin_jid: admin_jid.clone(), } } } @@ -3335,8 +3335,10 @@ pub struct BatchGetGroupInfoIq { } impl BatchGetGroupInfoIq { - pub fn new(group_jids: Vec) -> Self { - Self { group_jids } + pub fn new(group_jids: &[Jid]) -> Self { + Self { + group_jids: group_jids.to_vec(), + } } } @@ -3434,17 +3436,19 @@ pub struct GetGroupProfilePicturesIq { } impl GetGroupProfilePicturesIq { - pub fn new(group_jids: Vec) -> Self { + pub fn new(group_jids: &[Jid]) -> Self { Self { groups: group_jids - .into_iter() - .map(|jid| (jid, PictureType::Preview)) + .iter() + .map(|jid| (jid.clone(), PictureType::Preview)) .collect(), } } - pub fn with_type(groups: Vec<(Jid, PictureType)>) -> Self { - Self { groups } + pub fn with_type(groups: &[(Jid, PictureType)]) -> Self { + Self { + groups: groups.to_vec(), + } } } @@ -5273,12 +5277,7 @@ mod tests { let code = "A1B2C3D4".to_string(); let expiration: i64 = 1_700_000_123; - let spec = AcceptGroupInviteV4Iq::new( - group_jid.clone(), - code.clone(), - expiration, - admin_jid.clone(), - ); + let spec = AcceptGroupInviteV4Iq::new(&group_jid, &code, expiration, &admin_jid); let iq = spec.build_iq(); assert_eq!(iq.to, group_jid); diff --git a/wacore/src/stanza/call.rs b/wacore/src/stanza/call.rs index 7d26810f1..35fc4eb70 100644 --- a/wacore/src/stanza/call.rs +++ b/wacore/src/stanza/call.rs @@ -1131,7 +1131,7 @@ mod tests { let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap(); let media = call.media.expect("offer captures media"); - // Our own device LID as get_lid() yields it: agent=1. + // Our own device LID as lid() yields it: agent=1. assert_eq!( media .enc_for(Some(&wire_to)) From ba48974dba1435b17b6481add606394309bac007 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 17:37:22 +0000 Subject: [PATCH 2/3] refactor(api): mark SendOptions/EditOptions setters #[must_use] The with_* setters consume self and return Self, so discarding the result silently drops the configured value. --- src/send/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/send/mod.rs b/src/send/mod.rs index c2e0b6308..58d2957a5 100644 --- a/src/send/mod.rs +++ b/src/send/mod.rs @@ -289,36 +289,42 @@ pub struct SendOptions { impl SendOptions { /// See [`SendOptions::message_id`]. + #[must_use] pub fn with_message_id(mut self, message_id: impl Into) -> Self { self.message_id = Some(message_id.into()); self } /// See [`SendOptions::extra_stanza_nodes`]. + #[must_use] pub fn with_extra_stanza_nodes(mut self, nodes: Vec) -> Self { self.extra_stanza_nodes = nodes; self } /// See [`SendOptions::ephemeral_expiration`]. + #[must_use] pub fn with_ephemeral_expiration(mut self, seconds: u32) -> Self { self.ephemeral_expiration = Some(seconds); self } /// See [`SendOptions::stanza_type_override`]. + #[must_use] pub fn with_stanza_type_override(mut self, stanza_type: StanzaType) -> Self { self.stanza_type_override = Some(stanza_type); self } /// See [`SendOptions::group_metadata_freshness`]. + #[must_use] pub fn with_group_metadata_freshness(mut self, freshness: crate::cache::Freshness) -> Self { self.group_metadata_freshness = freshness; self } /// See [`SendOptions::device_freshness`]. + #[must_use] pub fn with_device_freshness(mut self, freshness: crate::cache::Freshness) -> Self { self.device_freshness = freshness; self @@ -347,6 +353,7 @@ pub struct EditOptions { impl EditOptions { /// See [`EditOptions::stanza_id`]. + #[must_use] pub fn with_stanza_id(mut self, stanza_id: impl Into) -> Self { self.stanza_id = Some(stanza_id.into()); self From 511eb7b79fd3517ce768894497efc3755ef9a283 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 17:42:05 +0000 Subject: [PATCH 3/3] docs(groups): correct query_info cache and get_metadata phone-number wording query_info returns a cached entry as-is; the phash request happens only on a miss. get_metadata backfills participant phone numbers on a best-effort basis, so an unmapped LID stays unset. --- src/features/groups.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/features/groups.rs b/src/features/groups.rs index a9049588b..4bd8b617f 100644 --- a/src/features/groups.rs +++ b/src/features/groups.rs @@ -371,10 +371,10 @@ impl<'a> Groups<'a> { /// /// Returns the slim [`GroupInfo`] the encryption path needs: participant /// JIDs, the group's addressing mode, the LID/PN mapping, and whether it is - /// a community announcement group. Results are shared through the group - /// cache and refreshed with the persisted participant phash, so a repeated - /// call is usually free and a stale entry costs a `not-modified` round trip - /// instead of a full metadata download. + /// a community announcement group. A cached entry is returned as-is, so a + /// repeated call is free. Only a cache miss goes to the network, and it + /// sends the persisted participant phash, so an unchanged group costs a + /// `not-modified` answer instead of a full metadata download. /// /// This is the right call for routing and encrypting a message. For the /// user-facing fields (subject, description, admin roles, group settings) @@ -625,9 +625,11 @@ impl<'a> Groups<'a> { /// Fetch the complete, user-facing metadata of a group. /// /// Returns an owned [`GroupMetadata`]: subject, description, creator, - /// per-participant admin roles, ephemeral and membership settings, plus the - /// participants' phone numbers resolved from their LIDs. The query always - /// hits the network (no phash is sent, so the server never answers + /// per-participant admin roles, and ephemeral and membership settings. In a + /// LID-addressed group, participant phone numbers the server left out are + /// backfilled from known LID/PN mappings on a best-effort basis; a + /// participant with no known mapping keeps `phone_number: None`. The query + /// always hits the network (no phash is sent, so the server never answers /// `not-modified`) and the result does not populate the group cache. /// /// This is the right call for displaying or auditing a group. When you only