Skip to main content

whatsapp_rust/client/
iq_ops.rs

1//! IQ-based operations: props, privacy settings, profiles and assorted requests.
2
3use super::*;
4
5impl Client {
6    pub async fn set_passive(&self, passive: bool) -> Result<(), crate::request::IqError> {
7        use wacore::iq::passive::PassiveModeSpec;
8        self.execute(PassiveModeSpec::new(passive)).await
9    }
10
11    pub async fn fetch_props(&self) -> Result<(), crate::request::IqError> {
12        use wacore::iq::props::PropsSpec;
13        use wacore::store::commands::DeviceCommand;
14
15        let stored_hash = self
16            .persistence_manager
17            .get_device_snapshot()
18            .props_hash
19            .clone();
20
21        // Deltas only contain changed props, so they're invalid against an empty cache.
22        let spec = match &stored_hash {
23            Some(hash) if self.ab_props.is_seeded() => {
24                debug!("Fetching props with hash for delta update...");
25                PropsSpec::with_hash(hash)
26            }
27            _ => {
28                debug!("Fetching props (full)...");
29                PropsSpec::new()
30            }
31        };
32
33        let response = self.execute(spec).await?;
34
35        if response.delta_update {
36            debug!(
37                "Props delta update received ({} changed props)",
38                response.experiment_props.len()
39            );
40        } else {
41            debug!(
42                "Props full update received ({} props, hash={:?})",
43                response.experiment_props.len(),
44                response.hash
45            );
46        }
47
48        self.ab_props
49            .apply_props(response.delta_update, response.experiment_props.into_iter())
50            .await;
51        self.latch_lid_migrated_from_props().await;
52
53        if let Some(new_hash) = response.hash {
54            self.persistence_manager
55                .process_command(DeviceCommand::SetPropsHash(Some(new_hash)))
56                .await;
57        }
58
59        Ok(())
60    }
61
62    pub(crate) fn ab_props(&self) -> &wacore::store::ab_props::AbPropsCache {
63        &self.ab_props
64    }
65
66    pub async fn fetch_privacy_settings(
67        &self,
68    ) -> Result<wacore::iq::privacy::PrivacySettingsResponse, crate::request::IqError> {
69        use wacore::iq::privacy::PrivacySettingsSpec;
70
71        debug!("Fetching privacy settings...");
72
73        self.execute(PrivacySettingsSpec::new()).await
74    }
75
76    /// Set a privacy setting.
77    ///
78    /// Use [`PrivacyCategory::is_valid_value`](wacore::iq::privacy::PrivacyCategory::is_valid_value)
79    /// to check valid combinations.
80    ///
81    /// # Example
82    /// ```ignore
83    /// use wacore::iq::privacy::{PrivacyCategory, PrivacyValue};
84    /// client.set_privacy_setting(PrivacyCategory::Last, PrivacyValue::Contacts).await?;
85    /// ```
86    pub async fn set_privacy_setting(
87        &self,
88        category: wacore::iq::privacy::PrivacyCategory,
89        value: wacore::iq::privacy::PrivacyValue,
90    ) -> Result<wacore::iq::privacy::SetPrivacySettingResponse, crate::request::IqError> {
91        use wacore::iq::privacy::SetPrivacySettingSpec;
92        self.execute(SetPrivacySettingSpec::new(category, value))
93            .await
94    }
95
96    /// Set a privacy setting to `contact_blacklist` with a disallowed list update.
97    ///
98    /// Only `Last`, `Profile`, `Status`, `GroupAdd` support disallowed lists.
99    /// Returns the server's updated dhash for use in subsequent updates.
100    pub async fn set_privacy_disallowed_list(
101        &self,
102        category: wacore::iq::privacy::PrivacyCategory,
103        update: wacore::iq::privacy::DisallowedListUpdate,
104    ) -> Result<wacore::iq::privacy::SetPrivacySettingResponse, crate::request::IqError> {
105        use wacore::iq::privacy::SetPrivacySettingSpec;
106        self.execute(SetPrivacySettingSpec::with_disallowed_list(
107            category, update,
108        ))
109        .await
110    }
111
112    /// Set the default disappearing messages duration (seconds). Pass 0 to disable.
113    pub async fn set_default_disappearing_mode(
114        &self,
115        duration: u32,
116    ) -> Result<(), crate::request::IqError> {
117        use wacore::iq::privacy::SetDefaultDisappearingModeSpec;
118        self.execute(SetDefaultDisappearingModeSpec::new(duration))
119            .await
120    }
121
122    /// Turn disappearing messages on or off for a 1:1 chat (`duration` in
123    /// seconds; `0` disables).
124    ///
125    /// Sends an `EPHEMERAL_SETTING` protocol message, mirroring WA Web's
126    /// `WAWebUpdateEphemeralSettingChatAction`. For groups use
127    /// [`Groups::set_ephemeral`](crate::Groups::set_ephemeral); for the account
128    /// default use [`Client::set_default_disappearing_mode`].
129    pub async fn set_chat_disappearing_timer(
130        &self,
131        chat: Jid,
132        duration: u32,
133    ) -> Result<crate::send::SendResult, crate::send::SendError> {
134        // 1:1 only: groups use Groups::set_ephemeral (a separate IQ). Sending the
135        // EPHEMERAL_SETTING body to a group/status/newsletter would produce a
136        // message that does not change the chat's timer, so fail fast instead.
137        if !(chat.is_pn() || chat.is_lid()) {
138            return Err(crate::send::SendError::InvalidRequest(
139                "set_chat_disappearing_timer is 1:1-only; use Groups::set_ephemeral for groups"
140                    .into(),
141            ));
142        }
143        let msg = build_ephemeral_setting_message(duration, wacore::time::now_secs_u64() as i64);
144        self.send_message(chat, msg).await
145    }
146
147    /// Get business profile for a WhatsApp Business account.
148    pub async fn get_business_profile(
149        &self,
150        jid: &Jid,
151    ) -> Result<Option<wacore::iq::business::BusinessProfile>, crate::request::IqError> {
152        use wacore::iq::business::BusinessProfileSpec;
153        self.execute(BusinessProfileSpec::new(jid)).await
154    }
155
156    pub async fn send_digest_key_bundle(&self) -> Result<(), crate::request::IqError> {
157        use wacore::iq::prekeys::DigestKeyBundleSpec;
158
159        debug!("Sending digest key bundle...");
160
161        self.execute(DigestKeyBundleSpec::new()).await.map(|_| ())
162    }
163
164    /// Override `DeviceProps` fields before the initial pairing. Only fields
165    /// with `Some` are changed. In-memory only — WA Web regenerates
166    /// `device_props` at each registration, and it has no wire effect after
167    /// pairing. Call before `connect()` on every process start that still
168    /// needs to pair.
169    pub async fn set_device_props(&self, override_: wacore::store::DevicePropsOverride) {
170        use wacore::store::commands::DeviceCommand;
171        if override_.is_empty() {
172            return;
173        }
174        if self.persistence_manager.get_device_snapshot().pn.is_some() {
175            warn!(
176                target: "Client/DeviceProps",
177                "set_device_props called after pairing — stored but not sent on the wire"
178            );
179        }
180        self.persistence_manager
181            .process_command(DeviceCommand::SetDeviceProps(override_))
182            .await;
183    }
184
185    /// Set the noise-handshake `ClientPayload` profile. In-memory only;
186    /// call before each `connect()` on a fresh process.
187    pub async fn set_client_profile(&self, profile: wacore::client_profile::ClientProfile) {
188        use wacore::store::commands::DeviceCommand;
189        self.persistence_manager
190            .process_command(DeviceCommand::SetClientProfile(profile))
191            .await;
192    }
193}
194
195/// Builds the `EPHEMERAL_SETTING` protocol message that turns a 1:1 chat's
196/// disappearing timer on/off. The timer data lives directly on `ProtocolMessage`
197/// (`ephemeral_expiration`, `ephemeral_setting_timestamp`, `disappearing_mode`);
198/// there is no `ephemeral_setting` field. Timestamp is unix seconds. Mirrors
199/// WA Web's `MsgChatActionUtils` (`disappearingModeInitiator: ChangedInChat`,
200/// `disappearingModeTrigger: Unknown`).
201fn build_ephemeral_setting_message(duration: u32, now_secs: i64) -> waproto::whatsapp::Message {
202    use waproto::whatsapp as wa;
203    wa::Message {
204        protocol_message: buffa::MessageField::some(wa::message::ProtocolMessage {
205            r#type: Some(wa::message::protocol_message::Type::EphemeralSetting),
206            ephemeral_expiration: Some(duration),
207            ephemeral_setting_timestamp: Some(now_secs),
208            disappearing_mode: buffa::MessageField::some(wa::DisappearingMode {
209                initiator: Some(wa::disappearing_mode::Initiator::ChangedInChat),
210                trigger: Some(wa::disappearing_mode::Trigger::Unknown),
211                ..Default::default()
212            }),
213            ..Default::default()
214        }),
215        ..Default::default()
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::build_ephemeral_setting_message;
222    use waproto::whatsapp as wa;
223
224    #[test]
225    fn ephemeral_setting_message_shape() {
226        let msg = build_ephemeral_setting_message(86400, 1_700_000_000);
227        let pm = msg
228            .protocol_message
229            .as_option()
230            .expect("protocol_message set");
231        assert_eq!(
232            pm.r#type,
233            Some(wa::message::protocol_message::Type::EphemeralSetting)
234        );
235        assert_eq!(pm.ephemeral_expiration, Some(86400));
236        assert_eq!(pm.ephemeral_setting_timestamp, Some(1_700_000_000));
237        let dm = pm
238            .disappearing_mode
239            .as_option()
240            .expect("disappearing_mode set");
241        assert_eq!(
242            dm.initiator,
243            Some(wa::disappearing_mode::Initiator::ChangedInChat)
244        );
245        assert_eq!(dm.trigger, Some(wa::disappearing_mode::Trigger::Unknown));
246    }
247
248    #[test]
249    fn ephemeral_setting_disable_uses_zero_duration() {
250        let msg = build_ephemeral_setting_message(0, 1);
251        assert_eq!(
252            msg.protocol_message
253                .as_option()
254                .unwrap()
255                .ephemeral_expiration,
256            Some(0)
257        );
258    }
259}