1use crate::client::Client;
7use crate::features::groups::GroupError;
8use crate::features::groups::GroupMetadata;
9use crate::features::groups::GroupParticipant;
10use crate::features::groups::GroupParticipantOptions;
11use crate::features::groups::ParticipantChangeResponse;
12use crate::features::groups::PreviousDescription;
13use crate::features::mex::{MexError, mex_request};
14use crate::request::IqError;
15use log::warn;
16use thiserror::Error;
17use wacore::iq::groups::{
18 CommunityParticipatingIq, DeleteCommunityIq, GetLinkedGroupsParticipantsIq, GroupCreateOptions,
19 JoinLinkedGroupIq, LinkSubgroupsIq, QueryLinkedGroupIq, UnlinkSubgroupsIq,
20};
21use wacore::iq::mex_operations::{fetch_all_subgroups, query_subgroup_participant_count};
22use wacore_binary::Jid;
23
24#[derive(Debug, Error)]
26#[non_exhaustive]
27pub enum CommunityError {
28 #[error("{0}")]
30 Iq(#[from] IqError),
31 #[error("{0}")]
33 Mex(#[from] MexError),
34 #[error("{0}")]
36 Group(#[from] GroupError),
37 #[error("invalid community request: {0}")]
39 InvalidRequest(String),
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46#[non_exhaustive]
47pub enum GroupType {
48 Default,
50 Community,
52 LinkedSubgroup,
54 LinkedAnnouncementGroup,
56 LinkedGeneralGroup,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct CreateCommunityOptions {
63 pub name: String,
64 pub description: Option<String>,
65 pub closed: bool,
67 pub allow_non_admin_sub_group_creation: bool,
69 pub create_general_chat: bool,
71}
72
73impl CreateCommunityOptions {
74 pub fn new(name: impl Into<String>) -> Self {
75 Self {
76 name: name.into(),
77 description: None,
78 closed: false,
79 allow_non_admin_sub_group_creation: false,
80 create_general_chat: true,
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
87#[non_exhaustive]
88pub struct CreateCommunityResult {
89 pub metadata: GroupMetadata,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94#[non_exhaustive]
95pub struct CommunitySubgroup {
96 pub id: Jid,
97 pub subject: String,
98 pub participant_count: Option<u32>,
99 pub creation: Option<u64>,
101 pub owner: Option<Jid>,
103 pub is_default_sub_group: bool,
104 pub is_general_chat: bool,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109#[non_exhaustive]
110pub struct LinkSubgroupsResult {
111 pub linked_jids: Vec<Jid>,
112 pub failed_groups: Vec<(Jid, u32)>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
117#[non_exhaustive]
118pub struct UnlinkSubgroupsResult {
119 pub unlinked_jids: Vec<Jid>,
120 pub failed_groups: Vec<(Jid, u32)>,
121}
122
123pub fn group_type(metadata: &GroupMetadata) -> GroupType {
125 if metadata.is_default_sub_group {
126 GroupType::LinkedAnnouncementGroup
127 } else if metadata.is_general_chat {
128 GroupType::LinkedGeneralGroup
129 } else if metadata.parent_group_jid.is_some() {
130 GroupType::LinkedSubgroup
131 } else if metadata.is_parent_group {
132 GroupType::Community
133 } else {
134 GroupType::Default
135 }
136}
137
138pub struct Community<'a> {
141 client: &'a Client,
142}
143
144impl<'a> Community<'a> {
145 pub(crate) fn new(client: &'a Client) -> Self {
146 Self { client }
147 }
148
149 pub async fn create(
154 &self,
155 options: CreateCommunityOptions,
156 ) -> Result<CreateCommunityResult, CommunityError> {
157 let description = options.description.clone();
158
159 let create_options = GroupCreateOptions {
160 subject: options.name,
161 is_parent: true,
162 closed: options.closed,
163 allow_non_admin_sub_group_creation: options.allow_non_admin_sub_group_creation,
164 create_general_chat: options.create_general_chat,
165 ..Default::default()
166 };
167
168 let mut metadata = self
169 .client
170 .groups()
171 .create_group(create_options)
172 .await?
173 .metadata;
174
175 if let Some(desc_text) = description
176 && let Ok(desc) = wacore::iq::groups::GroupDescription::new(&desc_text)
177 {
178 self.client
179 .groups()
180 .set_description(&metadata.id, Some(desc), PreviousDescription::Absent)
183 .await?;
184 metadata.description = Some(desc_text);
185 }
186
187 Ok(CreateCommunityResult { metadata })
188 }
189
190 pub async fn create_subgroup(
192 &self,
193 name: impl Into<String>,
194 participants: &[Jid],
195 parent_jid: impl Into<Jid>,
196 ) -> Result<CreateCommunityResult, CommunityError> {
197 let options = GroupCreateOptions {
198 subject: name.into(),
199 participants: participants
200 .iter()
201 .cloned()
202 .map(GroupParticipantOptions::new)
203 .collect(),
204 linked_parent: Some(parent_jid.into()),
205 ..Default::default()
206 };
207 let metadata = self.client.groups().create_group(options).await?.metadata;
208 Ok(CreateCommunityResult { metadata })
209 }
210
211 pub async fn deactivate(&self, community_jid: impl Into<Jid>) -> Result<(), CommunityError> {
213 let community_jid = &community_jid.into();
214 self.client
215 .execute(DeleteCommunityIq::new(community_jid))
216 .await?;
217 Ok(())
218 }
219
220 pub async fn remove_participants(
222 &self,
223 community_jid: impl Into<Jid>,
224 participants: &[Jid],
225 ) -> Result<Vec<ParticipantChangeResponse>, CommunityError> {
226 Ok(self
227 .client
228 .groups()
229 .remove_participants_including_linked_groups(community_jid, participants)
230 .await?)
231 }
232
233 pub async fn link_subgroups(
235 &self,
236 community_jid: impl Into<Jid>,
237 subgroup_jids: &[Jid],
238 ) -> Result<LinkSubgroupsResult, CommunityError> {
239 let community_jid = &community_jid.into();
240 let response = self
241 .client
242 .execute(LinkSubgroupsIq::new(community_jid, subgroup_jids))
243 .await?;
244
245 let mut linked_jids = Vec::with_capacity(response.groups.len());
246 let mut failed_groups = Vec::with_capacity(response.groups.len());
247
248 for group in response.groups {
249 if let Some(error) = group.error {
250 failed_groups.push((group.jid, error));
251 } else {
252 linked_jids.push(group.jid);
253 }
254 }
255
256 Ok(LinkSubgroupsResult {
257 linked_jids,
258 failed_groups,
259 })
260 }
261
262 pub async fn unlink_subgroups(
264 &self,
265 community_jid: impl Into<Jid>,
266 subgroup_jids: &[Jid],
267 remove_orphan_members: bool,
268 ) -> Result<UnlinkSubgroupsResult, CommunityError> {
269 let community_jid = &community_jid.into();
270 let response = self
271 .client
272 .execute(UnlinkSubgroupsIq::new(
273 community_jid,
274 subgroup_jids,
275 remove_orphan_members,
276 ))
277 .await?;
278
279 let mut unlinked_jids = Vec::with_capacity(response.groups.len());
280 let mut failed_groups = Vec::with_capacity(response.groups.len());
281
282 for group in response.groups {
283 if let Some(error) = group.error {
284 failed_groups.push((group.jid, error));
285 } else {
286 unlinked_jids.push(group.jid);
287 }
288 }
289
290 Ok(UnlinkSubgroupsResult {
291 unlinked_jids,
292 failed_groups,
293 })
294 }
295
296 pub async fn get_subgroups(
298 &self,
299 community_jid: &Jid,
300 ) -> Result<Vec<CommunitySubgroup>, CommunityError> {
301 let response = self
302 .client
303 .mex()
304 .query(mex_request!(fetch_all_subgroups {
305 group_id: Some(community_jid.to_string()),
306 ..Default::default()
307 }))
308 .await?;
309
310 let data = response.data.ok_or_else(|| {
311 CommunityError::InvalidRequest("MEX response missing data field".into())
312 })?;
313
314 let group_query = &data["xwa2_group_query_by_id"];
315 let mut subgroups = Vec::new();
316
317 if let Some(default_sub) = group_query.get("default_sub_group")
319 && !default_sub.is_null()
320 && let Some(sg) = parse_subgroup_node(default_sub, true)
321 {
322 subgroups.push(sg);
323 }
324
325 if let Some(sub_groups) = group_query.get("sub_groups")
327 && let Some(edges) = sub_groups.get("edges").and_then(|e| e.as_array())
328 {
329 for edge in edges {
330 if let Some(node) = edge.get("node")
331 && let Some(sg) = parse_subgroup_node(node, false)
332 {
333 subgroups.push(sg);
334 }
335 }
336 }
337
338 Ok(subgroups)
339 }
340
341 pub async fn get_participating(
343 &self,
344 ) -> Result<std::collections::HashMap<Jid, GroupMetadata>, CommunityError> {
345 let response = self.client.execute(CommunityParticipatingIq::new()).await?;
346 let mut result: std::collections::HashMap<Jid, GroupMetadata> = response
347 .groups
348 .into_iter()
349 .map(|community| {
350 let id = community.id.clone();
351 (id, GroupMetadata::from(community))
352 })
353 .collect();
354
355 for metadata in result.values_mut() {
356 self.client.groups().fill_participant_pns(metadata).await;
357 }
358
359 Ok(result)
360 }
361
362 pub async fn get_subgroup_participant_counts(
364 &self,
365 community_jid: &Jid,
366 ) -> Result<Vec<(Jid, u32)>, CommunityError> {
367 let response = self
368 .client
369 .mex()
370 .query(mex_request!(query_subgroup_participant_count {
371 input: Some(query_subgroup_participant_count::Input {
372 group_jid: Some(community_jid.to_string()),
373 ..Default::default()
374 }),
375 }))
376 .await?;
377
378 let data = response.data.ok_or_else(|| {
379 CommunityError::InvalidRequest("MEX response missing data field".into())
380 })?;
381
382 let group_query = &data["xwa2_group_query_by_id"];
383 let edges_ref = group_query
384 .get("sub_groups")
385 .and_then(|s| s.get("edges"))
386 .and_then(|e| e.as_array());
387 let mut counts = Vec::with_capacity(edges_ref.map_or(0, |e| e.len()));
388
389 if let Some(edges) = edges_ref {
390 for edge in edges {
391 if let Some(node) = edge.get("node") {
392 let id_str = node["id"].as_str().unwrap_or_default();
393 let count = node
394 .get("total_participants_count")
395 .or_else(|| node.get("participants_count"))
396 .and_then(|c| c.as_u64())
397 .unwrap_or(0) as u32;
398 match id_str.parse::<Jid>() {
399 Ok(jid) => counts.push((jid, count)),
400 Err(_) => warn!(
401 "community: skipping subgroup with unparseable id: {:?}",
402 id_str
403 ),
404 }
405 }
406 }
407 }
408
409 Ok(counts)
410 }
411
412 pub async fn query_linked_group(
414 &self,
415 community_jid: impl Into<Jid>,
416 subgroup_jid: impl Into<Jid>,
417 ) -> Result<GroupMetadata, CommunityError> {
418 let community_jid = &community_jid.into();
419 let subgroup_jid = &subgroup_jid.into();
420 let response = self
421 .client
422 .execute(QueryLinkedGroupIq::new(community_jid, subgroup_jid))
423 .await?;
424 Ok(GroupMetadata::from(response))
425 }
426
427 pub async fn join_subgroup(
429 &self,
430 community_jid: impl Into<Jid>,
431 subgroup_jid: impl Into<Jid>,
432 ) -> Result<GroupMetadata, CommunityError> {
433 let community_jid = &community_jid.into();
434 let subgroup_jid = &subgroup_jid.into();
435 let response = self
436 .client
437 .execute(JoinLinkedGroupIq::new(community_jid, subgroup_jid))
438 .await?;
439 Ok(GroupMetadata::from(response))
440 }
441
442 pub async fn get_linked_groups_participants(
444 &self,
445 community_jid: impl Into<Jid>,
446 ) -> Result<Vec<GroupParticipant>, CommunityError> {
447 let community_jid = &community_jid.into();
448 let response = self
449 .client
450 .execute(GetLinkedGroupsParticipantsIq::new(community_jid))
451 .await?;
452 Ok(response.into_iter().map(Into::into).collect())
453 }
454}
455
456fn json_u64(value: &serde_json::Value) -> Option<u64> {
457 value
458 .as_u64()
459 .or_else(|| value.as_str()?.parse::<u64>().ok())
460}
461
462fn json_jid(value: &serde_json::Value) -> Option<Jid> {
463 if let Some(value) = value.as_str() {
464 return value.parse().ok();
465 }
466
467 let object = value.as_object()?;
468 ["id", "lid", "pn"]
469 .into_iter()
470 .filter_map(|field| object.get(field)?.as_str())
471 .find_map(|value| value.parse().ok())
472}
473
474fn json_bool(value: &serde_json::Value) -> Option<bool> {
475 value.as_bool().or_else(|| match value.as_str()? {
476 "1" | "true" => Some(true),
477 "0" | "false" => Some(false),
478 _ => None,
479 })
480}
481
482fn parse_subgroup_node(node: &serde_json::Value, is_default: bool) -> Option<CommunitySubgroup> {
483 let id_str = node.get("id")?.as_str()?;
484 let jid: Jid = id_str.parse().ok()?;
485
486 let subject = node
488 .get("subject")
489 .and_then(|s| {
490 s.as_str().map(|v| v.to_string()).or_else(|| {
491 s.get("value")
492 .and_then(|v| v.as_str())
493 .map(|v| v.to_string())
494 })
495 })
496 .unwrap_or_default();
497
498 let participant_count = node
499 .get("participants_count")
500 .or_else(|| node.get("total_participants_count"))
501 .and_then(json_u64)
502 .and_then(|count| u32::try_from(count).ok());
503
504 let creation = node
505 .get("creation")
506 .or_else(|| node.get("creation_time"))
507 .and_then(json_u64)
508 .or_else(|| node.get("subject")?.get("creation_time").and_then(json_u64));
509 let owner = node
510 .get("creator")
511 .or_else(|| node.get("owner"))
512 .and_then(json_jid)
513 .or_else(|| node.get("subject")?.get("creator").and_then(json_jid));
514
515 let is_general_from_props = node
517 .get("properties")
518 .and_then(|p| p.get("general_chat"))
519 .and_then(json_bool)
520 .unwrap_or(false);
521
522 Some(CommunitySubgroup {
523 id: jid,
524 subject,
525 participant_count,
526 creation,
527 owner,
528 is_default_sub_group: is_default,
529 is_general_chat: is_general_from_props,
530 })
531}
532
533impl Client {
534 pub fn community(&self) -> Community<'_> {
535 Community::new(self)
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[test]
544 fn subgroup_parser_preserves_typed_metadata() {
545 let node = serde_json::json!({
546 "id": "120363000000000002@g.us",
547 "subject": {
548 "value": "Fictitious subgroup",
549 "creation_time": "1700000012"
550 },
551 "creator": {
552 "id": "100000000000002@lid",
553 "pn": "15550000002@s.whatsapp.net"
554 },
555 "total_participants_count": 42,
556 "properties": { "general_chat": "1" }
557 });
558
559 let subgroup = parse_subgroup_node(&node, false).expect("valid subgroup");
560 assert_eq!(subgroup.subject, "Fictitious subgroup");
561 assert_eq!(subgroup.creation, Some(1_700_000_012));
562 assert_eq!(subgroup.participant_count, Some(42));
563 assert_eq!(subgroup.owner, Some("100000000000002@lid".parse().unwrap()));
564 assert!(subgroup.is_general_chat);
565 assert!(!subgroup.is_default_sub_group);
566 }
567
568 #[test]
569 fn subgroup_parser_accepts_legacy_scalar_metadata() {
570 let node = serde_json::json!({
571 "id": "120363000000000003@g.us",
572 "subject": "Legacy subgroup",
573 "creation": 1700000024,
574 "owner": "15550000003@s.whatsapp.net",
575 "properties": { "general_chat": false }
576 });
577
578 let subgroup = parse_subgroup_node(&node, true).expect("valid subgroup");
579 assert_eq!(subgroup.creation, Some(1_700_000_024));
580 assert_eq!(
581 subgroup.owner,
582 Some("15550000003@s.whatsapp.net".parse().unwrap())
583 );
584 assert!(!subgroup.is_general_chat);
585 assert!(subgroup.is_default_sub_group);
586 }
587}