Skip to main content

whatsapp_rust/features/
profile.rs

1//! Profile management for the user's own account.
2//!
3//! Provides APIs for changing push name (display name) and status text (about).
4
5use crate::client::Client;
6use crate::store::commands::DeviceCommand;
7use anyhow::Result;
8use log::{debug, warn};
9use wacore::iq::contacts::SetProfilePictureSpec;
10use wacore::iq::profile::SetStatusTextSpec;
11use wacore_binary::builder::NodeBuilder;
12
13pub use wacore::iq::contacts::SetProfilePictureResponse;
14
15/// Feature handle for profile operations.
16pub struct Profile<'a> {
17    client: &'a Client,
18}
19
20impl<'a> Profile<'a> {
21    pub(crate) fn new(client: &'a Client) -> Self {
22        Self { client }
23    }
24
25    /// Set the user's status text (about).
26    ///
27    /// Uses the stable IQ-based approach matching WhatsApp Web's `WAWebSetAboutJob`:
28    /// ```xml
29    /// <iq type="set" xmlns="status" to="s.whatsapp.net">
30    ///   <status>Hello world!</status>
31    /// </iq>
32    /// ```
33    ///
34    /// Note: This sets the profile "About" text, not ephemeral text status updates.
35    pub async fn set_status_text(&self, text: &str) -> Result<()> {
36        debug!("Setting status text (length={})", text.len());
37
38        self.client.execute(SetStatusTextSpec::new(text)).await?;
39
40        Ok(())
41    }
42
43    /// Set the user's push name (display name).
44    ///
45    /// Updates the local device store, sends a presence stanza with the new name,
46    /// and propagates the change via app state sync (`setting_pushName` mutation
47    /// in the `critical_block` collection) for cross-device synchronization.
48    ///
49    /// Matches WhatsApp Web's `WAWebPushNameBridge` behavior:
50    /// 1. Send `<presence name="..."/>` immediately (no type attribute)
51    /// 2. Sync via app state mutation to `critical_block` collection
52    ///
53    /// ## Wire Format
54    /// ```xml
55    /// <presence name="New Name"/>
56    /// ```
57    pub async fn set_push_name(&self, name: &str) -> Result<()> {
58        if name.is_empty() {
59            return Err(anyhow::anyhow!("Push name cannot be empty"));
60        }
61
62        debug!("Setting push name (length={})", name.len());
63
64        // Send presence with name only (no type attribute), matching WhatsApp Web's
65        // WASmaxOutPresenceAvailabilityRequest which uses OPTIONAL for type.
66        let node = NodeBuilder::new("presence").attr("name", name).build();
67        self.client.send_node(node).await?;
68
69        // Send app state sync mutation for cross-device propagation.
70        // This writes a `setting_pushName` mutation to the `critical_block` collection,
71        // matching WhatsApp Web's WAWebPushNameBridge behavior.
72        if let Err(e) = self.send_push_name_mutation(name).await {
73            // Non-fatal: the presence was already sent so the name change takes
74            // effect immediately. App state sync may fail if keys aren't available
75            // yet (e.g. right after pairing, before initial sync completes).
76            warn!("Failed to send push name app state mutation: {e}");
77        }
78
79        // Persist only after the network send succeeds
80        self.client
81            .persistence_manager()
82            .process_command(DeviceCommand::SetPushName(name.to_string()))
83            .await;
84
85        Ok(())
86    }
87
88    /// Set the user's own profile picture.
89    ///
90    /// Sends a JPEG image as the new profile picture. The image should already
91    /// be properly sized/cropped by the caller (WhatsApp typically uses 640x640).
92    ///
93    /// ## Wire Format
94    /// ```xml
95    /// <iq type="set" xmlns="w:profile:picture" to="s.whatsapp.net">
96    ///   <picture type="image">{jpeg bytes}</picture>
97    /// </iq>
98    /// ```
99    pub async fn set_profile_picture(
100        &self,
101        image_data: Vec<u8>,
102    ) -> Result<SetProfilePictureResponse> {
103        debug!("Setting profile picture (size={} bytes)", image_data.len());
104        Ok(self
105            .client
106            .execute(SetProfilePictureSpec::set_own(image_data))
107            .await?)
108    }
109
110    /// Remove the user's own profile picture.
111    pub async fn remove_profile_picture(&self) -> Result<SetProfilePictureResponse> {
112        debug!("Removing profile picture");
113        Ok(self
114            .client
115            .execute(SetProfilePictureSpec::remove_own())
116            .await?)
117    }
118
119    /// Build and send the `setting_pushName` app state mutation.
120    async fn send_push_name_mutation(&self, name: &str) -> Result<()> {
121        use rand::Rng;
122        use wacore::appstate::encode::encode_record;
123        use waproto::whatsapp as wa;
124
125        let index = serde_json::to_vec(&["setting_pushName"])?;
126
127        let value = wa::SyncActionValue {
128            push_name_setting: Some(wa::sync_action_value::PushNameSetting {
129                name: Some(name.to_string()),
130            }),
131            timestamp: Some(wacore::time::now_millis()),
132            ..Default::default()
133        };
134
135        // Get the latest sync key for encryption
136        let proc = self.client.get_app_state_processor().await;
137        let key_id = proc
138            .backend
139            .get_latest_sync_key_id()
140            .await
141            .map_err(|e| anyhow::anyhow!(e))?
142            .ok_or_else(|| anyhow::anyhow!("No app state sync key available"))?;
143        let keys = proc.get_app_state_key(&key_id).await?;
144
145        // Generate random IV
146        let mut iv = [0u8; 16];
147        rand::make_rng::<rand::rngs::StdRng>().fill_bytes(&mut iv);
148
149        let (mutation, value_mac) = encode_record(
150            wa::syncd_mutation::SyncdOperation::Set,
151            &index,
152            &value,
153            &keys,
154            &key_id,
155            &iv,
156        );
157
158        self.client
159            .send_app_state_patch("critical_block", vec![(mutation, value_mac)])
160            .await
161    }
162}
163
164impl Client {
165    /// Access profile operations.
166    pub fn profile(&self) -> Profile<'_> {
167        Profile::new(self)
168    }
169}