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}