whatsapp_rust/features/
groups.rs1use 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}