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, 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}