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}