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::groups::{
13 DeleteCommunityIq, GetLinkedGroupsParticipantsIq, GroupCreateIq, GroupCreateOptions,
14 JoinLinkedGroupIq, LinkSubgroupsIq, QueryLinkedGroupIq, UnlinkSubgroupsIq,
15};
16use wacore::iq::mex_ids::community as community_docs;
17use wacore_binary::Jid;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum GroupType {
25 Default,
27 Community,
29 LinkedSubgroup,
31 LinkedAnnouncementGroup,
33 LinkedGeneralGroup,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct CreateCommunityOptions {
40 pub name: String,
41 pub description: Option<String>,
42 pub closed: bool,
44 pub allow_non_admin_sub_group_creation: bool,
46 pub create_general_chat: bool,
48}
49
50impl CreateCommunityOptions {
51 pub fn new(name: impl Into<String>) -> Self {
52 Self {
53 name: name.into(),
54 description: None,
55 closed: false,
56 allow_non_admin_sub_group_creation: false,
57 create_general_chat: true,
58 }
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct CreateCommunityResult {
65 pub metadata: GroupMetadata,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
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, PartialEq, Eq)]
80pub struct LinkSubgroupsResult {
81 pub linked_jids: Vec<Jid>,
82 pub failed_groups: Vec<(Jid, u32)>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
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 group = self
138 .client
139 .execute(GroupCreateIq::new(create_options))
140 .await?;
141 let mut metadata = GroupMetadata::from(group);
142
143 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(&metadata.id, Some(desc), None)
149 .await?;
150 metadata.description = Some(desc_text);
151 }
152
153 Ok(CreateCommunityResult { metadata })
154 }
155
156 pub async fn deactivate(&self, community_jid: &Jid) -> Result<(), anyhow::Error> {
158 self.client
159 .execute(DeleteCommunityIq::new(community_jid))
160 .await?;
161 Ok(())
162 }
163
164 pub async fn link_subgroups(
166 &self,
167 community_jid: &Jid,
168 subgroup_jids: &[Jid],
169 ) -> Result<LinkSubgroupsResult, anyhow::Error> {
170 let response = self
171 .client
172 .execute(LinkSubgroupsIq::new(community_jid, subgroup_jids))
173 .await?;
174
175 let mut linked_jids = Vec::with_capacity(response.groups.len());
176 let mut failed_groups = Vec::with_capacity(response.groups.len());
177
178 for group in response.groups {
179 if let Some(error) = group.error {
180 failed_groups.push((group.jid, error));
181 } else {
182 linked_jids.push(group.jid);
183 }
184 }
185
186 Ok(LinkSubgroupsResult {
187 linked_jids,
188 failed_groups,
189 })
190 }
191
192 pub async fn unlink_subgroups(
194 &self,
195 community_jid: &Jid,
196 subgroup_jids: &[Jid],
197 remove_orphan_members: bool,
198 ) -> Result<UnlinkSubgroupsResult, anyhow::Error> {
199 let response = self
200 .client
201 .execute(UnlinkSubgroupsIq::new(
202 community_jid,
203 subgroup_jids,
204 remove_orphan_members,
205 ))
206 .await?;
207
208 let mut unlinked_jids = Vec::with_capacity(response.groups.len());
209 let mut failed_groups = Vec::with_capacity(response.groups.len());
210
211 for group in response.groups {
212 if let Some(error) = group.error {
213 failed_groups.push((group.jid, error));
214 } else {
215 unlinked_jids.push(group.jid);
216 }
217 }
218
219 Ok(UnlinkSubgroupsResult {
220 unlinked_jids,
221 failed_groups,
222 })
223 }
224
225 pub async fn get_subgroups(
227 &self,
228 community_jid: &Jid,
229 ) -> Result<Vec<CommunitySubgroup>, MexError> {
230 let response = self
231 .client
232 .mex()
233 .query(MexRequest {
234 doc: community_docs::FETCH_ALL_SUBGROUPS,
235 variables: json!({
236 "group_id": community_jid.to_string()
237 }),
238 })
239 .await?;
240
241 let data = response
242 .data
243 .ok_or_else(|| MexError::PayloadParsing("missing data field".into()))?;
244
245 let group_query = &data["xwa2_group_query_by_id"];
246 let mut subgroups = Vec::new();
247
248 if let Some(default_sub) = group_query.get("default_sub_group")
250 && !default_sub.is_null()
251 && let Some(sg) = parse_subgroup_node(default_sub, true)
252 {
253 subgroups.push(sg);
254 }
255
256 if let Some(sub_groups) = group_query.get("sub_groups")
258 && let Some(edges) = sub_groups.get("edges").and_then(|e| e.as_array())
259 {
260 for edge in edges {
261 if let Some(node) = edge.get("node")
262 && let Some(sg) = parse_subgroup_node(node, false)
263 {
264 subgroups.push(sg);
265 }
266 }
267 }
268
269 Ok(subgroups)
270 }
271
272 pub async fn get_subgroup_participant_counts(
274 &self,
275 community_jid: &Jid,
276 ) -> Result<Vec<(Jid, u32)>, MexError> {
277 let response = self
278 .client
279 .mex()
280 .query(MexRequest {
281 doc: community_docs::FETCH_SUBGROUP_PARTICIPANT_COUNT,
282 variables: json!({
283 "input": {
284 "group_jid": community_jid.to_string()
285 }
286 }),
287 })
288 .await?;
289
290 let data = response
291 .data
292 .ok_or_else(|| MexError::PayloadParsing("missing data field".into()))?;
293
294 let group_query = &data["xwa2_group_query_by_id"];
295 let edges_ref = group_query
296 .get("sub_groups")
297 .and_then(|s| s.get("edges"))
298 .and_then(|e| e.as_array());
299 let mut counts = Vec::with_capacity(edges_ref.map_or(0, |e| e.len()));
300
301 if let Some(edges) = edges_ref {
302 for edge in edges {
303 if let Some(node) = edge.get("node") {
304 let id_str = node["id"].as_str().unwrap_or_default();
305 let count = node
306 .get("total_participants_count")
307 .or_else(|| node.get("participants_count"))
308 .and_then(|c| c.as_u64())
309 .unwrap_or(0) as u32;
310 match id_str.parse::<Jid>() {
311 Ok(jid) => counts.push((jid, count)),
312 Err(_) => warn!(
313 "community: skipping subgroup with unparseable id: {:?}",
314 id_str
315 ),
316 }
317 }
318 }
319 }
320
321 Ok(counts)
322 }
323
324 pub async fn query_linked_group(
326 &self,
327 community_jid: &Jid,
328 subgroup_jid: &Jid,
329 ) -> Result<GroupMetadata, anyhow::Error> {
330 let response = self
331 .client
332 .execute(QueryLinkedGroupIq::new(community_jid, subgroup_jid))
333 .await?;
334 Ok(GroupMetadata::from(response))
335 }
336
337 pub async fn join_subgroup(
339 &self,
340 community_jid: &Jid,
341 subgroup_jid: &Jid,
342 ) -> Result<GroupMetadata, anyhow::Error> {
343 let response = self
344 .client
345 .execute(JoinLinkedGroupIq::new(community_jid, subgroup_jid))
346 .await?;
347 Ok(GroupMetadata::from(response))
348 }
349
350 pub async fn get_linked_groups_participants(
352 &self,
353 community_jid: &Jid,
354 ) -> Result<Vec<GroupParticipant>, anyhow::Error> {
355 let response = self
356 .client
357 .execute(GetLinkedGroupsParticipantsIq::new(community_jid))
358 .await?;
359 Ok(response.into_iter().map(Into::into).collect())
360 }
361}
362
363fn parse_subgroup_node(node: &serde_json::Value, is_default: bool) -> Option<CommunitySubgroup> {
364 let id_str = node.get("id")?.as_str()?;
365 let jid: Jid = id_str.parse().ok()?;
366
367 let subject = node
369 .get("subject")
370 .and_then(|s| {
371 s.as_str().map(|v| v.to_string()).or_else(|| {
372 s.get("value")
373 .and_then(|v| v.as_str())
374 .map(|v| v.to_string())
375 })
376 })
377 .unwrap_or_default();
378
379 let participant_count = node
380 .get("participants_count")
381 .or_else(|| node.get("total_participants_count"))
382 .and_then(|c| c.as_u64())
383 .map(|c| c as u32);
384
385 let is_general_from_props = node
387 .get("properties")
388 .and_then(|p| p.get("general_chat"))
389 .and_then(|v| v.as_bool())
390 .unwrap_or(false);
391
392 Some(CommunitySubgroup {
393 id: jid,
394 subject,
395 participant_count,
396 is_default_sub_group: is_default,
397 is_general_chat: is_general_from_props,
398 })
399}
400
401impl Client {
402 pub fn community(&self) -> Community<'_> {
403 Community::new(self)
404 }
405}