Skip to main content

whatsapp_rust/features/
contacts.rs

1//! Contact information feature.
2//!
3//! Profile picture types are defined in `wacore::iq::contacts`.
4//! Usync types are defined in `wacore::iq::usync`.
5
6use crate::client::Client;
7use crate::request::IqError;
8use anyhow::Result;
9use log::debug;
10use std::collections::HashMap;
11use wacore::iq::contacts::{ProfilePictureSpec, ProfilePictureType};
12use wacore::iq::usync::{ContactInfoSpec, IsOnWhatsAppSpec, UserInfoSpec};
13use wacore_binary::jid::{Jid, JidExt};
14
15// Re-export types from wacore
16pub use wacore::iq::contacts::ProfilePicture;
17pub use wacore::iq::usync::{ContactInfo, IsOnWhatsAppResult, UserInfo};
18
19pub struct Contacts<'a> {
20    client: &'a Client,
21}
22
23impl<'a> Contacts<'a> {
24    pub(crate) fn new(client: &'a Client) -> Self {
25        Self { client }
26    }
27
28    pub async fn is_on_whatsapp(&self, phones: &[&str]) -> Result<Vec<IsOnWhatsAppResult>> {
29        if phones.is_empty() {
30            return Ok(Vec::new());
31        }
32
33        debug!("is_on_whatsapp: checking {} numbers", phones.len());
34
35        let request_id = self.client.generate_request_id();
36        let phone_strings: Vec<String> = phones.iter().map(|s| s.to_string()).collect();
37        let spec = IsOnWhatsAppSpec::new(phone_strings, request_id);
38
39        Ok(self.client.execute(spec).await?)
40    }
41
42    pub async fn get_info(&self, phones: &[&str]) -> Result<Vec<ContactInfo>> {
43        if phones.is_empty() {
44            return Ok(Vec::new());
45        }
46
47        debug!("get_info: fetching info for {} numbers", phones.len());
48
49        let request_id = self.client.generate_request_id();
50        let phone_strings: Vec<String> = phones.iter().map(|s| s.to_string()).collect();
51        let spec = ContactInfoSpec::new(phone_strings, request_id);
52
53        Ok(self.client.execute(spec).await?)
54    }
55
56    pub async fn get_profile_picture(
57        &self,
58        jid: &Jid,
59        preview: bool,
60    ) -> Result<Option<ProfilePicture>> {
61        debug!(
62            "get_profile_picture: fetching {} picture for {}",
63            if preview { "preview" } else { "full" },
64            jid
65        );
66
67        let picture_type = if preview {
68            ProfilePictureType::Preview
69        } else {
70            ProfilePictureType::Full
71        };
72        let mut spec = ProfilePictureSpec::new(jid, picture_type);
73
74        // Include tctoken for user JIDs (skip groups, newsletters)
75        if !jid.is_group()
76            && !jid.is_newsletter()
77            && let Some(token) = self.client.lookup_tc_token_for_jid(jid).await
78        {
79            spec = spec.with_tc_token(token);
80        }
81
82        match self.client.execute(spec).await {
83            Ok(pic) => Ok(pic),
84            // 404/401 = no profile picture (or not authorized to see it).
85            // WhatsApp server returns type="error" IQ for these cases.
86            Err(IqError::ServerError { code, .. }) if code == 404 || code == 401 => Ok(None),
87            Err(e) => Err(e.into()),
88        }
89    }
90
91    pub async fn get_user_info(&self, jids: &[Jid]) -> Result<HashMap<Jid, UserInfo>> {
92        if jids.is_empty() {
93            return Ok(HashMap::new());
94        }
95
96        debug!("get_user_info: fetching info for {} JIDs", jids.len());
97
98        let request_id = self.client.generate_request_id();
99        let spec = UserInfoSpec::new(jids.to_vec(), request_id);
100
101        Ok(self.client.execute(spec).await?)
102    }
103}
104
105impl Client {
106    pub fn contacts(&self) -> Contacts<'_> {
107        Contacts::new(self)
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_contact_info_struct() {
117        let jid: Jid = "1234567890@s.whatsapp.net"
118            .parse()
119            .expect("test JID should be valid");
120        let lid: Jid = "12345678@lid".parse().expect("test JID should be valid");
121
122        let info = ContactInfo {
123            jid: jid.clone(),
124            lid: Some(lid.clone()),
125            is_registered: true,
126            is_business: false,
127            status: Some("Hey there!".to_string()),
128            picture_id: Some(123456789),
129        };
130
131        assert!(info.is_registered);
132        assert!(!info.is_business);
133        assert_eq!(info.status, Some("Hey there!".to_string()));
134        assert_eq!(info.picture_id, Some(123456789));
135        assert!(info.lid.is_some());
136    }
137
138    #[test]
139    fn test_profile_picture_struct() {
140        let pic = ProfilePicture {
141            id: "123456789".to_string(),
142            url: "https://example.com/pic.jpg".to_string(),
143            direct_path: Some("/v/pic.jpg".to_string()),
144            hash: None,
145        };
146
147        assert_eq!(pic.id, "123456789");
148        assert_eq!(pic.url, "https://example.com/pic.jpg");
149        assert!(pic.direct_path.is_some());
150    }
151
152    #[test]
153    fn test_is_on_whatsapp_result_struct() {
154        let jid: Jid = "1234567890@s.whatsapp.net"
155            .parse()
156            .expect("test JID should be valid");
157        let result = IsOnWhatsAppResult {
158            jid,
159            is_registered: true,
160        };
161
162        assert!(result.is_registered);
163    }
164}