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, ClientError};
6use crate::request::IqError;
7use crate::store::commands::DeviceCommand;
8use anyhow::Result;
9use log::{debug, warn};
10use thiserror::Error;
11use wacore::iq::contacts::SetProfilePictureSpec;
12use wacore::iq::profile::SetStatusTextSpec;
13use wacore_binary::builder::NodeBuilder;
14
15pub use wacore::iq::contacts::SetProfilePictureResponse;
16
17/// Error returned by own-profile operations (push name, status text, picture).
18#[derive(Debug, Error)]
19#[non_exhaustive]
20pub enum ProfileError {
21 /// An IQ to the server failed (status text / profile picture).
22 #[error("{0}")]
23 Iq(#[from] IqError),
24 /// Connection/transport failure sending a stanza (push-name presence).
25 #[error("{0}")]
26 Client(#[from] ClientError),
27 /// A provided argument is invalid (e.g. an empty push name).
28 #[error("invalid argument: {0}")]
29 InvalidArgument(String),
30 /// Catch-all for internal failures with no dedicated variant.
31 #[error("{0}")]
32 Internal(#[from] anyhow::Error),
33}
34
35/// Feature handle for profile operations.
36pub struct Profile<'a> {
37 client: &'a Client,
38}
39
40impl<'a> Profile<'a> {
41 pub(crate) fn new(client: &'a Client) -> Self {
42 Self { client }
43 }
44
45 /// Set the user's status text (about).
46 ///
47 /// Uses the stable IQ-based approach matching WhatsApp Web's `WAWebSetAboutJob`:
48 /// ```xml
49 /// <iq type="set" xmlns="status" to="s.whatsapp.net">
50 /// <status>Hello world!</status>
51 /// </iq>
52 /// ```
53 ///
54 /// Note: This sets the profile "About" text, not ephemeral text status updates.
55 pub async fn set_status_text(&self, text: &str) -> Result<(), ProfileError> {
56 debug!("Setting status text (length={})", text.len());
57
58 self.client.execute(SetStatusTextSpec::new(text)).await?;
59
60 Ok(())
61 }
62
63 /// Set the user's push name (display name).
64 ///
65 /// Updates the local device store, sends a presence stanza with the new name,
66 /// and propagates the change via app state sync (`setting_pushName` mutation
67 /// in the `critical_block` collection) for cross-device synchronization.
68 ///
69 /// Matches WhatsApp Web's `WAWebPushNameBridge` behavior:
70 /// 1. Send `<presence name="..."/>` immediately (no type attribute)
71 /// 2. Sync via app state mutation to `critical_block` collection
72 ///
73 /// ## Wire Format
74 /// ```xml
75 /// <presence name="New Name"/>
76 /// ```
77 pub async fn set_push_name(&self, name: &str) -> Result<(), ProfileError> {
78 if name.is_empty() {
79 return Err(ProfileError::InvalidArgument(
80 "push name cannot be empty".into(),
81 ));
82 }
83
84 debug!("Setting push name (length={})", name.len());
85
86 // Send presence with name only (no type attribute), matching WhatsApp Web's
87 // WASmaxOutPresenceAvailabilityRequest which uses OPTIONAL for type.
88 let node = NodeBuilder::new("presence").attr("name", name).build();
89 self.client.send_node(node).await?;
90
91 // Send app state sync mutation for cross-device propagation.
92 // This writes a `setting_pushName` mutation to the `critical_block` collection,
93 // matching WhatsApp Web's WAWebPushNameBridge behavior.
94 if let Err(e) = self.send_push_name_mutation(name).await {
95 // Non-fatal: the presence was already sent so the name change takes
96 // effect immediately. App state sync may fail if keys aren't available
97 // yet (e.g. right after pairing, before initial sync completes).
98 warn!("Failed to send push name app state mutation: {e}");
99 }
100
101 // Persist only after the network send succeeds
102 self.client
103 .persistence_manager()
104 .process_command(DeviceCommand::SetPushName(name.to_string()))
105 .await;
106
107 Ok(())
108 }
109
110 /// Set the user's own profile picture.
111 ///
112 /// Sends a JPEG image as the new profile picture. The image should already
113 /// be properly sized/cropped by the caller (WhatsApp typically uses 640x640).
114 ///
115 /// Passing empty `image_data` **removes** the picture (matching WhatsApp Web);
116 /// call [`Profile::remove_profile_picture`] when removal is the intent.
117 ///
118 /// ## Wire Format
119 /// ```xml
120 /// <iq type="set" xmlns="w:profile:picture" to="s.whatsapp.net">
121 /// <picture type="image">{jpeg bytes}</picture>
122 /// </iq>
123 /// ```
124 pub async fn set_profile_picture(
125 &self,
126 image_data: Vec<u8>,
127 ) -> Result<SetProfilePictureResponse, ProfileError> {
128 // for_own routes empty bytes to the remove path, matching WA Web; no panic.
129 debug!("Setting profile picture (size={} bytes)", image_data.len());
130 Ok(self
131 .client
132 .execute(SetProfilePictureSpec::for_own(image_data))
133 .await?)
134 }
135
136 /// Remove the user's own profile picture.
137 pub async fn remove_profile_picture(&self) -> Result<SetProfilePictureResponse, ProfileError> {
138 debug!("Removing profile picture");
139 Ok(self
140 .client
141 .execute(SetProfilePictureSpec::remove_own())
142 .await?)
143 }
144
145 /// Build and send the `setting_pushName` app state mutation.
146 async fn send_push_name_mutation(&self, name: &str) -> Result<()> {
147 use wacore::appstate::schemas;
148 use waproto::whatsapp as wa;
149
150 let value = wa::SyncActionValue {
151 push_name_setting: buffa::MessageField::some(wa::sync_action_value::PushNameSetting {
152 name: Some(name.to_string()),
153 }),
154 timestamp: Some(wacore::time::now_millis()),
155 ..Default::default()
156 };
157 // setting_pushName's index has no args (collection/version come from the schema).
158 self.client
159 .send_app_state_action(&schemas::SETTING_PUSH_NAME, &[], &value)
160 .await?;
161 Ok(())
162 }
163}
164
165impl Client {
166 /// Access profile operations.
167 pub fn profile(&self) -> Profile<'_> {
168 Profile::new(self)
169 }
170}