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