whatsapp_rust/features/
groups.rs

1use crate::client::Client;
2use crate::request::InfoQuery;
3use std::collections::HashMap;
4use std::sync::LazyLock;
5use wacore::client::context::GroupInfo;
6use wacore_binary::builder::NodeBuilder;
7use wacore_binary::jid::{GROUP_SERVER, Jid};
8use wacore_binary::node::NodeContent;
9
10static G_US_JID: LazyLock<Jid> = LazyLock::new(|| Jid::new("", GROUP_SERVER));
11
12#[derive(Debug, Clone)]
13pub struct GroupMetadata {
14    pub id: Jid,
15    pub subject: String,
16    pub participants: Vec<GroupParticipant>,
17    pub addressing_mode: crate::types::message::AddressingMode,
18}
19
20#[derive(Debug, Clone)]
21pub struct GroupParticipant {
22    pub jid: Jid,
23    pub phone_number: Option<Jid>,
24    pub is_admin: bool,
25}
26
27pub struct Groups<'a> {
28    client: &'a Client,
29}
30
31impl<'a> Groups<'a> {
32    pub(crate) fn new(client: &'a Client) -> Self {
33        Self { client }
34    }
35
36    pub async fn query_info(&self, jid: &Jid) -> Result<GroupInfo, anyhow::Error> {
37        if let Some(cached) = self.client.get_group_cache().await.get(jid).await {
38            return Ok(cached);
39        }
40
41        let query_node = NodeBuilder::new("query")
42            .attr("request", "interactive")
43            .build();
44
45        let iq = InfoQuery::get(
46            "w:g2",
47            jid.clone(),
48            Some(NodeContent::Nodes(vec![query_node])),
49        );
50
51        let resp_node = self.client.send_iq(iq).await?;
52
53        let group_node = resp_node
54            .get_optional_child("group")
55            .ok_or_else(|| anyhow::anyhow!("<group> not found in group info response"))?;
56
57        let mut participants = Vec::new();
58        let mut lid_to_pn_map = HashMap::new();
59
60        let addressing_mode_str = group_node
61            .attrs()
62            .optional_string("addressing_mode")
63            .unwrap_or("pn");
64        let addressing_mode = match addressing_mode_str {
65            "lid" => crate::types::message::AddressingMode::Lid,
66            _ => crate::types::message::AddressingMode::Pn,
67        };
68
69        for participant_node in group_node.get_children_by_tag("participant") {
70            let participant_jid = participant_node.attrs().jid("jid");
71            participants.push(participant_jid.clone());
72
73            if addressing_mode == crate::types::message::AddressingMode::Lid
74                && let Some(phone_number) = participant_node.attrs().optional_jid("phone_number")
75            {
76                lid_to_pn_map.insert(participant_jid.user.clone(), phone_number);
77            }
78        }
79
80        let mut info = GroupInfo::new(participants, addressing_mode);
81        if !lid_to_pn_map.is_empty() {
82            info.set_lid_to_pn_map(lid_to_pn_map);
83        }
84
85        self.client
86            .get_group_cache()
87            .await
88            .insert(jid.clone(), info.clone())
89            .await;
90
91        Ok(info)
92    }
93
94    pub async fn get_participating(&self) -> Result<HashMap<String, GroupMetadata>, anyhow::Error> {
95        let participants_node = NodeBuilder::new("participants").build();
96        let description_node = NodeBuilder::new("description").build();
97        let participating_node = NodeBuilder::new("participating")
98            .children([participants_node, description_node])
99            .build();
100
101        let iq = InfoQuery::get(
102            "w:g2",
103            G_US_JID.clone(),
104            Some(NodeContent::Nodes(vec![participating_node])),
105        );
106
107        let resp_node = self.client.send_iq(iq).await?;
108
109        let mut result = HashMap::new();
110
111        if let Some(groups_node) = resp_node.get_optional_child("groups") {
112            for group_node in groups_node.get_children_by_tag("group") {
113                let group_id_str = group_node.attrs().string("id");
114                let group_jid: Jid = if group_id_str.contains('@') {
115                    group_id_str
116                        .parse()
117                        .unwrap_or_else(|_| Jid::group(&group_id_str))
118                } else {
119                    Jid::group(&group_id_str)
120                };
121
122                let subject = group_node
123                    .attrs()
124                    .optional_string("subject")
125                    .unwrap_or_default()
126                    .to_string();
127
128                let addressing_mode_str = group_node
129                    .attrs()
130                    .optional_string("addressing_mode")
131                    .unwrap_or("pn");
132                let addressing_mode = match addressing_mode_str {
133                    "lid" => crate::types::message::AddressingMode::Lid,
134                    _ => crate::types::message::AddressingMode::Pn,
135                };
136
137                let mut participants = Vec::new();
138                for participant_node in group_node.get_children_by_tag("participant") {
139                    let jid = participant_node.attrs().jid("jid");
140                    let phone_number = participant_node.attrs().optional_jid("phone_number");
141                    let admin_type = participant_node.attrs().optional_string("type");
142                    let is_admin = admin_type == Some("admin") || admin_type == Some("superadmin");
143
144                    participants.push(GroupParticipant {
145                        jid,
146                        phone_number,
147                        is_admin,
148                    });
149                }
150
151                let metadata = GroupMetadata {
152                    id: group_jid.clone(),
153                    subject,
154                    participants,
155                    addressing_mode,
156                };
157
158                result.insert(group_jid.to_string(), metadata);
159            }
160        }
161
162        Ok(result)
163    }
164
165    pub async fn get_metadata(&self, jid: &Jid) -> Result<GroupMetadata, anyhow::Error> {
166        let query_node = NodeBuilder::new("query")
167            .attr("request", "interactive")
168            .build();
169
170        let iq = InfoQuery::get(
171            "w:g2",
172            jid.clone(),
173            Some(NodeContent::Nodes(vec![query_node])),
174        );
175
176        let resp_node = self.client.send_iq(iq).await?;
177
178        let group_node = resp_node
179            .get_optional_child("group")
180            .ok_or_else(|| anyhow::anyhow!("<group> not found in group info response"))?;
181
182        let subject = group_node
183            .attrs()
184            .optional_string("subject")
185            .unwrap_or_default()
186            .to_string();
187
188        let addressing_mode_str = group_node
189            .attrs()
190            .optional_string("addressing_mode")
191            .unwrap_or("pn");
192        let addressing_mode = match addressing_mode_str {
193            "lid" => crate::types::message::AddressingMode::Lid,
194            _ => crate::types::message::AddressingMode::Pn,
195        };
196
197        let mut participants = Vec::new();
198        for participant_node in group_node.get_children_by_tag("participant") {
199            let participant_jid = participant_node.attrs().jid("jid");
200            let phone_number = participant_node.attrs().optional_jid("phone_number");
201            let admin_type = participant_node.attrs().optional_string("type");
202            let is_admin = admin_type == Some("admin") || admin_type == Some("superadmin");
203
204            participants.push(GroupParticipant {
205                jid: participant_jid,
206                phone_number,
207                is_admin,
208            });
209        }
210
211        Ok(GroupMetadata {
212            id: jid.clone(),
213            subject,
214            participants,
215            addressing_mode,
216        })
217    }
218}
219
220impl Client {
221    pub fn groups(&self) -> Groups<'_> {
222        Groups::new(self)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_group_metadata_struct() {
232        let jid: Jid = "123456789@g.us"
233            .parse()
234            .expect("test group JID should be valid");
235        let participant_jid: Jid = "1234567890@s.whatsapp.net"
236            .parse()
237            .expect("test participant JID should be valid");
238
239        let metadata = GroupMetadata {
240            id: jid.clone(),
241            subject: "Test Group".to_string(),
242            participants: vec![GroupParticipant {
243                jid: participant_jid,
244                phone_number: None,
245                is_admin: true,
246            }],
247            addressing_mode: crate::types::message::AddressingMode::Pn,
248        };
249
250        assert_eq!(metadata.subject, "Test Group");
251        assert_eq!(metadata.participants.len(), 1);
252        assert!(metadata.participants[0].is_admin);
253    }
254}