1use crate::StringEnum;
2use crate::iq::node::{collect_children, optional_attr, required_attr, required_child};
3use crate::iq::spec::IqSpec;
4use crate::protocol::ProtocolNode;
5use crate::request::InfoQuery;
6use anyhow::{Result, anyhow};
7use std::num::NonZeroU32;
8use typed_builder::TypedBuilder;
9use wacore_binary::builder::NodeBuilder;
10use wacore_binary::jid::{GROUP_SERVER, Jid};
11use wacore_binary::node::{Node, NodeContent};
12
13pub use crate::types::message::AddressingMode;
15pub const GROUP_IQ_NAMESPACE: &str = "w:g2";
17
18pub const GROUP_SUBJECT_MAX_LENGTH: usize = 100;
20
21pub const GROUP_DESCRIPTION_MAX_LENGTH: usize = 2048;
23
24pub const GROUP_SIZE_LIMIT: usize = 257;
26#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
28pub enum MemberLinkMode {
29 #[str = "admin_link"]
30 AdminLink,
31 #[str = "all_member_link"]
32 AllMemberLink,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
37pub enum MemberAddMode {
38 #[str = "admin_add"]
39 AdminAdd,
40 #[str = "all_member_add"]
41 AllMemberAdd,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
46pub enum MembershipApprovalMode {
47 #[string_default]
48 #[str = "off"]
49 Off,
50 #[str = "on"]
51 On,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
56pub enum GroupQueryRequestType {
57 #[string_default]
58 #[str = "interactive"]
59 Interactive,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
64pub enum ParticipantType {
65 #[string_default]
66 #[str = "member"]
67 Member,
68 #[str = "admin"]
69 Admin,
70 #[str = "superadmin"]
71 SuperAdmin,
72}
73
74impl ParticipantType {
75 pub fn is_admin(&self) -> bool {
76 matches!(self, ParticipantType::Admin | ParticipantType::SuperAdmin)
77 }
78}
79
80impl TryFrom<Option<&str>> for ParticipantType {
81 type Error = anyhow::Error;
82
83 fn try_from(value: Option<&str>) -> Result<Self> {
84 match value {
85 Some("admin") => Ok(ParticipantType::Admin),
86 Some("superadmin") => Ok(ParticipantType::SuperAdmin),
87 Some("member") | None => Ok(ParticipantType::Member),
88 Some(other) => Err(anyhow!("unknown participant type: {other}")),
89 }
90 }
91}
92crate::define_validated_string! {
93 pub struct GroupSubject(max_len = GROUP_SUBJECT_MAX_LENGTH, name = "Group subject")
97}
98
99crate::define_validated_string! {
100 pub struct GroupDescription(max_len = GROUP_DESCRIPTION_MAX_LENGTH, name = "Group description")
104}
105#[derive(Debug, Clone, TypedBuilder)]
107#[builder(build_method(into))]
108pub struct GroupParticipantOptions {
109 pub jid: Jid,
110 #[builder(default, setter(strip_option))]
111 pub phone_number: Option<Jid>,
112 #[builder(default, setter(strip_option))]
113 pub privacy: Option<Vec<u8>>,
114}
115
116impl GroupParticipantOptions {
117 pub fn new(jid: Jid) -> Self {
118 Self {
119 jid,
120 phone_number: None,
121 privacy: None,
122 }
123 }
124
125 pub fn from_phone(phone_number: Jid) -> Self {
126 Self::new(phone_number)
127 }
128
129 pub fn from_lid_and_phone(lid: Jid, phone_number: Jid) -> Self {
130 Self::new(lid).with_phone_number(phone_number)
131 }
132
133 pub fn with_phone_number(mut self, phone_number: Jid) -> Self {
134 self.phone_number = Some(phone_number);
135 self
136 }
137
138 pub fn with_privacy(mut self, privacy: Vec<u8>) -> Self {
139 self.privacy = Some(privacy);
140 self
141 }
142}
143
144#[derive(Debug, Clone, TypedBuilder)]
146#[builder(build_method(into))]
147pub struct GroupCreateOptions {
148 #[builder(setter(into))]
149 pub subject: String,
150 #[builder(default)]
151 pub participants: Vec<GroupParticipantOptions>,
152 #[builder(default = Some(MemberLinkMode::AdminLink), setter(strip_option))]
153 pub member_link_mode: Option<MemberLinkMode>,
154 #[builder(default = Some(MemberAddMode::AllMemberAdd), setter(strip_option))]
155 pub member_add_mode: Option<MemberAddMode>,
156 #[builder(default = Some(MembershipApprovalMode::Off), setter(strip_option))]
157 pub membership_approval_mode: Option<MembershipApprovalMode>,
158 #[builder(default = Some(0), setter(strip_option))]
159 pub ephemeral_expiration: Option<u32>,
160 #[builder(default)]
162 pub is_parent: bool,
163 #[builder(default)]
166 pub closed: bool,
167 #[builder(default)]
170 pub allow_non_admin_sub_group_creation: bool,
171 #[builder(default)]
174 pub create_general_chat: bool,
175}
176
177impl GroupCreateOptions {
178 pub fn new(subject: impl Into<String>) -> Self {
180 Self {
181 subject: subject.into(),
182 ..Default::default()
183 }
184 }
185
186 pub fn with_participant(mut self, participant: GroupParticipantOptions) -> Self {
187 self.participants.push(participant);
188 self
189 }
190
191 pub fn with_participants(mut self, participants: Vec<GroupParticipantOptions>) -> Self {
192 self.participants = participants;
193 self
194 }
195
196 pub fn with_member_link_mode(mut self, mode: MemberLinkMode) -> Self {
197 self.member_link_mode = Some(mode);
198 self
199 }
200
201 pub fn with_member_add_mode(mut self, mode: MemberAddMode) -> Self {
202 self.member_add_mode = Some(mode);
203 self
204 }
205
206 pub fn with_membership_approval_mode(mut self, mode: MembershipApprovalMode) -> Self {
207 self.membership_approval_mode = Some(mode);
208 self
209 }
210
211 pub fn with_ephemeral_expiration(mut self, expiration: u32) -> Self {
212 self.ephemeral_expiration = Some(expiration);
213 self
214 }
215}
216
217impl Default for GroupCreateOptions {
218 fn default() -> Self {
219 Self {
220 subject: String::new(),
221 participants: Vec::new(),
222 member_link_mode: Some(MemberLinkMode::AdminLink),
223 member_add_mode: Some(MemberAddMode::AllMemberAdd),
224 membership_approval_mode: Some(MembershipApprovalMode::Off),
225 ephemeral_expiration: Some(0),
226 is_parent: false,
227 closed: false,
228 allow_non_admin_sub_group_creation: false,
229 create_general_chat: false,
230 }
231 }
232}
233
234pub fn normalize_participants(
236 participants: &[GroupParticipantOptions],
237) -> Vec<GroupParticipantOptions> {
238 participants
239 .iter()
240 .cloned()
241 .map(|p| {
242 if !p.jid.is_lid() && p.phone_number.is_some() {
243 GroupParticipantOptions {
244 phone_number: None,
245 ..p
246 }
247 } else {
248 p
249 }
250 })
251 .collect()
252}
253
254pub fn build_create_group_node(options: &GroupCreateOptions) -> Node {
256 let mut children = Vec::new();
257
258 if let Some(link_mode) = &options.member_link_mode {
259 children.push(
260 NodeBuilder::new("member_link_mode")
261 .string_content(link_mode.as_str())
262 .build(),
263 );
264 }
265
266 if let Some(add_mode) = &options.member_add_mode {
267 children.push(
268 NodeBuilder::new("member_add_mode")
269 .string_content(add_mode.as_str())
270 .build(),
271 );
272 }
273
274 let participants = normalize_participants(&options.participants);
276
277 for participant in &participants {
278 let mut attrs = vec![("jid", participant.jid.to_string())];
279 if let Some(pn) = &participant.phone_number {
280 attrs.push(("phone_number", pn.to_string()));
281 }
282
283 let participant_node = if let Some(privacy_bytes) = &participant.privacy {
284 NodeBuilder::new("participant")
285 .attrs(attrs)
286 .children([NodeBuilder::new("privacy")
287 .string_content(hex::encode(privacy_bytes))
288 .build()])
289 .build()
290 } else {
291 NodeBuilder::new("participant").attrs(attrs).build()
292 };
293 children.push(participant_node);
294 }
295
296 if let Some(expiration) = &options.ephemeral_expiration {
297 children.push(
298 NodeBuilder::new("ephemeral")
299 .attr("expiration", expiration.to_string())
300 .build(),
301 );
302 }
303
304 if let Some(approval_mode) = &options.membership_approval_mode {
305 children.push(
306 NodeBuilder::new("membership_approval_mode")
307 .children([NodeBuilder::new("group_join")
308 .attr("state", approval_mode.as_str())
309 .build()])
310 .build(),
311 );
312 }
313
314 if options.is_parent {
316 let mut parent_builder = NodeBuilder::new("parent");
317 if options.closed {
318 parent_builder =
319 parent_builder.attr("default_membership_approval_mode", "request_required");
320 }
321 children.push(parent_builder.build());
322
323 if options.allow_non_admin_sub_group_creation {
324 children.push(NodeBuilder::new("allow_non_admin_sub_group_creation").build());
325 }
326 if options.create_general_chat {
327 children.push(NodeBuilder::new("create_general_chat").build());
328 }
329 }
330
331 NodeBuilder::new("create")
332 .attr("subject", &options.subject)
333 .children(children)
334 .build()
335}
336#[derive(Debug, Clone, crate::ProtocolNode)]
340#[protocol(tag = "query")]
341pub struct GroupQueryRequest {
342 #[attr(name = "request", string_enum)]
343 pub request: GroupQueryRequestType,
344}
345
346#[derive(Debug, Clone)]
348pub struct GroupParticipantResponse {
349 pub jid: Jid,
350 pub phone_number: Option<Jid>,
351 pub participant_type: ParticipantType,
352}
353
354impl ProtocolNode for GroupParticipantResponse {
355 fn tag(&self) -> &'static str {
356 "participant"
357 }
358
359 fn into_node(self) -> Node {
360 let mut builder = NodeBuilder::new("participant").attr("jid", self.jid);
361 if let Some(pn) = self.phone_number {
362 builder = builder.attr("phone_number", pn);
363 }
364 if self.participant_type != ParticipantType::Member {
365 builder = builder.attr("type", self.participant_type.as_str());
366 }
367 builder.build()
368 }
369
370 fn try_from_node(node: &Node) -> Result<Self> {
371 if node.tag != "participant" {
372 return Err(anyhow!("expected <participant>, got <{}>", node.tag));
373 }
374 let jid = node
375 .attrs()
376 .optional_jid("jid")
377 .ok_or_else(|| anyhow!("participant missing required 'jid' attribute"))?;
378 let phone_number = node.attrs().optional_jid("phone_number");
379 let participant_type = node
381 .attrs()
382 .optional_string("type")
383 .and_then(|s| ParticipantType::try_from(s.as_ref()).ok())
384 .unwrap_or(ParticipantType::Member);
385
386 Ok(Self {
387 jid,
388 phone_number,
389 participant_type,
390 })
391 }
392}
393
394#[derive(Debug, Clone)]
396pub struct GroupInfoResponse {
397 pub id: Jid,
398 pub subject: GroupSubject,
399 pub addressing_mode: AddressingMode,
400 pub participants: Vec<GroupParticipantResponse>,
401 pub creator: Option<Jid>,
403 pub creation_time: Option<u64>,
405 pub subject_time: Option<u64>,
407 pub subject_owner: Option<Jid>,
409 pub description: Option<String>,
411 pub description_id: Option<String>,
413 pub is_locked: bool,
415 pub is_announcement: bool,
417 pub ephemeral_expiration: u32,
419 pub membership_approval: bool,
421 pub member_add_mode: Option<MemberAddMode>,
423 pub member_link_mode: Option<MemberLinkMode>,
425 pub size: Option<u32>,
427 pub is_parent_group: bool,
429 pub parent_group_jid: Option<Jid>,
431 pub is_default_sub_group: bool,
433 pub is_general_chat: bool,
435 pub allow_non_admin_sub_group_creation: bool,
437}
438
439impl ProtocolNode for GroupInfoResponse {
440 fn tag(&self) -> &'static str {
441 "group"
442 }
443
444 fn into_node(self) -> Node {
445 let mut children: Vec<Node> = self
446 .participants
447 .into_iter()
448 .map(|p| p.into_node())
449 .collect();
450
451 if self.is_locked {
452 children.push(NodeBuilder::new("locked").build());
453 }
454 if self.is_announcement {
455 children.push(NodeBuilder::new("announcement").build());
456 }
457 if self.ephemeral_expiration > 0 {
458 children.push(
459 NodeBuilder::new("ephemeral")
460 .attr("expiration", self.ephemeral_expiration.to_string())
461 .build(),
462 );
463 }
464 if self.membership_approval {
465 children.push(
466 NodeBuilder::new("membership_approval_mode")
467 .children(vec![
468 NodeBuilder::new("group_join").attr("state", "on").build(),
469 ])
470 .build(),
471 );
472 }
473 if let Some(ref add_mode) = self.member_add_mode {
474 children.push(
475 NodeBuilder::new("member_add_mode")
476 .string_content(add_mode.as_str())
477 .build(),
478 );
479 }
480 if let Some(ref link_mode) = self.member_link_mode {
481 children.push(
482 NodeBuilder::new("member_link_mode")
483 .string_content(link_mode.as_str())
484 .build(),
485 );
486 }
487 if let Some(ref desc) = self.description {
488 let mut desc_builder = NodeBuilder::new("description");
489 if let Some(ref desc_id) = self.description_id {
490 desc_builder = desc_builder.attr("id", desc_id.as_str());
491 }
492 children.push(desc_builder.string_content(desc.as_str()).build());
493 }
494
495 if self.is_parent_group {
497 children.push(NodeBuilder::new("parent").build());
498 }
499 if let Some(ref parent_jid) = self.parent_group_jid {
500 children.push(
501 NodeBuilder::new("linked_parent")
502 .attr("jid", parent_jid.clone())
503 .build(),
504 );
505 }
506 if self.is_default_sub_group {
507 children.push(NodeBuilder::new("default_sub_group").build());
508 }
509 if self.is_general_chat {
510 children.push(NodeBuilder::new("general_chat").build());
511 }
512 if self.allow_non_admin_sub_group_creation {
513 children.push(NodeBuilder::new("allow_non_admin_sub_group_creation").build());
514 }
515
516 let mut builder = NodeBuilder::new("group")
517 .attr("id", self.id)
518 .attr("subject", self.subject.as_str())
519 .attr("addressing_mode", self.addressing_mode.as_str());
520
521 if let Some(creator) = self.creator {
522 builder = builder.attr("creator", creator);
523 }
524 if let Some(creation_time) = self.creation_time {
525 builder = builder.attr("creation", creation_time.to_string());
526 }
527 if let Some(subject_time) = self.subject_time {
528 builder = builder.attr("s_t", subject_time.to_string());
529 }
530 if let Some(subject_owner) = self.subject_owner {
531 builder = builder.attr("s_o", subject_owner);
532 }
533 if let Some(size) = self.size {
534 builder = builder.attr("size", size.to_string());
535 }
536
537 builder.children(children).build()
538 }
539
540 fn try_from_node(node: &Node) -> Result<Self> {
541 if node.tag != "group" {
542 return Err(anyhow!("expected <group>, got <{}>", node.tag));
543 }
544
545 let id_str = required_attr(node, "id")?;
546 let id = if id_str.contains('@') {
547 id_str.parse()?
548 } else {
549 Jid::group(id_str)
550 };
551
552 let subject = GroupSubject::new_unchecked(
553 optional_attr(node, "subject")
554 .as_deref()
555 .unwrap_or_default(),
556 );
557
558 let addressing_mode = AddressingMode::try_from(
559 optional_attr(node, "addressing_mode")
560 .as_deref()
561 .unwrap_or("pn"),
562 )?;
563
564 let participants = collect_children::<GroupParticipantResponse>(node, "participant")?;
565
566 let creator = node
568 .attrs()
569 .optional_string("creator")
570 .and_then(|s| s.parse::<Jid>().ok());
571 let creation_time = node
572 .attrs()
573 .optional_string("creation")
574 .and_then(|s| s.parse::<u64>().ok());
575 let subject_time = node
576 .attrs()
577 .optional_string("s_t")
578 .and_then(|s| s.parse::<u64>().ok());
579 let subject_owner = node
580 .attrs()
581 .optional_string("s_o")
582 .and_then(|s| s.parse::<Jid>().ok());
583 let size = node
584 .attrs()
585 .optional_string("size")
586 .and_then(|s| s.parse::<u32>().ok());
587
588 let is_locked = node.get_optional_child_by_tag(&["locked"]).is_some();
590 let is_announcement = node.get_optional_child_by_tag(&["announcement"]).is_some();
591
592 let ephemeral_expiration = node
593 .get_optional_child_by_tag(&["ephemeral"])
594 .and_then(|n| n.attrs().optional_string("expiration"))
595 .and_then(|s| s.parse::<u32>().ok())
596 .unwrap_or(0);
597
598 let membership_approval = node
599 .get_optional_child_by_tag(&["membership_approval_mode", "group_join"])
600 .and_then(|n| n.attrs().optional_string("state"))
601 .is_some_and(|s| s == "on");
602
603 let member_add_mode = node
604 .get_optional_child_by_tag(&["member_add_mode"])
605 .and_then(|n| match &n.content {
606 Some(NodeContent::String(s)) => MemberAddMode::try_from(s.as_str()).ok(),
607 _ => None,
608 });
609
610 let member_link_mode = node
611 .get_optional_child_by_tag(&["member_link_mode"])
612 .and_then(|n| match &n.content {
613 Some(NodeContent::String(s)) => MemberLinkMode::try_from(s.as_str()).ok(),
614 _ => None,
615 });
616
617 let description_node = node.get_optional_child_by_tag(&["description"]);
619 let description = description_node.and_then(|n| match &n.content {
620 Some(NodeContent::String(s)) => Some(s.clone()),
621 _ => None,
622 });
623 let description_id = description_node
624 .and_then(|n| n.attrs().optional_string("id"))
625 .map(|s| s.to_string());
626
627 let is_parent_group = node.get_optional_child_by_tag(&["parent"]).is_some();
629 let parent_group_jid = node
630 .get_optional_child_by_tag(&["linked_parent"])
631 .and_then(|n| n.attrs().optional_jid("jid"));
632 let is_default_sub_group = node
633 .get_optional_child_by_tag(&["default_sub_group"])
634 .is_some();
635 let is_general_chat = node.get_optional_child_by_tag(&["general_chat"]).is_some();
636 let allow_non_admin_sub_group_creation = node
637 .get_optional_child_by_tag(&["allow_non_admin_sub_group_creation"])
638 .is_some();
639
640 Ok(Self {
641 id,
642 subject,
643 addressing_mode,
644 participants,
645 creator,
646 creation_time,
647 subject_time,
648 subject_owner,
649 description,
650 description_id,
651 is_locked,
652 is_announcement,
653 ephemeral_expiration,
654 membership_approval,
655 member_add_mode,
656 member_link_mode,
657 size,
658 is_parent_group,
659 parent_group_jid,
660 is_default_sub_group,
661 is_general_chat,
662 allow_non_admin_sub_group_creation,
663 })
664 }
665}
666#[derive(Debug, Clone)]
668pub struct GroupParticipatingRequest {
669 pub include_participants: bool,
670 pub include_description: bool,
671}
672
673impl GroupParticipatingRequest {
674 pub fn new() -> Self {
675 Self {
676 include_participants: true,
677 include_description: true,
678 }
679 }
680}
681
682impl Default for GroupParticipatingRequest {
683 fn default() -> Self {
684 Self::new()
685 }
686}
687
688impl ProtocolNode for GroupParticipatingRequest {
689 fn tag(&self) -> &'static str {
690 "participating"
691 }
692
693 fn into_node(self) -> Node {
694 let mut children = Vec::new();
695 if self.include_participants {
696 children.push(NodeBuilder::new("participants").build());
697 }
698 if self.include_description {
699 children.push(NodeBuilder::new("description").build());
700 }
701 NodeBuilder::new("participating").children(children).build()
702 }
703
704 fn try_from_node(node: &Node) -> Result<Self> {
705 if node.tag != "participating" {
706 return Err(anyhow!("expected <participating>, got <{}>", node.tag));
707 }
708 Ok(Self::default())
709 }
710}
711
712#[derive(Debug, Clone, Default)]
714pub struct GroupParticipatingResponse {
715 pub groups: Vec<GroupInfoResponse>,
716}
717
718impl ProtocolNode for GroupParticipatingResponse {
719 fn tag(&self) -> &'static str {
720 "groups"
721 }
722
723 fn into_node(self) -> Node {
724 let children: Vec<Node> = self.groups.into_iter().map(|g| g.into_node()).collect();
725 NodeBuilder::new("groups").children(children).build()
726 }
727
728 fn try_from_node(node: &Node) -> Result<Self> {
729 if node.tag != "groups" {
730 return Err(anyhow!("expected <groups>, got <{}>", node.tag));
731 }
732
733 let groups = collect_children::<GroupInfoResponse>(node, "group")?;
734
735 Ok(Self { groups })
736 }
737}
738#[derive(Debug, Clone)]
740pub struct GroupQueryIq {
741 pub group_jid: Jid,
742}
743
744impl GroupQueryIq {
745 pub fn new(group_jid: &Jid) -> Self {
746 Self {
747 group_jid: group_jid.clone(),
748 }
749 }
750}
751
752impl IqSpec for GroupQueryIq {
753 type Response = GroupInfoResponse;
754
755 fn build_iq(&self) -> InfoQuery<'static> {
756 InfoQuery::get_ref(
757 GROUP_IQ_NAMESPACE,
758 &self.group_jid,
759 Some(NodeContent::Nodes(vec![
760 GroupQueryRequest::default().into_node(),
761 ])),
762 )
763 }
764
765 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
766 let group_node = required_child(response, "group")?;
767 GroupInfoResponse::try_from_node(group_node)
768 }
769}
770
771#[derive(Debug, Clone, Default)]
773pub struct GroupParticipatingIq;
774
775impl GroupParticipatingIq {
776 pub fn new() -> Self {
777 Self
778 }
779}
780
781impl IqSpec for GroupParticipatingIq {
782 type Response = GroupParticipatingResponse;
783
784 fn build_iq(&self) -> InfoQuery<'static> {
785 InfoQuery::get(
786 GROUP_IQ_NAMESPACE,
787 Jid::new("", GROUP_SERVER),
788 Some(NodeContent::Nodes(vec![
789 GroupParticipatingRequest::new().into_node(),
790 ])),
791 )
792 }
793
794 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
795 let groups_node = required_child(response, "groups")?;
796 GroupParticipatingResponse::try_from_node(groups_node)
797 }
798}
799
800#[derive(Debug, Clone)]
802pub struct GroupCreateIq {
803 pub options: GroupCreateOptions,
804}
805
806impl GroupCreateIq {
807 pub fn new(options: GroupCreateOptions) -> Self {
808 Self { options }
809 }
810}
811
812impl IqSpec for GroupCreateIq {
813 type Response = Jid;
814
815 fn build_iq(&self) -> InfoQuery<'static> {
816 InfoQuery::set(
817 GROUP_IQ_NAMESPACE,
818 Jid::new("", GROUP_SERVER),
819 Some(NodeContent::Nodes(vec![build_create_group_node(
820 &self.options,
821 )])),
822 )
823 }
824
825 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
826 let group_node = required_child(response, "group")?;
827 let group_id_str = required_attr(group_node, "id")?;
828
829 if group_id_str.contains('@') {
830 group_id_str.parse().map_err(Into::into)
831 } else {
832 Ok(Jid::group(group_id_str))
833 }
834 }
835}
836
837#[derive(Debug, Clone, crate::ProtocolNode)]
845#[protocol(tag = "participant")]
846pub struct ParticipantChangeResponse {
847 #[attr(name = "jid", jid)]
848 pub jid: Jid,
849 #[attr(name = "type")]
851 pub status: Option<String>,
852 #[attr(name = "error")]
853 pub error: Option<String>,
854}
855
856#[derive(Debug, Clone)]
865pub struct SetGroupSubjectIq {
866 pub group_jid: Jid,
867 pub subject: GroupSubject,
868}
869
870impl SetGroupSubjectIq {
871 pub fn new(group_jid: &Jid, subject: GroupSubject) -> Self {
872 Self {
873 group_jid: group_jid.clone(),
874 subject,
875 }
876 }
877}
878
879impl IqSpec for SetGroupSubjectIq {
880 type Response = ();
881
882 fn build_iq(&self) -> InfoQuery<'static> {
883 InfoQuery::set_ref(
884 GROUP_IQ_NAMESPACE,
885 &self.group_jid,
886 Some(NodeContent::Nodes(vec![
887 NodeBuilder::new("subject")
888 .string_content(self.subject.as_str())
889 .build(),
890 ])),
891 )
892 }
893
894 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
895 Ok(())
896 }
897}
898
899#[derive(Debug, Clone)]
912pub struct SetGroupDescriptionIq {
913 pub group_jid: Jid,
914 pub description: Option<GroupDescription>,
915 pub id: String,
917 pub prev: Option<String>,
919}
920
921impl SetGroupDescriptionIq {
922 pub fn new(
923 group_jid: &Jid,
924 description: Option<GroupDescription>,
925 prev: Option<String>,
926 ) -> Self {
927 use rand::RngExt;
928 let id = format!(
929 "{:08X}",
930 rand::make_rng::<rand::rngs::StdRng>().random::<u32>()
931 );
932 Self {
933 group_jid: group_jid.clone(),
934 description,
935 id,
936 prev,
937 }
938 }
939}
940
941impl IqSpec for SetGroupDescriptionIq {
942 type Response = ();
943
944 fn build_iq(&self) -> InfoQuery<'static> {
945 let desc_node = if let Some(ref desc) = self.description {
946 let mut builder = NodeBuilder::new("description").attr("id", &self.id);
947 if let Some(ref prev) = self.prev {
948 builder = builder.attr("prev", prev);
949 }
950 builder
951 .children([NodeBuilder::new("body")
952 .string_content(desc.as_str())
953 .build()])
954 .build()
955 } else {
956 let mut builder = NodeBuilder::new("description")
957 .attr("id", &self.id)
958 .attr("delete", "true");
959 if let Some(ref prev) = self.prev {
960 builder = builder.attr("prev", prev);
961 }
962 builder.build()
963 };
964
965 InfoQuery::set_ref(
966 GROUP_IQ_NAMESPACE,
967 &self.group_jid,
968 Some(NodeContent::Nodes(vec![desc_node])),
969 )
970 }
971
972 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
973 Ok(())
974 }
975}
976
977#[derive(Debug, Clone)]
986pub struct LeaveGroupIq {
987 pub group_jid: Jid,
988}
989
990impl LeaveGroupIq {
991 pub fn new(group_jid: &Jid) -> Self {
992 Self {
993 group_jid: group_jid.clone(),
994 }
995 }
996}
997
998impl IqSpec for LeaveGroupIq {
999 type Response = ();
1000
1001 fn build_iq(&self) -> InfoQuery<'static> {
1002 let group_node = NodeBuilder::new("group")
1003 .attr("id", self.group_jid.clone())
1004 .build();
1005 let leave_node = NodeBuilder::new("leave").children([group_node]).build();
1006
1007 InfoQuery::set(
1008 GROUP_IQ_NAMESPACE,
1009 Jid::new("", GROUP_SERVER),
1010 Some(NodeContent::Nodes(vec![leave_node])),
1011 )
1012 }
1013
1014 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
1015 Ok(())
1016 }
1017}
1018
1019macro_rules! define_group_participant_iq {
1022 (
1023 $(#[$meta:meta])*
1024 $name:ident, action = $action:literal, response = Vec<ParticipantChangeResponse>
1025 ) => {
1026 $(#[$meta])*
1027 #[derive(Debug, Clone)]
1028 pub struct $name {
1029 pub group_jid: Jid,
1030 pub participants: Vec<Jid>,
1031 }
1032
1033 impl $name {
1034 pub fn new(group_jid: &Jid, participants: &[Jid]) -> Self {
1035 Self {
1036 group_jid: group_jid.clone(),
1037 participants: participants.to_vec(),
1038 }
1039 }
1040 }
1041
1042 impl IqSpec for $name {
1043 type Response = Vec<ParticipantChangeResponse>;
1044
1045 fn build_iq(&self) -> InfoQuery<'static> {
1046 let children: Vec<Node> = self
1047 .participants
1048 .iter()
1049 .map(|jid| {
1050 NodeBuilder::new("participant")
1051 .attr("jid", jid.clone())
1052 .build()
1053 })
1054 .collect();
1055
1056 let action_node = NodeBuilder::new($action).children(children).build();
1057
1058 InfoQuery::set_ref(
1059 GROUP_IQ_NAMESPACE,
1060 &self.group_jid,
1061 Some(NodeContent::Nodes(vec![action_node])),
1062 )
1063 }
1064
1065 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
1066 let action_node = required_child(response, $action)?;
1067 collect_children::<ParticipantChangeResponse>(action_node, "participant")
1068 }
1069 }
1070 };
1071 (
1072 $(#[$meta:meta])*
1073 $name:ident, action = $action:literal, response = ()
1074 ) => {
1075 $(#[$meta])*
1076 #[derive(Debug, Clone)]
1077 pub struct $name {
1078 pub group_jid: Jid,
1079 pub participants: Vec<Jid>,
1080 }
1081
1082 impl $name {
1083 pub fn new(group_jid: &Jid, participants: &[Jid]) -> Self {
1084 Self {
1085 group_jid: group_jid.clone(),
1086 participants: participants.to_vec(),
1087 }
1088 }
1089 }
1090
1091 impl IqSpec for $name {
1092 type Response = ();
1093
1094 fn build_iq(&self) -> InfoQuery<'static> {
1095 let children: Vec<Node> = self
1096 .participants
1097 .iter()
1098 .map(|jid| {
1099 NodeBuilder::new("participant")
1100 .attr("jid", jid.clone())
1101 .build()
1102 })
1103 .collect();
1104
1105 let action_node = NodeBuilder::new($action).children(children).build();
1106
1107 InfoQuery::set_ref(
1108 GROUP_IQ_NAMESPACE,
1109 &self.group_jid,
1110 Some(NodeContent::Nodes(vec![action_node])),
1111 )
1112 }
1113
1114 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
1115 Ok(())
1116 }
1117 }
1118 };
1119}
1120
1121define_group_participant_iq!(
1122 AddParticipantsIq, action = "add", response = Vec<ParticipantChangeResponse>
1131);
1132
1133define_group_participant_iq!(
1134 RemoveParticipantsIq, action = "remove", response = Vec<ParticipantChangeResponse>
1143);
1144
1145define_group_participant_iq!(
1146 PromoteParticipantsIq, action = "promote", response = ()
1155);
1156
1157define_group_participant_iq!(
1158 DemoteParticipantsIq, action = "demote", response = ()
1167);
1168
1169#[derive(Debug, Clone)]
1176pub struct GetGroupInviteLinkIq {
1177 pub group_jid: Jid,
1178 pub reset: bool,
1179}
1180
1181impl GetGroupInviteLinkIq {
1182 pub fn new(group_jid: &Jid, reset: bool) -> Self {
1183 Self {
1184 group_jid: group_jid.clone(),
1185 reset,
1186 }
1187 }
1188}
1189
1190impl IqSpec for GetGroupInviteLinkIq {
1191 type Response = String;
1192
1193 fn build_iq(&self) -> InfoQuery<'static> {
1194 let content = Some(NodeContent::Nodes(vec![NodeBuilder::new("invite").build()]));
1195 if self.reset {
1196 InfoQuery::set_ref(GROUP_IQ_NAMESPACE, &self.group_jid, content)
1197 } else {
1198 InfoQuery::get_ref(GROUP_IQ_NAMESPACE, &self.group_jid, content)
1199 }
1200 }
1201
1202 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
1203 let invite_node = required_child(response, "invite")?;
1204 let code = required_attr(invite_node, "code")?;
1205 Ok(format!("https://chat.whatsapp.com/{code}"))
1206 }
1207}
1208
1209#[derive(Debug, Clone)]
1229pub struct SetGroupLockedIq {
1230 pub group_jid: Jid,
1231 pub locked: bool,
1232}
1233
1234impl SetGroupLockedIq {
1235 pub fn lock(group_jid: &Jid) -> Self {
1236 Self {
1237 group_jid: group_jid.clone(),
1238 locked: true,
1239 }
1240 }
1241
1242 pub fn unlock(group_jid: &Jid) -> Self {
1243 Self {
1244 group_jid: group_jid.clone(),
1245 locked: false,
1246 }
1247 }
1248}
1249
1250impl IqSpec for SetGroupLockedIq {
1251 type Response = ();
1252
1253 fn build_iq(&self) -> InfoQuery<'static> {
1254 let tag = if self.locked { "locked" } else { "unlocked" };
1255 InfoQuery::set_ref(
1256 GROUP_IQ_NAMESPACE,
1257 &self.group_jid,
1258 Some(NodeContent::Nodes(vec![NodeBuilder::new(tag).build()])),
1259 )
1260 }
1261
1262 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
1263 Ok(())
1264 }
1265}
1266
1267#[derive(Debug, Clone)]
1278pub struct SetGroupAnnouncementIq {
1279 pub group_jid: Jid,
1280 pub announce: bool,
1281}
1282
1283impl SetGroupAnnouncementIq {
1284 pub fn announce(group_jid: &Jid) -> Self {
1285 Self {
1286 group_jid: group_jid.clone(),
1287 announce: true,
1288 }
1289 }
1290
1291 pub fn unannounce(group_jid: &Jid) -> Self {
1292 Self {
1293 group_jid: group_jid.clone(),
1294 announce: false,
1295 }
1296 }
1297}
1298
1299impl IqSpec for SetGroupAnnouncementIq {
1300 type Response = ();
1301
1302 fn build_iq(&self) -> InfoQuery<'static> {
1303 let tag = if self.announce {
1304 "announcement"
1305 } else {
1306 "not_announcement"
1307 };
1308 InfoQuery::set_ref(
1309 GROUP_IQ_NAMESPACE,
1310 &self.group_jid,
1311 Some(NodeContent::Nodes(vec![NodeBuilder::new(tag).build()])),
1312 )
1313 }
1314
1315 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
1316 Ok(())
1317 }
1318}
1319
1320#[derive(Debug, Clone)]
1337pub struct SetGroupEphemeralIq {
1338 pub group_jid: Jid,
1339 pub expiration: Option<NonZeroU32>,
1341}
1342
1343impl SetGroupEphemeralIq {
1344 pub fn enable(group_jid: &Jid, expiration: NonZeroU32) -> Self {
1346 Self {
1347 group_jid: group_jid.clone(),
1348 expiration: Some(expiration),
1349 }
1350 }
1351
1352 pub fn disable(group_jid: &Jid) -> Self {
1354 Self {
1355 group_jid: group_jid.clone(),
1356 expiration: None,
1357 }
1358 }
1359}
1360
1361impl IqSpec for SetGroupEphemeralIq {
1362 type Response = ();
1363
1364 fn build_iq(&self) -> InfoQuery<'static> {
1365 let node = match self.expiration {
1366 Some(exp) => NodeBuilder::new("ephemeral")
1367 .attr("expiration", exp.to_string())
1368 .build(),
1369 None => NodeBuilder::new("not_ephemeral").build(),
1370 };
1371 InfoQuery::set_ref(
1372 GROUP_IQ_NAMESPACE,
1373 &self.group_jid,
1374 Some(NodeContent::Nodes(vec![node])),
1375 )
1376 }
1377
1378 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
1379 Ok(())
1380 }
1381}
1382
1383#[derive(Debug, Clone)]
1396pub struct SetGroupMembershipApprovalIq {
1397 pub group_jid: Jid,
1398 pub mode: MembershipApprovalMode,
1399}
1400
1401impl SetGroupMembershipApprovalIq {
1402 pub fn new(group_jid: &Jid, mode: MembershipApprovalMode) -> Self {
1403 Self {
1404 group_jid: group_jid.clone(),
1405 mode,
1406 }
1407 }
1408}
1409
1410impl IqSpec for SetGroupMembershipApprovalIq {
1411 type Response = ();
1412
1413 fn build_iq(&self) -> InfoQuery<'static> {
1414 let node = NodeBuilder::new("membership_approval_mode")
1415 .children([NodeBuilder::new("group_join")
1416 .attr("state", self.mode.as_str())
1417 .build()])
1418 .build();
1419 InfoQuery::set_ref(
1420 GROUP_IQ_NAMESPACE,
1421 &self.group_jid,
1422 Some(NodeContent::Nodes(vec![node])),
1423 )
1424 }
1425
1426 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
1427 Ok(())
1428 }
1429}
1430
1431#[derive(Debug, Clone)]
1437pub struct LinkedGroupResult {
1438 pub jid: Jid,
1439 pub error: Option<u32>,
1441}
1442
1443#[derive(Debug, Clone)]
1445pub struct LinkSubgroupsResponse {
1446 pub groups: Vec<LinkedGroupResult>,
1447}
1448
1449#[derive(Debug, Clone)]
1451pub struct UnlinkSubgroupsResponse {
1452 pub groups: Vec<LinkedGroupResult>,
1453}
1454
1455#[derive(Debug, Clone)]
1468pub struct LinkSubgroupsIq {
1469 pub parent_jid: Jid,
1470 pub subgroup_jids: Vec<Jid>,
1471}
1472
1473impl LinkSubgroupsIq {
1474 pub fn new(parent_jid: &Jid, subgroup_jids: &[Jid]) -> Self {
1475 Self {
1476 parent_jid: parent_jid.clone(),
1477 subgroup_jids: subgroup_jids.to_vec(),
1478 }
1479 }
1480}
1481
1482impl IqSpec for LinkSubgroupsIq {
1483 type Response = LinkSubgroupsResponse;
1484
1485 fn build_iq(&self) -> InfoQuery<'static> {
1486 let group_nodes: Vec<Node> = self
1487 .subgroup_jids
1488 .iter()
1489 .map(|jid| NodeBuilder::new("group").attr("jid", jid.clone()).build())
1490 .collect();
1491
1492 let link_node = NodeBuilder::new("link")
1493 .attr("link_type", "sub_group")
1494 .children(group_nodes)
1495 .build();
1496
1497 let links_node = NodeBuilder::new("links").children([link_node]).build();
1498
1499 InfoQuery::set_ref(
1500 GROUP_IQ_NAMESPACE,
1501 &self.parent_jid,
1502 Some(NodeContent::Nodes(vec![links_node])),
1503 )
1504 }
1505
1506 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
1507 let links_node = required_child(response, "links")?;
1508 let link_node = required_child(links_node, "link")?;
1509
1510 let mut groups = Vec::new();
1511 for child in link_node.get_children_by_tag("group") {
1512 let jid_str = required_attr(child, "jid")?;
1513 let jid: Jid = jid_str.parse()?;
1514 let error = child
1515 .attrs()
1516 .optional_string("error")
1517 .and_then(|s| s.parse::<u32>().ok());
1518 groups.push(LinkedGroupResult { jid, error });
1519 }
1520
1521 Ok(LinkSubgroupsResponse { groups })
1522 }
1523}
1524
1525#[derive(Debug, Clone)]
1536pub struct UnlinkSubgroupsIq {
1537 pub parent_jid: Jid,
1538 pub subgroup_jids: Vec<Jid>,
1539 pub remove_orphan_members: bool,
1540}
1541
1542impl UnlinkSubgroupsIq {
1543 pub fn new(parent_jid: &Jid, subgroup_jids: &[Jid], remove_orphan_members: bool) -> Self {
1544 Self {
1545 parent_jid: parent_jid.clone(),
1546 subgroup_jids: subgroup_jids.to_vec(),
1547 remove_orphan_members,
1548 }
1549 }
1550}
1551
1552impl IqSpec for UnlinkSubgroupsIq {
1553 type Response = UnlinkSubgroupsResponse;
1554
1555 fn build_iq(&self) -> InfoQuery<'static> {
1556 let group_nodes: Vec<Node> = self
1557 .subgroup_jids
1558 .iter()
1559 .map(|jid| {
1560 let mut builder = NodeBuilder::new("group").attr("jid", jid.clone());
1561 if self.remove_orphan_members {
1562 builder = builder.attr("remove_orphaned_members", "true");
1563 }
1564 builder.build()
1565 })
1566 .collect();
1567
1568 let unlink_node = NodeBuilder::new("unlink")
1569 .attr("unlink_type", "sub_group")
1570 .children(group_nodes)
1571 .build();
1572
1573 InfoQuery::set_ref(
1574 GROUP_IQ_NAMESPACE,
1575 &self.parent_jid,
1576 Some(NodeContent::Nodes(vec![unlink_node])),
1577 )
1578 }
1579
1580 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
1581 let unlink_node = required_child(response, "unlink")?;
1582
1583 let mut groups = Vec::new();
1584 for child in unlink_node.get_children_by_tag("group") {
1585 let jid_str = required_attr(child, "jid")?;
1586 let jid: Jid = jid_str.parse()?;
1587 let error = child
1588 .attrs()
1589 .optional_string("error")
1590 .and_then(|s| s.parse::<u32>().ok());
1591 groups.push(LinkedGroupResult { jid, error });
1592 }
1593
1594 Ok(UnlinkSubgroupsResponse { groups })
1595 }
1596}
1597
1598#[derive(Debug, Clone)]
1607pub struct DeleteCommunityIq {
1608 pub parent_jid: Jid,
1609}
1610
1611impl DeleteCommunityIq {
1612 pub fn new(parent_jid: &Jid) -> Self {
1613 Self {
1614 parent_jid: parent_jid.clone(),
1615 }
1616 }
1617}
1618
1619impl IqSpec for DeleteCommunityIq {
1620 type Response = ();
1621
1622 fn build_iq(&self) -> InfoQuery<'static> {
1623 InfoQuery::set_ref(
1624 GROUP_IQ_NAMESPACE,
1625 &self.parent_jid,
1626 Some(NodeContent::Nodes(vec![
1627 NodeBuilder::new("delete_parent").build(),
1628 ])),
1629 )
1630 }
1631
1632 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
1633 Ok(())
1634 }
1635}
1636
1637#[derive(Debug, Clone)]
1646pub struct QueryLinkedGroupIq {
1647 pub parent_jid: Jid,
1648 pub subgroup_jid: Jid,
1649}
1650
1651impl QueryLinkedGroupIq {
1652 pub fn new(parent_jid: &Jid, subgroup_jid: &Jid) -> Self {
1653 Self {
1654 parent_jid: parent_jid.clone(),
1655 subgroup_jid: subgroup_jid.clone(),
1656 }
1657 }
1658}
1659
1660impl IqSpec for QueryLinkedGroupIq {
1661 type Response = GroupInfoResponse;
1662
1663 fn build_iq(&self) -> InfoQuery<'static> {
1664 let query_node = NodeBuilder::new("query_linked")
1665 .attr("type", "sub_group")
1666 .attr("jid", self.subgroup_jid.clone())
1667 .build();
1668
1669 InfoQuery::get_ref(
1670 GROUP_IQ_NAMESPACE,
1671 &self.parent_jid,
1672 Some(NodeContent::Nodes(vec![query_node])),
1673 )
1674 }
1675
1676 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
1677 let linked_node = required_child(response, "linked_group")?;
1678 let group_node = required_child(linked_node, "group")?;
1679 GroupInfoResponse::try_from_node(group_node)
1680 }
1681}
1682
1683#[derive(Debug, Clone)]
1692pub struct JoinLinkedGroupIq {
1693 pub parent_jid: Jid,
1694 pub subgroup_jid: Jid,
1695}
1696
1697impl JoinLinkedGroupIq {
1698 pub fn new(parent_jid: &Jid, subgroup_jid: &Jid) -> Self {
1699 Self {
1700 parent_jid: parent_jid.clone(),
1701 subgroup_jid: subgroup_jid.clone(),
1702 }
1703 }
1704}
1705
1706impl IqSpec for JoinLinkedGroupIq {
1707 type Response = GroupInfoResponse;
1708
1709 fn build_iq(&self) -> InfoQuery<'static> {
1710 let node = NodeBuilder::new("join_linked_group")
1711 .attr("jid", self.subgroup_jid.clone())
1712 .build();
1713
1714 InfoQuery::set_ref(
1715 GROUP_IQ_NAMESPACE,
1716 &self.parent_jid,
1717 Some(NodeContent::Nodes(vec![node])),
1718 )
1719 }
1720
1721 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
1722 let linked_node = required_child(response, "linked_group")?;
1723 let group_node = required_child(linked_node, "group")?;
1724 GroupInfoResponse::try_from_node(group_node)
1725 }
1726}
1727
1728#[derive(Debug, Clone)]
1737pub struct GetLinkedGroupsParticipantsIq {
1738 pub parent_jid: Jid,
1739}
1740
1741impl GetLinkedGroupsParticipantsIq {
1742 pub fn new(parent_jid: &Jid) -> Self {
1743 Self {
1744 parent_jid: parent_jid.clone(),
1745 }
1746 }
1747}
1748
1749impl IqSpec for GetLinkedGroupsParticipantsIq {
1750 type Response = Vec<GroupParticipantResponse>;
1751
1752 fn build_iq(&self) -> InfoQuery<'static> {
1753 InfoQuery::get_ref(
1754 GROUP_IQ_NAMESPACE,
1755 &self.parent_jid,
1756 Some(NodeContent::Nodes(vec![
1757 NodeBuilder::new("linked_groups_participants").build(),
1758 ])),
1759 )
1760 }
1761
1762 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
1763 let container = required_child(response, "linked_groups_participants")?;
1764
1765 let direct = collect_children::<GroupParticipantResponse>(container, "participant")?;
1767 if !direct.is_empty() {
1768 return Ok(direct);
1769 }
1770
1771 let mut all = Vec::new();
1773 for group_node in container.get_children_by_tag("group") {
1774 let participants =
1775 collect_children::<GroupParticipantResponse>(group_node, "participant")?;
1776 all.extend(participants);
1777 }
1778 Ok(all)
1779 }
1780}
1781
1782#[derive(Debug, Clone, PartialEq)]
1788pub enum JoinGroupResult {
1789 Joined(Jid),
1790 PendingApproval(Jid),
1791}
1792
1793impl JoinGroupResult {
1794 pub fn group_jid(&self) -> &Jid {
1795 match self {
1796 JoinGroupResult::Joined(jid) | JoinGroupResult::PendingApproval(jid) => jid,
1797 }
1798 }
1799}
1800
1801#[derive(Debug, Clone)]
1809pub struct AcceptGroupInviteIq {
1810 pub code: String,
1811}
1812
1813impl AcceptGroupInviteIq {
1814 pub fn new(code: impl Into<String>) -> Self {
1815 Self { code: code.into() }
1816 }
1817}
1818
1819impl IqSpec for AcceptGroupInviteIq {
1820 type Response = JoinGroupResult;
1821
1822 fn build_iq(&self) -> InfoQuery<'static> {
1823 let to = Jid::new("", GROUP_SERVER);
1824 InfoQuery::set_ref(
1825 GROUP_IQ_NAMESPACE,
1826 &to,
1827 Some(NodeContent::Nodes(vec![
1828 NodeBuilder::new("invite").attr("code", &self.code).build(),
1829 ])),
1830 )
1831 }
1832
1833 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
1834 if let Some(group_node) = response.get_optional_child("group") {
1835 let jid_str = required_attr(group_node, "jid")?;
1836 let jid: Jid = jid_str
1837 .parse()
1838 .map_err(|e| anyhow!("invalid group jid: {e}"))?;
1839 return Ok(JoinGroupResult::Joined(jid));
1840 }
1841 if let Some(approval_node) = response.get_optional_child("membership_approval_request") {
1842 let jid_str = required_attr(approval_node, "jid")?;
1843 let jid: Jid = jid_str
1844 .parse()
1845 .map_err(|e| anyhow!("invalid group jid: {e}"))?;
1846 return Ok(JoinGroupResult::PendingApproval(jid));
1847 }
1848 Err(anyhow!(
1849 "expected <group> or <membership_approval_request> child in invite response"
1850 ))
1851 }
1852}
1853
1854#[derive(Debug, Clone)]
1866pub struct GetGroupInviteInfoIq {
1867 pub code: String,
1868}
1869
1870impl GetGroupInviteInfoIq {
1871 pub fn new(code: impl Into<String>) -> Self {
1872 Self { code: code.into() }
1873 }
1874}
1875
1876impl IqSpec for GetGroupInviteInfoIq {
1877 type Response = GroupInfoResponse;
1878
1879 fn build_iq(&self) -> InfoQuery<'static> {
1880 let to = Jid::new("", GROUP_SERVER);
1881 InfoQuery::get_ref(
1882 GROUP_IQ_NAMESPACE,
1883 &to,
1884 Some(NodeContent::Nodes(vec![
1885 NodeBuilder::new("invite").attr("code", &self.code).build(),
1886 ])),
1887 )
1888 }
1889
1890 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
1891 let group_node = required_child(response, "group")?;
1892 GroupInfoResponse::try_from_node(group_node)
1893 }
1894}
1895
1896#[derive(Debug, Clone)]
1908pub struct GetMembershipRequestsIq {
1909 pub group_jid: Jid,
1910}
1911
1912impl GetMembershipRequestsIq {
1913 pub fn new(jid: &Jid) -> Self {
1914 Self {
1915 group_jid: jid.clone(),
1916 }
1917 }
1918}
1919
1920#[derive(Debug, Clone, serde::Serialize)]
1921pub struct MembershipRequest {
1922 pub jid: Jid,
1923 #[serde(skip_serializing_if = "Option::is_none")]
1924 pub request_time: Option<u64>,
1925}
1926
1927impl IqSpec for GetMembershipRequestsIq {
1928 type Response = Vec<MembershipRequest>;
1929
1930 fn build_iq(&self) -> InfoQuery<'static> {
1931 InfoQuery::get_ref(
1932 GROUP_IQ_NAMESPACE,
1933 &self.group_jid,
1934 Some(NodeContent::Nodes(vec![
1935 NodeBuilder::new("membership_approval_requests").build(),
1936 ])),
1937 )
1938 }
1939
1940 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
1941 let requests_node = response
1942 .get_optional_child("membership_approval_requests")
1943 .ok_or_else(|| anyhow!("missing membership_approval_requests"))?;
1944
1945 let mut requests = Vec::new();
1946 for child in requests_node.get_children_by_tag("membership_approval_request") {
1947 let jid_str = required_attr(child, "jid")?;
1948 let jid: Jid = jid_str
1949 .parse()
1950 .map_err(|e| anyhow!("invalid jid in membership request: {e}"))?;
1951 let request_time = child
1952 .attrs()
1953 .optional_string("request_time")
1954 .and_then(|s| s.parse::<u64>().ok());
1955 requests.push(MembershipRequest { jid, request_time });
1956 }
1957 Ok(requests)
1958 }
1959}
1960
1961#[derive(Debug, Clone)]
1973pub struct MembershipRequestActionIq {
1974 pub group_jid: Jid,
1975 pub participants: Vec<Jid>,
1976 pub approve: bool,
1977}
1978
1979impl MembershipRequestActionIq {
1980 pub fn approve(group_jid: &Jid, participants: &[Jid]) -> Self {
1981 Self {
1982 group_jid: group_jid.clone(),
1983 participants: participants.to_vec(),
1984 approve: true,
1985 }
1986 }
1987
1988 pub fn reject(group_jid: &Jid, participants: &[Jid]) -> Self {
1989 Self {
1990 group_jid: group_jid.clone(),
1991 participants: participants.to_vec(),
1992 approve: false,
1993 }
1994 }
1995}
1996
1997impl IqSpec for MembershipRequestActionIq {
1998 type Response = Vec<ParticipantChangeResponse>;
1999
2000 fn build_iq(&self) -> InfoQuery<'static> {
2001 let action_tag = if self.approve { "approve" } else { "reject" };
2002 let participant_nodes: Vec<Node> = self
2003 .participants
2004 .iter()
2005 .map(|jid| {
2006 NodeBuilder::new("participant")
2007 .attr("jid", jid.clone())
2008 .build()
2009 })
2010 .collect();
2011
2012 InfoQuery::set_ref(
2013 GROUP_IQ_NAMESPACE,
2014 &self.group_jid,
2015 Some(NodeContent::Nodes(vec![
2016 NodeBuilder::new("membership_requests_action")
2017 .children(vec![
2018 NodeBuilder::new(action_tag)
2019 .children(participant_nodes)
2020 .build(),
2021 ])
2022 .build(),
2023 ])),
2024 )
2025 }
2026
2027 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
2028 let action_node = required_child(response, "membership_requests_action")?;
2029 let action_tag = if self.approve { "approve" } else { "reject" };
2030 let inner = required_child(action_node, action_tag)?;
2031 collect_children::<ParticipantChangeResponse>(inner, "participant")
2032 }
2033}
2034
2035#[derive(Debug, Clone)]
2047pub struct SetMemberAddModeIq {
2048 pub group_jid: Jid,
2049 pub mode: MemberAddMode,
2050}
2051
2052impl SetMemberAddModeIq {
2053 pub fn new(jid: &Jid, mode: MemberAddMode) -> Self {
2054 Self {
2055 group_jid: jid.clone(),
2056 mode,
2057 }
2058 }
2059}
2060
2061impl IqSpec for SetMemberAddModeIq {
2062 type Response = ();
2063
2064 fn build_iq(&self) -> InfoQuery<'static> {
2065 InfoQuery::set_ref(
2066 GROUP_IQ_NAMESPACE,
2067 &self.group_jid,
2068 Some(NodeContent::Nodes(vec![
2069 NodeBuilder::new("member_add_mode")
2070 .string_content(self.mode.as_str())
2071 .build(),
2072 ])),
2073 )
2074 }
2075
2076 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
2077 Ok(())
2078 }
2079}
2080
2081#[cfg(test)]
2082mod tests {
2083 use super::*;
2084 use crate::request::InfoQueryType;
2085
2086 #[test]
2087 fn test_group_subject_validation() {
2088 let subject = GroupSubject::new("Test Group").unwrap();
2089 assert_eq!(subject.as_str(), "Test Group");
2090
2091 let at_limit = "a".repeat(GROUP_SUBJECT_MAX_LENGTH);
2092 assert!(GroupSubject::new(&at_limit).is_ok());
2093
2094 let over_limit = "a".repeat(GROUP_SUBJECT_MAX_LENGTH + 1);
2095 assert!(GroupSubject::new(&over_limit).is_err());
2096 }
2097
2098 #[test]
2099 fn test_group_description_validation() {
2100 let desc = GroupDescription::new("Test Description").unwrap();
2101 assert_eq!(desc.as_str(), "Test Description");
2102
2103 let at_limit = "a".repeat(GROUP_DESCRIPTION_MAX_LENGTH);
2104 assert!(GroupDescription::new(&at_limit).is_ok());
2105
2106 let over_limit = "a".repeat(GROUP_DESCRIPTION_MAX_LENGTH + 1);
2107 assert!(GroupDescription::new(&over_limit).is_err());
2108 }
2109
2110 #[test]
2111 fn test_string_enum_member_add_mode() {
2112 assert_eq!(MemberAddMode::AdminAdd.as_str(), "admin_add");
2113 assert_eq!(MemberAddMode::AllMemberAdd.as_str(), "all_member_add");
2114 assert_eq!(
2115 MemberAddMode::try_from("admin_add").unwrap(),
2116 MemberAddMode::AdminAdd
2117 );
2118 assert!(MemberAddMode::try_from("invalid").is_err());
2119 }
2120
2121 #[test]
2122 fn test_string_enum_member_link_mode() {
2123 assert_eq!(MemberLinkMode::AdminLink.as_str(), "admin_link");
2124 assert_eq!(MemberLinkMode::AllMemberLink.as_str(), "all_member_link");
2125 assert_eq!(
2126 MemberLinkMode::try_from("admin_link").unwrap(),
2127 MemberLinkMode::AdminLink
2128 );
2129 }
2130
2131 #[test]
2132 fn test_participant_type_is_admin() {
2133 assert!(!ParticipantType::Member.is_admin());
2134 assert!(ParticipantType::Admin.is_admin());
2135 assert!(ParticipantType::SuperAdmin.is_admin());
2136 }
2137
2138 #[test]
2139 fn test_normalize_participants_drops_phone_for_pn() {
2140 let pn_jid: Jid = "15551234567@s.whatsapp.net".parse().unwrap();
2141 let lid_jid: Jid = "100000000000001@lid".parse().unwrap();
2142 let phone_jid: Jid = "15550000001@s.whatsapp.net".parse().unwrap();
2143
2144 let participants = vec![
2145 GroupParticipantOptions::new(pn_jid.clone()).with_phone_number(phone_jid.clone()),
2146 GroupParticipantOptions::new(lid_jid.clone()).with_phone_number(phone_jid.clone()),
2147 ];
2148
2149 let normalized = normalize_participants(&participants);
2150 assert!(normalized[0].phone_number.is_none());
2151 assert_eq!(normalized[0].jid, pn_jid);
2152 assert_eq!(normalized[1].phone_number.as_ref(), Some(&phone_jid));
2153 }
2154
2155 #[test]
2156 fn test_build_create_group_node() {
2157 let pn_jid: Jid = "15551234567@s.whatsapp.net".parse().unwrap();
2158 let options = GroupCreateOptions::new("Test Subject")
2159 .with_participant(GroupParticipantOptions::from_phone(pn_jid))
2160 .with_member_link_mode(MemberLinkMode::AllMemberLink)
2161 .with_member_add_mode(MemberAddMode::AdminAdd);
2162
2163 let node = build_create_group_node(&options);
2164 assert_eq!(node.tag, "create");
2165 assert_eq!(
2166 node.attrs().optional_string("subject").as_deref(),
2167 Some("Test Subject")
2168 );
2169
2170 let link_mode = node.get_children_by_tag("member_link_mode").next().unwrap();
2171 assert_eq!(
2172 link_mode.content.as_ref().and_then(|c| match c {
2173 NodeContent::String(s) => Some(s.as_str()),
2174 _ => None,
2175 }),
2176 Some("all_member_link")
2177 );
2178 }
2179
2180 #[test]
2181 fn test_typed_builder() {
2182 let options: GroupCreateOptions = GroupCreateOptions::builder()
2183 .subject("My Group")
2184 .member_add_mode(MemberAddMode::AdminAdd)
2185 .build();
2186
2187 assert_eq!(options.subject, "My Group");
2188 assert_eq!(options.member_add_mode, Some(MemberAddMode::AdminAdd));
2189 }
2190
2191 #[test]
2192 fn test_set_group_description_with_id_and_prev() {
2193 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
2194 let desc = GroupDescription::new("New description").unwrap();
2195 let spec = SetGroupDescriptionIq::new(&jid, Some(desc), Some("AABBCCDD".to_string()));
2196 let iq = spec.build_iq();
2197
2198 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2199 let desc_node = &nodes[0];
2200 assert_eq!(desc_node.tag, "description");
2201 let id = desc_node.attrs().optional_string("id").unwrap();
2203 assert_eq!(id.len(), 8);
2204 assert_eq!(
2205 desc_node.attrs().optional_string("prev").as_deref(),
2206 Some("AABBCCDD")
2207 );
2208 assert!(desc_node.get_children_by_tag("body").next().is_some());
2210 } else {
2211 panic!("expected nodes content");
2212 }
2213 }
2214
2215 #[test]
2216 fn test_set_group_description_delete() {
2217 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
2218 let spec = SetGroupDescriptionIq::new(&jid, None, Some("PREV1234".to_string()));
2219 let iq = spec.build_iq();
2220
2221 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2222 let desc_node = &nodes[0];
2223 assert_eq!(desc_node.tag, "description");
2224 assert_eq!(
2225 desc_node.attrs().optional_string("delete").as_deref(),
2226 Some("true")
2227 );
2228 assert_eq!(
2229 desc_node.attrs().optional_string("prev").as_deref(),
2230 Some("PREV1234")
2231 );
2232 assert!(desc_node.attrs().optional_string("id").is_some());
2234 } else {
2235 panic!("expected nodes content");
2236 }
2237 }
2238
2239 #[test]
2240 fn test_leave_group_iq() {
2241 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
2242 let spec = LeaveGroupIq::new(&jid);
2243 let iq = spec.build_iq();
2244
2245 assert_eq!(iq.namespace, GROUP_IQ_NAMESPACE);
2246 assert_eq!(iq.query_type, InfoQueryType::Set);
2247 assert_eq!(iq.to.server, GROUP_SERVER);
2249 }
2250
2251 #[test]
2252 fn test_add_participants_iq() {
2253 let group: Jid = "120363000000000001@g.us".parse().unwrap();
2254 let p1: Jid = "1234567890@s.whatsapp.net".parse().unwrap();
2255 let p2: Jid = "9876543210@s.whatsapp.net".parse().unwrap();
2256 let spec = AddParticipantsIq::new(&group, &[p1, p2]);
2257 let iq = spec.build_iq();
2258
2259 assert_eq!(iq.namespace, GROUP_IQ_NAMESPACE);
2260 assert_eq!(iq.to, group);
2261
2262 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2263 let add_node = &nodes[0];
2264 assert_eq!(add_node.tag, "add");
2265 let participants: Vec<_> = add_node.get_children_by_tag("participant").collect();
2266 assert_eq!(participants.len(), 2);
2267 } else {
2268 panic!("expected nodes content");
2269 }
2270 }
2271
2272 #[test]
2273 fn test_promote_demote_iq() {
2274 let group: Jid = "120363000000000001@g.us".parse().unwrap();
2275 let p1: Jid = "1234567890@s.whatsapp.net".parse().unwrap();
2276
2277 let promote = PromoteParticipantsIq::new(&group, std::slice::from_ref(&p1));
2278 let iq = promote.build_iq();
2279 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2280 assert_eq!(nodes[0].tag, "promote");
2281 } else {
2282 panic!("expected nodes content");
2283 }
2284
2285 let demote = DemoteParticipantsIq::new(&group, &[p1]);
2286 let iq = demote.build_iq();
2287 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2288 assert_eq!(nodes[0].tag, "demote");
2289 } else {
2290 panic!("expected nodes content");
2291 }
2292 }
2293
2294 #[test]
2295 fn test_get_group_invite_link_iq() {
2296 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
2297 let spec = GetGroupInviteLinkIq::new(&jid, false);
2298 let iq = spec.build_iq();
2299
2300 assert_eq!(iq.query_type, InfoQueryType::Get);
2301 assert_eq!(iq.to, jid);
2302
2303 let reset_spec = GetGroupInviteLinkIq::new(&jid, true);
2305 assert_eq!(reset_spec.build_iq().query_type, InfoQueryType::Set);
2306 }
2307
2308 #[test]
2309 fn test_get_group_invite_link_parse_response() {
2310 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
2311 let spec = GetGroupInviteLinkIq::new(&jid, false);
2312
2313 let response = NodeBuilder::new("response")
2314 .children([NodeBuilder::new("invite")
2315 .attr("code", "AbCdEfGhIjKl")
2316 .build()])
2317 .build();
2318
2319 let result = spec.parse_response(&response).unwrap();
2320 assert_eq!(result, "https://chat.whatsapp.com/AbCdEfGhIjKl");
2321 }
2322
2323 #[test]
2324 fn test_participant_change_response_parse() {
2325 let node = NodeBuilder::new("participant")
2326 .attr("jid", "1234567890@s.whatsapp.net")
2327 .attr("type", "200")
2328 .build();
2329
2330 let result = ParticipantChangeResponse::try_from_node(&node).unwrap();
2331 assert_eq!(result.jid.user, "1234567890");
2332 assert_eq!(result.status, Some("200".to_string()));
2333 }
2334
2335 #[test]
2336 fn test_set_group_locked_iq() {
2337 let group: Jid = "120363000000000001@g.us".parse().unwrap();
2338
2339 let lock = SetGroupLockedIq::lock(&group);
2340 let iq = lock.build_iq();
2341 assert_eq!(iq.query_type, InfoQueryType::Set);
2342 assert_eq!(iq.to, group);
2343 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2344 assert_eq!(nodes[0].tag, "locked");
2345 } else {
2346 panic!("expected nodes content");
2347 }
2348
2349 let unlock = SetGroupLockedIq::unlock(&group);
2350 let iq = unlock.build_iq();
2351 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2352 assert_eq!(nodes[0].tag, "unlocked");
2353 } else {
2354 panic!("expected nodes content");
2355 }
2356 }
2357
2358 #[test]
2359 fn test_set_group_announcement_iq() {
2360 let group: Jid = "120363000000000001@g.us".parse().unwrap();
2361
2362 let announce = SetGroupAnnouncementIq::announce(&group);
2363 let iq = announce.build_iq();
2364 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2365 assert_eq!(nodes[0].tag, "announcement");
2366 } else {
2367 panic!("expected nodes content");
2368 }
2369
2370 let not_announce = SetGroupAnnouncementIq::unannounce(&group);
2371 let iq = not_announce.build_iq();
2372 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2373 assert_eq!(nodes[0].tag, "not_announcement");
2374 } else {
2375 panic!("expected nodes content");
2376 }
2377 }
2378
2379 #[test]
2380 fn test_set_group_ephemeral_iq() {
2381 let group: Jid = "120363000000000001@g.us".parse().unwrap();
2382
2383 let enable = SetGroupEphemeralIq::enable(&group, NonZeroU32::new(86400).unwrap());
2384 let iq = enable.build_iq();
2385 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2386 assert_eq!(nodes[0].tag, "ephemeral");
2387 assert_eq!(
2388 nodes[0].attrs().optional_string("expiration").as_deref(),
2389 Some("86400")
2390 );
2391 } else {
2392 panic!("expected nodes content");
2393 }
2394
2395 let disable = SetGroupEphemeralIq::disable(&group);
2396 let iq = disable.build_iq();
2397 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2398 assert_eq!(nodes[0].tag, "not_ephemeral");
2399 } else {
2400 panic!("expected nodes content");
2401 }
2402 }
2403
2404 #[test]
2405 fn test_set_group_membership_approval_iq() {
2406 let group: Jid = "120363000000000001@g.us".parse().unwrap();
2407
2408 let spec = SetGroupMembershipApprovalIq::new(&group, MembershipApprovalMode::On);
2409 let iq = spec.build_iq();
2410 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2411 assert_eq!(nodes[0].tag, "membership_approval_mode");
2412 let join = nodes[0].get_children_by_tag("group_join").next().unwrap();
2413 assert!(join.attrs.get("state").is_some_and(|v| v == "on"));
2414 } else {
2415 panic!("expected nodes content");
2416 }
2417 }
2418
2419 #[test]
2424 fn test_build_create_community_node() {
2425 let options = GroupCreateOptions {
2426 subject: "My Community".to_string(),
2427 is_parent: true,
2428 closed: true,
2429 allow_non_admin_sub_group_creation: true,
2430 create_general_chat: true,
2431 ..Default::default()
2432 };
2433
2434 let node = build_create_group_node(&options);
2435 assert_eq!(node.tag, "create");
2436
2437 let parent = node.get_children_by_tag("parent").next().unwrap();
2439 assert_eq!(
2440 parent
2441 .attrs()
2442 .optional_string("default_membership_approval_mode")
2443 .as_deref(),
2444 Some("request_required")
2445 );
2446
2447 assert!(
2448 node.get_children_by_tag("allow_non_admin_sub_group_creation")
2449 .next()
2450 .is_some()
2451 );
2452 assert!(
2453 node.get_children_by_tag("create_general_chat")
2454 .next()
2455 .is_some()
2456 );
2457 }
2458
2459 #[test]
2460 fn test_build_create_non_community_omits_parent() {
2461 let options = GroupCreateOptions {
2462 subject: "Regular Group".to_string(),
2463 is_parent: false,
2464 ..Default::default()
2465 };
2466
2467 let node = build_create_group_node(&options);
2468 assert!(
2469 node.get_children_by_tag("parent").next().is_none(),
2470 "non-community group should not have <parent>"
2471 );
2472 }
2473
2474 #[test]
2475 fn test_link_subgroups_iq_build() {
2476 let parent: Jid = "120363000000000001@g.us".parse().unwrap();
2477 let sub: Jid = "120363000000000002@g.us".parse().unwrap();
2478
2479 let spec = LinkSubgroupsIq::new(&parent, std::slice::from_ref(&sub));
2480 let iq = spec.build_iq();
2481
2482 assert_eq!(iq.to, parent);
2483 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2484 let links = &nodes[0];
2485 assert_eq!(links.tag, "links");
2486 let link = links.get_children_by_tag("link").next().unwrap();
2487 assert_eq!(
2488 link.attrs().optional_string("link_type").as_deref(),
2489 Some("sub_group")
2490 );
2491 let group = link.get_children_by_tag("group").next().unwrap();
2492 assert_eq!(group.attrs().optional_jid("jid"), Some(sub));
2493 } else {
2494 panic!("expected nodes content");
2495 }
2496 }
2497
2498 #[test]
2499 fn test_link_subgroups_iq_parse_response() {
2500 let parent: Jid = "120363000000000001@g.us".parse().unwrap();
2501 let sub: Jid = "120363000000000002@g.us".parse().unwrap();
2502
2503 let response = NodeBuilder::new("iq")
2504 .children([NodeBuilder::new("links")
2505 .children([NodeBuilder::new("link")
2506 .attr("link_type", "sub_group")
2507 .children([NodeBuilder::new("group")
2508 .attr("jid", sub.to_string())
2509 .build()])
2510 .build()])
2511 .build()])
2512 .build();
2513
2514 let spec = LinkSubgroupsIq::new(&parent, std::slice::from_ref(&sub));
2515 let result = spec.parse_response(&response).unwrap();
2516 assert_eq!(result.groups.len(), 1);
2517 assert_eq!(result.groups[0].jid, sub);
2518 assert!(result.groups[0].error.is_none());
2519 }
2520
2521 #[test]
2522 fn test_unlink_subgroups_iq_build() {
2523 let parent: Jid = "120363000000000001@g.us".parse().unwrap();
2524 let sub: Jid = "120363000000000002@g.us".parse().unwrap();
2525
2526 let spec = UnlinkSubgroupsIq::new(&parent, std::slice::from_ref(&sub), true);
2527 let iq = spec.build_iq();
2528
2529 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2530 let unlink = &nodes[0];
2531 assert_eq!(unlink.tag, "unlink");
2532 assert_eq!(
2533 unlink.attrs().optional_string("unlink_type").as_deref(),
2534 Some("sub_group")
2535 );
2536 let group = unlink.get_children_by_tag("group").next().unwrap();
2537 assert_eq!(group.attrs().optional_jid("jid"), Some(sub));
2538 assert_eq!(
2539 group
2540 .attrs()
2541 .optional_string("remove_orphaned_members")
2542 .as_deref(),
2543 Some("true")
2544 );
2545 } else {
2546 panic!("expected nodes content");
2547 }
2548 }
2549
2550 #[test]
2551 fn test_unlink_subgroups_iq_parse_response_with_error() {
2552 let parent: Jid = "120363000000000001@g.us".parse().unwrap();
2553 let sub: Jid = "120363000000000002@g.us".parse().unwrap();
2554
2555 let response = NodeBuilder::new("iq")
2556 .children([NodeBuilder::new("unlink")
2557 .attr("unlink_type", "sub_group")
2558 .children([NodeBuilder::new("group")
2559 .attr("jid", sub.to_string())
2560 .attr("error", "406")
2561 .build()])
2562 .build()])
2563 .build();
2564
2565 let spec = UnlinkSubgroupsIq::new(&parent, std::slice::from_ref(&sub), false);
2566 let result = spec.parse_response(&response).unwrap();
2567 assert_eq!(result.groups.len(), 1);
2568 assert_eq!(result.groups[0].jid, sub);
2569 assert_eq!(result.groups[0].error, Some(406));
2570 }
2571
2572 #[test]
2573 fn test_delete_community_iq_build() {
2574 let parent: Jid = "120363000000000001@g.us".parse().unwrap();
2575 let spec = DeleteCommunityIq::new(&parent);
2576 let iq = spec.build_iq();
2577
2578 assert_eq!(iq.to, parent);
2579 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2580 assert_eq!(nodes[0].tag, "delete_parent");
2581 } else {
2582 panic!("expected nodes content");
2583 }
2584 }
2585
2586 #[test]
2587 fn test_query_linked_group_iq_build() {
2588 let parent: Jid = "120363000000000001@g.us".parse().unwrap();
2589 let sub: Jid = "120363000000000002@g.us".parse().unwrap();
2590
2591 let spec = QueryLinkedGroupIq::new(&parent, &sub);
2592 let iq = spec.build_iq();
2593
2594 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2595 let query = &nodes[0];
2596 assert_eq!(query.tag, "query_linked");
2597 assert_eq!(
2598 query.attrs().optional_string("type").as_deref(),
2599 Some("sub_group")
2600 );
2601 assert_eq!(query.attrs().optional_jid("jid"), Some(sub));
2602 } else {
2603 panic!("expected nodes content");
2604 }
2605 }
2606
2607 #[test]
2608 fn test_join_linked_group_iq_build() {
2609 let parent: Jid = "120363000000000001@g.us".parse().unwrap();
2610 let sub: Jid = "120363000000000002@g.us".parse().unwrap();
2611
2612 let spec = JoinLinkedGroupIq::new(&parent, &sub);
2613 let iq = spec.build_iq();
2614
2615 assert_eq!(iq.to, parent);
2616 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2617 let join = &nodes[0];
2618 assert_eq!(join.tag, "join_linked_group");
2619 assert_eq!(join.attrs().optional_jid("jid"), Some(sub));
2620 } else {
2621 panic!("expected nodes content");
2622 }
2623 }
2624
2625 #[test]
2626 fn test_get_linked_groups_participants_iq_build() {
2627 let parent: Jid = "120363000000000001@g.us".parse().unwrap();
2628 let spec = GetLinkedGroupsParticipantsIq::new(&parent);
2629 let iq = spec.build_iq();
2630
2631 assert_eq!(iq.to, parent);
2632 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
2633 assert_eq!(nodes[0].tag, "linked_groups_participants");
2634 } else {
2635 panic!("expected nodes content");
2636 }
2637 }
2638
2639 #[test]
2640 fn test_group_info_response_parses_community_fields() {
2641 let node = NodeBuilder::new("group")
2642 .attr("id", "120363000000000001@g.us")
2643 .attr("subject", "My Community")
2644 .children([
2645 NodeBuilder::new("parent").build(),
2646 NodeBuilder::new("allow_non_admin_sub_group_creation").build(),
2647 ])
2648 .build();
2649
2650 let response = GroupInfoResponse::try_from_node(&node).unwrap();
2651 assert!(response.is_parent_group);
2652 assert!(response.allow_non_admin_sub_group_creation);
2653 assert!(response.parent_group_jid.is_none());
2654 assert!(!response.is_default_sub_group);
2655 assert!(!response.is_general_chat);
2656 }
2657
2658 #[test]
2659 fn test_group_info_response_parses_subgroup_fields() {
2660 let parent_jid = "120363000000000001@g.us";
2661 let node = NodeBuilder::new("group")
2662 .attr("id", "120363000000000002@g.us")
2663 .attr("subject", "Sub Group")
2664 .children([
2665 NodeBuilder::new("linked_parent")
2666 .attr("jid", parent_jid)
2667 .build(),
2668 NodeBuilder::new("default_sub_group").build(),
2669 ])
2670 .build();
2671
2672 let response = GroupInfoResponse::try_from_node(&node).unwrap();
2673 assert!(!response.is_parent_group);
2674 assert!(response.is_default_sub_group);
2675 assert_eq!(response.parent_group_jid, Some(parent_jid.parse().unwrap()));
2676 }
2677}