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