1use crate::client::Client;
7use crate::features::groups::GroupMetadata;
8use crate::features::groups::GroupParticipant;
9use crate::features::mex::{MexError, MexRequest};
10use log::warn;
11use serde_json::json;
12use wacore::iq::community::mex_docs;
13use wacore::iq::groups::{
14 DeleteCommunityIq, GetLinkedGroupsParticipantsIq, GroupCreateIq, GroupCreateOptions,
15 JoinLinkedGroupIq, LinkSubgroupsIq, QueryLinkedGroupIq, UnlinkSubgroupsIq,
16};
17use wacore_binary::jid::Jid;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum GroupType {
24 Default,
26 Community,
28 LinkedSubgroup,
30 LinkedAnnouncementGroup,
32 LinkedGeneralGroup,
34}
35
36#[derive(Debug, Clone)]
38pub struct CreateCommunityOptions {
39 pub name: String,
40 pub description: Option<String>,
41 pub closed: bool,
43 pub allow_non_admin_sub_group_creation: bool,
45 pub create_general_chat: bool,
47}
48
49impl CreateCommunityOptions {
50 pub fn new(name: impl Into<String>) -> Self {
51 Self {
52 name: name.into(),
53 description: None,
54 closed: false,
55 allow_non_admin_sub_group_creation: false,
56 create_general_chat: true,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct CreateCommunityResult {
64 pub gid: Jid,
66}
67
68#[derive(Debug, Clone)]
70pub struct CommunitySubgroup {
71 pub id: Jid,
72 pub subject: String,
73 pub participant_count: Option<u32>,
74 pub is_default_sub_group: bool,
75 pub is_general_chat: bool,
76}
77
78#[derive(Debug, Clone)]
80pub struct LinkSubgroupsResult {
81 pub linked_jids: Vec<Jid>,
82 pub failed_groups: Vec<(Jid, u32)>,
83}
84
85#[derive(Debug, Clone)]
87pub struct UnlinkSubgroupsResult {
88 pub unlinked_jids: Vec<Jid>,
89 pub failed_groups: Vec<(Jid, u32)>,
90}
91
92pub fn group_type(metadata: &GroupMetadata) -> GroupType {
94 if metadata.is_default_sub_group {
95 GroupType::LinkedAnnouncementGroup
96 } else if metadata.is_general_chat {
97 GroupType::LinkedGeneralGroup
98 } else if metadata.parent_group_jid.is_some() {
99 GroupType::LinkedSubgroup
100 } else if metadata.is_parent_group {
101 GroupType::Community
102 } else {
103 GroupType::Default
104 }
105}
106
107pub struct Community<'a> {
110 client: &'a Client,
111}
112
113impl<'a> Community<'a> {
114 pub(crate) fn new(client: &'a Client) -> Self {
115 Self { client }
116 }
117
118 pub async fn create(
123 &self,
124 options: CreateCommunityOptions,
125 ) -> Result<CreateCommunityResult, anyhow::Error> {
126 let description = options.description.clone();
127
128 let create_options = GroupCreateOptions {
129 subject: options.name,
130 is_parent: true,
131 closed: options.closed,
132 allow_non_admin_sub_group_creation: options.allow_non_admin_sub_group_creation,
133 create_general_chat: options.create_general_chat,
134 ..Default::default()
135 };
136
137 let gid = self
138 .client
139 .execute(GroupCreateIq::new(create_options))
140 .await?;
141
142 if let Some(desc_text) = description
144 && let Ok(desc) = wacore::iq::groups::GroupDescription::new(&desc_text)
145 {
146 self.client
147 .groups()
148 .set_description(&gid, Some(desc), None)
149 .await?;
150 }
151
152 Ok(CreateCommunityResult { gid })
153 }
154
155 pub async fn deactivate(&self, community_jid: &Jid) -> Result<(), anyhow::Error> {
157 self.client
158 .execute(DeleteCommunityIq::new(community_jid))
159 .await?;
160 Ok(())
161 }
162
163 pub async fn link_subgroups(
165 &self,
166 community_jid: &Jid,
167 subgroup_jids: &[Jid],
168 ) -> Result<LinkSubgroupsResult, anyhow::Error> {
169 let response = self
170 .client
171 .execute(LinkSubgroupsIq::new(community_jid, subgroup_jids))
172 .await?;
173
174 let mut linked_jids = Vec::new();
175 let mut failed_groups = Vec::new();
176
177 for group in response.groups {
178 if let Some(error) = group.error {
179 failed_groups.push((group.jid, error));
180 } else {
181 linked_jids.push(group.jid);
182 }
183 }
184
185 Ok(LinkSubgroupsResult {
186 linked_jids,
187 failed_groups,
188 })
189 }
190
191 pub async fn unlink_subgroups(
193 &self,
194 community_jid: &Jid,
195 subgroup_jids: &[Jid],
196 remove_orphan_members: bool,
197 ) -> Result<UnlinkSubgroupsResult, anyhow::Error> {
198 let response = self
199 .client
200 .execute(UnlinkSubgroupsIq::new(
201 community_jid,
202 subgroup_jids,
203 remove_orphan_members,
204 ))
205 .await?;
206
207 let mut unlinked_jids = Vec::new();
208 let mut failed_groups = Vec::new();
209
210 for group in response.groups {
211 if let Some(error) = group.error {
212 failed_groups.push((group.jid, error));
213 } else {
214 unlinked_jids.push(group.jid);
215 }
216 }
217
218 Ok(UnlinkSubgroupsResult {
219 unlinked_jids,
220 failed_groups,
221 })
222 }
223
224 pub async fn get_subgroups(
226 &self,
227 community_jid: &Jid,
228 ) -> Result<Vec<CommunitySubgroup>, MexError> {
229 let response = self
230 .client
231 .mex()
232 .query(MexRequest {
233 doc_id: mex_docs::FETCH_ALL_SUBGROUPS,
234 variables: json!({
235 "group_id": community_jid.to_string()
236 }),
237 })
238 .await?;
239
240 let data = response
241 .data
242 .ok_or_else(|| MexError::PayloadParsing("missing data field".into()))?;
243
244 let group_query = &data["xwa2_group_query_by_id"];
245 let mut subgroups = Vec::new();
246
247 if let Some(default_sub) = group_query.get("default_sub_group")
249 && !default_sub.is_null()
250 && let Some(sg) = parse_subgroup_node(default_sub, true)
251 {
252 subgroups.push(sg);
253 }
254
255 if let Some(sub_groups) = group_query.get("sub_groups")
257 && let Some(edges) = sub_groups.get("edges").and_then(|e| e.as_array())
258 {
259 for edge in edges {
260 if let Some(node) = edge.get("node")
261 && let Some(sg) = parse_subgroup_node(node, false)
262 {
263 subgroups.push(sg);
264 }
265 }
266 }
267
268 Ok(subgroups)
269 }
270
271 pub async fn get_subgroup_participant_counts(
273 &self,
274 community_jid: &Jid,
275 ) -> Result<Vec<(Jid, u32)>, MexError> {
276 let response = self
277 .client
278 .mex()
279 .query(MexRequest {
280 doc_id: mex_docs::FETCH_SUBGROUP_PARTICIPANT_COUNT,
281 variables: json!({
282 "input": {
283 "group_jid": community_jid.to_string()
284 }
285 }),
286 })
287 .await?;
288
289 let data = response
290 .data
291 .ok_or_else(|| MexError::PayloadParsing("missing data field".into()))?;
292
293 let group_query = &data["xwa2_group_query_by_id"];
294 let mut counts = Vec::new();
295
296 if let Some(sub_groups) = group_query.get("sub_groups")
297 && let Some(edges) = sub_groups.get("edges").and_then(|e| e.as_array())
298 {
299 for edge in edges {
300 if let Some(node) = edge.get("node") {
301 let id_str = node["id"].as_str().unwrap_or_default();
302 let count = node
303 .get("total_participants_count")
304 .or_else(|| node.get("participants_count"))
305 .and_then(|c| c.as_u64())
306 .unwrap_or(0) as u32;
307 match id_str.parse::<Jid>() {
308 Ok(jid) => counts.push((jid, count)),
309 Err(_) => warn!(
310 "community: skipping subgroup with unparseable id: {:?}",
311 id_str
312 ),
313 }
314 }
315 }
316 }
317
318 Ok(counts)
319 }
320
321 pub async fn query_linked_group(
323 &self,
324 community_jid: &Jid,
325 subgroup_jid: &Jid,
326 ) -> Result<GroupMetadata, anyhow::Error> {
327 let response = self
328 .client
329 .execute(QueryLinkedGroupIq::new(community_jid, subgroup_jid))
330 .await?;
331 Ok(GroupMetadata::from(response))
332 }
333
334 pub async fn join_subgroup(
336 &self,
337 community_jid: &Jid,
338 subgroup_jid: &Jid,
339 ) -> Result<GroupMetadata, anyhow::Error> {
340 let response = self
341 .client
342 .execute(JoinLinkedGroupIq::new(community_jid, subgroup_jid))
343 .await?;
344 Ok(GroupMetadata::from(response))
345 }
346
347 pub async fn get_linked_groups_participants(
349 &self,
350 community_jid: &Jid,
351 ) -> Result<Vec<GroupParticipant>, anyhow::Error> {
352 let response = self
353 .client
354 .execute(GetLinkedGroupsParticipantsIq::new(community_jid))
355 .await?;
356 Ok(response.into_iter().map(Into::into).collect())
357 }
358}
359
360fn parse_subgroup_node(node: &serde_json::Value, is_default: bool) -> Option<CommunitySubgroup> {
361 let id_str = node.get("id")?.as_str()?;
362 let jid: Jid = id_str.parse().ok()?;
363
364 let subject = node
366 .get("subject")
367 .and_then(|s| {
368 s.as_str().map(|v| v.to_string()).or_else(|| {
369 s.get("value")
370 .and_then(|v| v.as_str())
371 .map(|v| v.to_string())
372 })
373 })
374 .unwrap_or_default();
375
376 let participant_count = node
377 .get("participants_count")
378 .or_else(|| node.get("total_participants_count"))
379 .and_then(|c| c.as_u64())
380 .map(|c| c as u32);
381
382 let is_general_from_props = node
384 .get("properties")
385 .and_then(|p| p.get("general_chat"))
386 .and_then(|v| v.as_bool())
387 .unwrap_or(false);
388
389 Some(CommunitySubgroup {
390 id: jid,
391 subject,
392 participant_count,
393 is_default_sub_group: is_default,
394 is_general_chat: is_general_from_props,
395 })
396}
397
398impl Client {
399 pub fn community(&self) -> Community<'_> {
400 Community::new(self)
401 }
402}