1use crate::client::Client;
2use std::collections::HashMap;
3use wacore::client::context::GroupInfo;
4use wacore::iq::groups::{
5 AcceptGroupInviteIq, AddParticipantsIq, DemoteParticipantsIq, GetGroupInviteInfoIq,
6 GetGroupInviteLinkIq, GetMembershipRequestsIq, GroupCreateIq, GroupInfoResponse,
7 GroupParticipantResponse, GroupParticipatingIq, GroupQueryIq, LeaveGroupIq,
8 MembershipRequestActionIq, PromoteParticipantsIq, RemoveParticipantsIq, SetGroupAnnouncementIq,
9 SetGroupDescriptionIq, SetGroupEphemeralIq, SetGroupLockedIq, SetGroupMembershipApprovalIq,
10 SetGroupSubjectIq, SetMemberAddModeIq, normalize_participants,
11};
12use wacore::types::message::AddressingMode;
13use wacore_binary::jid::Jid;
14
15pub use wacore::iq::groups::{
16 GroupCreateOptions, GroupDescription, GroupParticipantOptions, GroupSubject, JoinGroupResult,
17 MemberAddMode, MemberLinkMode, MembershipApprovalMode, MembershipRequest,
18 ParticipantChangeResponse,
19};
20
21#[derive(Debug, Clone)]
22pub struct GroupMetadata {
23 pub id: Jid,
24 pub subject: String,
25 pub participants: Vec<GroupParticipant>,
26 pub addressing_mode: AddressingMode,
27 pub creator: Option<Jid>,
29 pub creation_time: Option<u64>,
31 pub subject_time: Option<u64>,
33 pub subject_owner: Option<Jid>,
35 pub description: Option<String>,
37 pub description_id: Option<String>,
39 pub is_locked: bool,
41 pub is_announcement: bool,
43 pub ephemeral_expiration: u32,
45 pub membership_approval: bool,
47 pub member_add_mode: Option<MemberAddMode>,
49 pub member_link_mode: Option<MemberLinkMode>,
51 pub size: Option<u32>,
53 pub is_parent_group: bool,
55 pub parent_group_jid: Option<Jid>,
57 pub is_default_sub_group: bool,
59 pub is_general_chat: bool,
61 pub allow_non_admin_sub_group_creation: bool,
63}
64
65#[derive(Debug, Clone)]
66pub struct GroupParticipant {
67 pub jid: Jid,
68 pub phone_number: Option<Jid>,
69 pub is_admin: bool,
70}
71
72impl From<GroupParticipantResponse> for GroupParticipant {
73 fn from(p: GroupParticipantResponse) -> Self {
74 Self {
75 jid: p.jid,
76 phone_number: p.phone_number,
77 is_admin: p.participant_type.is_admin(),
78 }
79 }
80}
81
82impl From<GroupInfoResponse> for GroupMetadata {
83 fn from(group: GroupInfoResponse) -> Self {
84 Self {
85 id: group.id,
86 subject: group.subject.into_string(),
87 participants: group.participants.into_iter().map(Into::into).collect(),
88 addressing_mode: group.addressing_mode,
89 creator: group.creator,
90 creation_time: group.creation_time,
91 subject_time: group.subject_time,
92 subject_owner: group.subject_owner,
93 description: group.description,
94 description_id: group.description_id,
95 is_locked: group.is_locked,
96 is_announcement: group.is_announcement,
97 ephemeral_expiration: group.ephemeral_expiration,
98 membership_approval: group.membership_approval,
99 member_add_mode: group.member_add_mode,
100 member_link_mode: group.member_link_mode,
101 size: group.size,
102 is_parent_group: group.is_parent_group,
103 parent_group_jid: group.parent_group_jid,
104 is_default_sub_group: group.is_default_sub_group,
105 is_general_chat: group.is_general_chat,
106 allow_non_admin_sub_group_creation: group.allow_non_admin_sub_group_creation,
107 }
108 }
109}
110
111#[derive(Debug, Clone)]
112pub struct CreateGroupResult {
113 pub gid: Jid,
114}
115
116pub struct Groups<'a> {
117 client: &'a Client,
118}
119
120impl<'a> Groups<'a> {
121 pub(crate) fn new(client: &'a Client) -> Self {
122 Self { client }
123 }
124
125 pub async fn query_info(&self, jid: &Jid) -> Result<GroupInfo, anyhow::Error> {
126 if let Some(cached) = self.client.get_group_cache().await.get(jid).await {
127 return Ok(cached);
128 }
129
130 let group = self.client.execute(GroupQueryIq::new(jid)).await?;
131
132 let participants: Vec<Jid> = group.participants.iter().map(|p| p.jid.clone()).collect();
133
134 let lid_to_pn_map: HashMap<String, Jid> = if group.addressing_mode == AddressingMode::Lid {
135 group
136 .participants
137 .iter()
138 .filter_map(|p| {
139 p.phone_number
140 .as_ref()
141 .map(|pn| (p.jid.user.clone(), pn.clone()))
142 })
143 .collect()
144 } else {
145 HashMap::new()
146 };
147
148 let mut info = GroupInfo::new(participants, group.addressing_mode);
149 if !lid_to_pn_map.is_empty() {
150 info.set_lid_to_pn_map(lid_to_pn_map);
151 }
152
153 self.client
154 .get_group_cache()
155 .await
156 .insert(jid.clone(), info.clone())
157 .await;
158
159 Ok(info)
160 }
161
162 pub async fn get_participating(&self) -> Result<HashMap<String, GroupMetadata>, anyhow::Error> {
163 let response = self.client.execute(GroupParticipatingIq::new()).await?;
164
165 let result = response
166 .groups
167 .into_iter()
168 .map(|group| {
169 let key = group.id.to_string();
170 let metadata = GroupMetadata::from(group);
171 (key, metadata)
172 })
173 .collect();
174
175 Ok(result)
176 }
177
178 pub async fn get_metadata(&self, jid: &Jid) -> Result<GroupMetadata, anyhow::Error> {
179 let group = self.client.execute(GroupQueryIq::new(jid)).await?;
180 Ok(GroupMetadata::from(group))
181 }
182
183 pub async fn create_group(
184 &self,
185 mut options: GroupCreateOptions,
186 ) -> Result<CreateGroupResult, anyhow::Error> {
187 let mut resolved_participants = Vec::with_capacity(options.participants.len());
189
190 for participant in options.participants {
191 let resolved = if participant.jid.is_lid() && participant.phone_number.is_none() {
192 let phone_number = self
193 .client
194 .get_phone_number_from_lid(&participant.jid.user)
195 .await
196 .ok_or_else(|| {
197 anyhow::anyhow!("Missing phone number mapping for LID {}", participant.jid)
198 })?;
199 participant.with_phone_number(Jid::pn(phone_number))
200 } else {
201 participant
202 };
203 resolved_participants.push(resolved);
204 }
205
206 options.participants = normalize_participants(&resolved_participants);
207
208 let gid = self.client.execute(GroupCreateIq::new(options)).await?;
209
210 Ok(CreateGroupResult { gid })
211 }
212
213 pub async fn set_subject(&self, jid: &Jid, subject: GroupSubject) -> Result<(), anyhow::Error> {
214 Ok(self
215 .client
216 .execute(SetGroupSubjectIq::new(jid, subject))
217 .await?)
218 }
219
220 pub async fn set_description(
225 &self,
226 jid: &Jid,
227 description: Option<GroupDescription>,
228 prev: Option<String>,
229 ) -> Result<(), anyhow::Error> {
230 Ok(self
231 .client
232 .execute(SetGroupDescriptionIq::new(jid, description, prev))
233 .await?)
234 }
235
236 pub async fn leave(&self, jid: &Jid) -> Result<(), anyhow::Error> {
237 self.client.execute(LeaveGroupIq::new(jid)).await?;
238 self.client.get_group_cache().await.invalidate(jid).await;
239 Ok(())
240 }
241
242 pub async fn add_participants(
243 &self,
244 jid: &Jid,
245 participants: &[Jid],
246 ) -> Result<Vec<ParticipantChangeResponse>, anyhow::Error> {
247 let result = self
248 .client
249 .execute(AddParticipantsIq::new(jid, participants))
250 .await?;
251 let accepted: Vec<_> = result
256 .iter()
257 .filter(|r| r.status.as_deref() == Some("200"))
258 .map(|r| (r.jid.clone(), None))
259 .collect();
260 if !accepted.is_empty() {
261 let group_cache = self.client.get_group_cache().await;
262 if let Some(mut info) = group_cache.get(jid).await {
263 info.add_participants(&accepted);
264 group_cache.insert(jid.clone(), info).await;
265 }
266 }
267 Ok(result)
268 }
269
270 pub async fn remove_participants(
271 &self,
272 jid: &Jid,
273 participants: &[Jid],
274 ) -> Result<Vec<ParticipantChangeResponse>, anyhow::Error> {
275 let result = self
276 .client
277 .execute(RemoveParticipantsIq::new(jid, participants))
278 .await?;
279 let accepted: Vec<&str> = result
281 .iter()
282 .filter(|r| r.status.as_deref() == Some("200"))
283 .map(|r| r.jid.user.as_str())
284 .collect();
285 if !accepted.is_empty() {
286 let group_cache = self.client.get_group_cache().await;
287 if let Some(mut info) = group_cache.get(jid).await {
288 info.remove_participants(&accepted);
289 group_cache.insert(jid.clone(), info).await;
290 }
291 }
292 Ok(result)
293 }
294
295 pub async fn promote_participants(
296 &self,
297 jid: &Jid,
298 participants: &[Jid],
299 ) -> Result<(), anyhow::Error> {
300 Ok(self
301 .client
302 .execute(PromoteParticipantsIq::new(jid, participants))
303 .await?)
304 }
305
306 pub async fn demote_participants(
307 &self,
308 jid: &Jid,
309 participants: &[Jid],
310 ) -> Result<(), anyhow::Error> {
311 Ok(self
312 .client
313 .execute(DemoteParticipantsIq::new(jid, participants))
314 .await?)
315 }
316
317 pub async fn get_invite_link(&self, jid: &Jid, reset: bool) -> Result<String, anyhow::Error> {
318 Ok(self
319 .client
320 .execute(GetGroupInviteLinkIq::new(jid, reset))
321 .await?)
322 }
323
324 pub async fn set_locked(&self, jid: &Jid, locked: bool) -> Result<(), anyhow::Error> {
326 let spec = if locked {
327 SetGroupLockedIq::lock(jid)
328 } else {
329 SetGroupLockedIq::unlock(jid)
330 };
331 Ok(self.client.execute(spec).await?)
332 }
333
334 pub async fn set_announce(&self, jid: &Jid, announce: bool) -> Result<(), anyhow::Error> {
336 let spec = if announce {
337 SetGroupAnnouncementIq::announce(jid)
338 } else {
339 SetGroupAnnouncementIq::unannounce(jid)
340 };
341 Ok(self.client.execute(spec).await?)
342 }
343
344 pub async fn set_ephemeral(&self, jid: &Jid, expiration: u32) -> Result<(), anyhow::Error> {
349 let spec = match std::num::NonZeroU32::new(expiration) {
350 Some(exp) => SetGroupEphemeralIq::enable(jid, exp),
351 None => SetGroupEphemeralIq::disable(jid),
352 };
353 Ok(self.client.execute(spec).await?)
354 }
355
356 pub async fn set_membership_approval(
358 &self,
359 jid: &Jid,
360 mode: MembershipApprovalMode,
361 ) -> Result<(), anyhow::Error> {
362 Ok(self
363 .client
364 .execute(SetGroupMembershipApprovalIq::new(jid, mode))
365 .await?)
366 }
367
368 pub async fn join_with_invite_code(
370 &self,
371 code: &str,
372 ) -> Result<JoinGroupResult, anyhow::Error> {
373 let code = strip_invite_url(code);
374 Ok(self.client.execute(AcceptGroupInviteIq::new(code)).await?)
375 }
376
377 pub async fn get_invite_info(&self, code: &str) -> Result<GroupMetadata, anyhow::Error> {
379 let code = strip_invite_url(code);
380 let group = self.client.execute(GetGroupInviteInfoIq::new(code)).await?;
381 Ok(GroupMetadata::from(group))
382 }
383
384 pub async fn get_membership_requests(
386 &self,
387 jid: &Jid,
388 ) -> Result<Vec<MembershipRequest>, anyhow::Error> {
389 Ok(self
390 .client
391 .execute(GetMembershipRequestsIq::new(jid))
392 .await?)
393 }
394
395 pub async fn approve_membership_requests(
397 &self,
398 jid: &Jid,
399 participants: &[Jid],
400 ) -> Result<Vec<ParticipantChangeResponse>, anyhow::Error> {
401 Ok(self
402 .client
403 .execute(MembershipRequestActionIq::approve(jid, participants))
404 .await?)
405 }
406
407 pub async fn reject_membership_requests(
409 &self,
410 jid: &Jid,
411 participants: &[Jid],
412 ) -> Result<Vec<ParticipantChangeResponse>, anyhow::Error> {
413 Ok(self
414 .client
415 .execute(MembershipRequestActionIq::reject(jid, participants))
416 .await?)
417 }
418
419 pub async fn set_member_add_mode(
421 &self,
422 jid: &Jid,
423 mode: MemberAddMode,
424 ) -> Result<(), anyhow::Error> {
425 Ok(self
426 .client
427 .execute(SetMemberAddModeIq::new(jid, mode))
428 .await?)
429 }
430}
431
432impl Client {
433 pub fn groups(&self) -> Groups<'_> {
434 Groups::new(self)
435 }
436}
437
438fn strip_invite_url(code: &str) -> &str {
439 let code = code.trim().trim_end_matches('/');
440 code.strip_prefix("https://chat.whatsapp.com/")
441 .or_else(|| code.strip_prefix("http://chat.whatsapp.com/"))
442 .unwrap_or(code)
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 #[test]
450 fn test_group_metadata_struct() {
451 let jid: Jid = "123456789@g.us"
452 .parse()
453 .expect("test group JID should be valid");
454 let participant_jid: Jid = "1234567890@s.whatsapp.net"
455 .parse()
456 .expect("test participant JID should be valid");
457
458 let metadata = GroupMetadata {
459 id: jid.clone(),
460 subject: "Test Group".to_string(),
461 participants: vec![GroupParticipant {
462 jid: participant_jid,
463 phone_number: None,
464 is_admin: true,
465 }],
466 addressing_mode: AddressingMode::Pn,
467 creator: None,
468 creation_time: None,
469 subject_time: None,
470 subject_owner: None,
471 description: None,
472 description_id: None,
473 is_locked: false,
474 is_announcement: false,
475 ephemeral_expiration: 0,
476 membership_approval: false,
477 member_add_mode: None,
478 member_link_mode: None,
479 size: None,
480 is_parent_group: false,
481 parent_group_jid: None,
482 is_default_sub_group: false,
483 is_general_chat: false,
484 allow_non_admin_sub_group_creation: false,
485 };
486
487 assert_eq!(metadata.subject, "Test Group");
488 assert_eq!(metadata.participants.len(), 1);
489 assert!(metadata.participants[0].is_admin);
490 }
491
492 }