1use crate::client::Client;
2use std::collections::HashMap;
3use wacore::client::context::GroupInfo;
4use wacore::iq::groups::{
5 AcceptGroupInviteIq, AcceptGroupInviteV4Iq, AddParticipantsIq, DemoteParticipantsIq,
6 GetGroupInviteInfoIq, GetGroupInviteLinkIq, GetMembershipRequestsIq, GroupCreateIq,
7 GroupInfoResponse, 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 join_with_invite_v4(
379 &self,
380 group_jid: &Jid,
381 code: &str,
382 expiration: i64,
383 admin_jid: &Jid,
384 ) -> Result<JoinGroupResult, anyhow::Error> {
385 if expiration > 0 {
386 let now = wacore::time::now_millis() / 1000;
387 if expiration < now {
388 anyhow::bail!("V4 invite has expired (expiration={expiration}, now={now})");
389 }
390 }
391 Ok(self
392 .client
393 .execute(AcceptGroupInviteV4Iq::new(
394 group_jid.clone(),
395 code.to_string(),
396 expiration,
397 admin_jid.clone(),
398 ))
399 .await?)
400 }
401
402 pub async fn get_invite_info(&self, code: &str) -> Result<GroupMetadata, anyhow::Error> {
404 let code = strip_invite_url(code);
405 let group = self.client.execute(GetGroupInviteInfoIq::new(code)).await?;
406 Ok(GroupMetadata::from(group))
407 }
408
409 pub async fn get_membership_requests(
411 &self,
412 jid: &Jid,
413 ) -> Result<Vec<MembershipRequest>, anyhow::Error> {
414 Ok(self
415 .client
416 .execute(GetMembershipRequestsIq::new(jid))
417 .await?)
418 }
419
420 pub async fn approve_membership_requests(
422 &self,
423 jid: &Jid,
424 participants: &[Jid],
425 ) -> Result<Vec<ParticipantChangeResponse>, anyhow::Error> {
426 Ok(self
427 .client
428 .execute(MembershipRequestActionIq::approve(jid, participants))
429 .await?)
430 }
431
432 pub async fn reject_membership_requests(
434 &self,
435 jid: &Jid,
436 participants: &[Jid],
437 ) -> Result<Vec<ParticipantChangeResponse>, anyhow::Error> {
438 Ok(self
439 .client
440 .execute(MembershipRequestActionIq::reject(jid, participants))
441 .await?)
442 }
443
444 pub async fn set_member_add_mode(
446 &self,
447 jid: &Jid,
448 mode: MemberAddMode,
449 ) -> Result<(), anyhow::Error> {
450 Ok(self
451 .client
452 .execute(SetMemberAddModeIq::new(jid, mode))
453 .await?)
454 }
455}
456
457impl Client {
458 pub fn groups(&self) -> Groups<'_> {
459 Groups::new(self)
460 }
461}
462
463fn strip_invite_url(code: &str) -> &str {
464 let code = code.trim().trim_end_matches('/');
465 code.strip_prefix("https://chat.whatsapp.com/")
466 .or_else(|| code.strip_prefix("http://chat.whatsapp.com/"))
467 .unwrap_or(code)
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn test_group_metadata_struct() {
476 let jid: Jid = "123456789@g.us"
477 .parse()
478 .expect("test group JID should be valid");
479 let participant_jid: Jid = "1234567890@s.whatsapp.net"
480 .parse()
481 .expect("test participant JID should be valid");
482
483 let metadata = GroupMetadata {
484 id: jid.clone(),
485 subject: "Test Group".to_string(),
486 participants: vec![GroupParticipant {
487 jid: participant_jid,
488 phone_number: None,
489 is_admin: true,
490 }],
491 addressing_mode: AddressingMode::Pn,
492 creator: None,
493 creation_time: None,
494 subject_time: None,
495 subject_owner: None,
496 description: None,
497 description_id: None,
498 is_locked: false,
499 is_announcement: false,
500 ephemeral_expiration: 0,
501 membership_approval: false,
502 member_add_mode: None,
503 member_link_mode: None,
504 size: None,
505 is_parent_group: false,
506 parent_group_jid: None,
507 is_default_sub_group: false,
508 is_general_chat: false,
509 allow_non_admin_sub_group_creation: false,
510 };
511
512 assert_eq!(metadata.subject, "Test Group");
513 assert_eq!(metadata.participants.len(), 1);
514 assert!(metadata.participants[0].is_admin);
515 }
516
517 }