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_ng::builder::NodeBuilder;
10use wacore_binary_ng::jid::{GROUP_SERVER, Jid};
11use wacore_binary_ng::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}
161
162impl GroupCreateOptions {
163 pub fn new(subject: impl Into<String>) -> Self {
165 Self {
166 subject: subject.into(),
167 ..Default::default()
168 }
169 }
170
171 pub fn with_participant(mut self, participant: GroupParticipantOptions) -> Self {
172 self.participants.push(participant);
173 self
174 }
175
176 pub fn with_participants(mut self, participants: Vec<GroupParticipantOptions>) -> Self {
177 self.participants = participants;
178 self
179 }
180
181 pub fn with_member_link_mode(mut self, mode: MemberLinkMode) -> Self {
182 self.member_link_mode = Some(mode);
183 self
184 }
185
186 pub fn with_member_add_mode(mut self, mode: MemberAddMode) -> Self {
187 self.member_add_mode = Some(mode);
188 self
189 }
190
191 pub fn with_membership_approval_mode(mut self, mode: MembershipApprovalMode) -> Self {
192 self.membership_approval_mode = Some(mode);
193 self
194 }
195
196 pub fn with_ephemeral_expiration(mut self, expiration: u32) -> Self {
197 self.ephemeral_expiration = Some(expiration);
198 self
199 }
200}
201
202impl Default for GroupCreateOptions {
203 fn default() -> Self {
204 Self {
205 subject: String::new(),
206 participants: Vec::new(),
207 member_link_mode: Some(MemberLinkMode::AdminLink),
208 member_add_mode: Some(MemberAddMode::AllMemberAdd),
209 membership_approval_mode: Some(MembershipApprovalMode::Off),
210 ephemeral_expiration: Some(0),
211 }
212 }
213}
214
215pub fn normalize_participants(
217 participants: &[GroupParticipantOptions],
218) -> Vec<GroupParticipantOptions> {
219 participants
220 .iter()
221 .cloned()
222 .map(|p| {
223 if !p.jid.is_lid() && p.phone_number.is_some() {
224 GroupParticipantOptions {
225 phone_number: None,
226 ..p
227 }
228 } else {
229 p
230 }
231 })
232 .collect()
233}
234
235pub fn build_create_group_node(options: &GroupCreateOptions) -> Node {
237 let mut children = Vec::new();
238
239 if let Some(link_mode) = &options.member_link_mode {
240 children.push(
241 NodeBuilder::new("member_link_mode")
242 .string_content(link_mode.as_str())
243 .build(),
244 );
245 }
246
247 if let Some(add_mode) = &options.member_add_mode {
248 children.push(
249 NodeBuilder::new("member_add_mode")
250 .string_content(add_mode.as_str())
251 .build(),
252 );
253 }
254
255 let participants = normalize_participants(&options.participants);
257
258 for participant in &participants {
259 let mut attrs = vec![("jid", participant.jid.to_string())];
260 if let Some(pn) = &participant.phone_number {
261 attrs.push(("phone_number", pn.to_string()));
262 }
263
264 let participant_node = if let Some(privacy_bytes) = &participant.privacy {
265 NodeBuilder::new("participant")
266 .attrs(attrs)
267 .children([NodeBuilder::new("privacy")
268 .string_content(hex::encode(privacy_bytes))
269 .build()])
270 .build()
271 } else {
272 NodeBuilder::new("participant").attrs(attrs).build()
273 };
274 children.push(participant_node);
275 }
276
277 if let Some(expiration) = &options.ephemeral_expiration {
278 children.push(
279 NodeBuilder::new("ephemeral")
280 .attr("expiration", expiration.to_string())
281 .build(),
282 );
283 }
284
285 if let Some(approval_mode) = &options.membership_approval_mode {
286 children.push(
287 NodeBuilder::new("membership_approval_mode")
288 .children([NodeBuilder::new("group_join")
289 .attr("state", approval_mode.as_str())
290 .build()])
291 .build(),
292 );
293 }
294
295 NodeBuilder::new("create")
296 .attr("subject", &options.subject)
297 .children(children)
298 .build()
299}
300#[derive(Debug, Clone, crate::ProtocolNode)]
304#[protocol(tag = "query")]
305pub struct GroupQueryRequest {
306 #[attr(name = "request", string_enum)]
307 pub request: GroupQueryRequestType,
308}
309
310#[derive(Debug, Clone)]
312pub struct GroupParticipantResponse {
313 pub jid: Jid,
314 pub phone_number: Option<Jid>,
315 pub participant_type: ParticipantType,
316}
317
318impl ProtocolNode for GroupParticipantResponse {
319 fn tag(&self) -> &'static str {
320 "participant"
321 }
322
323 fn into_node(self) -> Node {
324 let mut builder = NodeBuilder::new("participant").attr("jid", self.jid.to_string());
325 if let Some(pn) = &self.phone_number {
326 builder = builder.attr("phone_number", pn.to_string());
327 }
328 if self.participant_type != ParticipantType::Member {
329 builder = builder.attr("type", self.participant_type.as_str());
330 }
331 builder.build()
332 }
333
334 fn try_from_node(node: &Node) -> Result<Self> {
335 if node.tag != "participant" {
336 return Err(anyhow!("expected <participant>, got <{}>", node.tag));
337 }
338 let jid = node
339 .attrs()
340 .optional_jid("jid")
341 .ok_or_else(|| anyhow!("participant missing required 'jid' attribute"))?;
342 let phone_number = node.attrs().optional_jid("phone_number");
343 let participant_type = ParticipantType::try_from(node.attrs().optional_string("type"))
345 .unwrap_or(ParticipantType::Member);
346
347 Ok(Self {
348 jid,
349 phone_number,
350 participant_type,
351 })
352 }
353}
354
355#[derive(Debug, Clone)]
357pub struct GroupInfoResponse {
358 pub id: Jid,
359 pub subject: GroupSubject,
360 pub addressing_mode: AddressingMode,
361 pub participants: Vec<GroupParticipantResponse>,
362 pub creator: Option<Jid>,
364 pub creation_time: Option<u64>,
366 pub subject_time: Option<u64>,
368 pub subject_owner: Option<Jid>,
370 pub description: Option<String>,
372 pub description_id: Option<String>,
374 pub is_locked: bool,
376 pub is_announcement: bool,
378 pub ephemeral_expiration: u32,
380 pub membership_approval: bool,
382 pub member_add_mode: Option<MemberAddMode>,
384 pub member_link_mode: Option<MemberLinkMode>,
386 pub size: Option<u32>,
388}
389
390impl ProtocolNode for GroupInfoResponse {
391 fn tag(&self) -> &'static str {
392 "group"
393 }
394
395 fn into_node(self) -> Node {
396 let mut children: Vec<Node> = self
397 .participants
398 .into_iter()
399 .map(|p| p.into_node())
400 .collect();
401
402 if self.is_locked {
403 children.push(NodeBuilder::new("locked").build());
404 }
405 if self.is_announcement {
406 children.push(NodeBuilder::new("announcement").build());
407 }
408 if self.ephemeral_expiration > 0 {
409 children.push(
410 NodeBuilder::new("ephemeral")
411 .attr("expiration", self.ephemeral_expiration.to_string())
412 .build(),
413 );
414 }
415 if self.membership_approval {
416 children.push(
417 NodeBuilder::new("membership_approval_mode")
418 .children(vec![
419 NodeBuilder::new("group_join").attr("state", "on").build(),
420 ])
421 .build(),
422 );
423 }
424 if let Some(ref add_mode) = self.member_add_mode {
425 children.push(
426 NodeBuilder::new("member_add_mode")
427 .string_content(add_mode.as_str())
428 .build(),
429 );
430 }
431 if let Some(ref link_mode) = self.member_link_mode {
432 children.push(
433 NodeBuilder::new("member_link_mode")
434 .string_content(link_mode.as_str())
435 .build(),
436 );
437 }
438 if let Some(ref desc) = self.description {
439 let mut desc_builder = NodeBuilder::new("description");
440 if let Some(ref desc_id) = self.description_id {
441 desc_builder = desc_builder.attr("id", desc_id.as_str());
442 }
443 children.push(desc_builder.string_content(desc.as_str()).build());
444 }
445
446 let mut builder = NodeBuilder::new("group")
447 .attr("id", self.id.to_string())
448 .attr("subject", self.subject.as_str())
449 .attr("addressing_mode", self.addressing_mode.as_str());
450
451 if let Some(ref creator) = self.creator {
452 builder = builder.attr("creator", creator.to_string());
453 }
454 if let Some(creation_time) = self.creation_time {
455 builder = builder.attr("creation", creation_time.to_string());
456 }
457 if let Some(subject_time) = self.subject_time {
458 builder = builder.attr("s_t", subject_time.to_string());
459 }
460 if let Some(ref subject_owner) = self.subject_owner {
461 builder = builder.attr("s_o", subject_owner.to_string());
462 }
463 if let Some(size) = self.size {
464 builder = builder.attr("size", size.to_string());
465 }
466
467 builder.children(children).build()
468 }
469
470 fn try_from_node(node: &Node) -> Result<Self> {
471 if node.tag != "group" {
472 return Err(anyhow!("expected <group>, got <{}>", node.tag));
473 }
474
475 let id_str = required_attr(node, "id")?;
476 let id = if id_str.contains('@') {
477 id_str.parse()?
478 } else {
479 Jid::group(id_str)
480 };
481
482 let subject =
483 GroupSubject::new_unchecked(optional_attr(node, "subject").unwrap_or_default());
484
485 let addressing_mode =
486 AddressingMode::try_from(optional_attr(node, "addressing_mode").unwrap_or("pn"))?;
487
488 let participants = collect_children::<GroupParticipantResponse>(node, "participant")?;
489
490 let creator = node
492 .attrs()
493 .optional_string("creator")
494 .and_then(|s| s.parse::<Jid>().ok());
495 let creation_time = node
496 .attrs()
497 .optional_string("creation")
498 .and_then(|s| s.parse::<u64>().ok());
499 let subject_time = node
500 .attrs()
501 .optional_string("s_t")
502 .and_then(|s| s.parse::<u64>().ok());
503 let subject_owner = node
504 .attrs()
505 .optional_string("s_o")
506 .and_then(|s| s.parse::<Jid>().ok());
507 let size = node
508 .attrs()
509 .optional_string("size")
510 .and_then(|s| s.parse::<u32>().ok());
511
512 let is_locked = node.get_optional_child_by_tag(&["locked"]).is_some();
514 let is_announcement = node.get_optional_child_by_tag(&["announcement"]).is_some();
515
516 let ephemeral_expiration = node
517 .get_optional_child_by_tag(&["ephemeral"])
518 .and_then(|n| n.attrs().optional_string("expiration"))
519 .and_then(|s| s.parse::<u32>().ok())
520 .unwrap_or(0);
521
522 let membership_approval = node
523 .get_optional_child_by_tag(&["membership_approval_mode", "group_join"])
524 .and_then(|n| n.attrs().optional_string("state"))
525 .is_some_and(|s| s == "on");
526
527 let member_add_mode = node
528 .get_optional_child_by_tag(&["member_add_mode"])
529 .and_then(|n| match &n.content {
530 Some(NodeContent::String(s)) => MemberAddMode::try_from(s.as_str()).ok(),
531 _ => None,
532 });
533
534 let member_link_mode = node
535 .get_optional_child_by_tag(&["member_link_mode"])
536 .and_then(|n| match &n.content {
537 Some(NodeContent::String(s)) => MemberLinkMode::try_from(s.as_str()).ok(),
538 _ => None,
539 });
540
541 let description_node = node.get_optional_child_by_tag(&["description"]);
543 let description = description_node.and_then(|n| match &n.content {
544 Some(NodeContent::String(s)) => Some(s.clone()),
545 _ => None,
546 });
547 let description_id = description_node
548 .and_then(|n| n.attrs().optional_string("id"))
549 .map(|s| s.to_string());
550
551 Ok(Self {
552 id,
553 subject,
554 addressing_mode,
555 participants,
556 creator,
557 creation_time,
558 subject_time,
559 subject_owner,
560 description,
561 description_id,
562 is_locked,
563 is_announcement,
564 ephemeral_expiration,
565 membership_approval,
566 member_add_mode,
567 member_link_mode,
568 size,
569 })
570 }
571}
572#[derive(Debug, Clone)]
574pub struct GroupParticipatingRequest {
575 pub include_participants: bool,
576 pub include_description: bool,
577}
578
579impl GroupParticipatingRequest {
580 pub fn new() -> Self {
581 Self {
582 include_participants: true,
583 include_description: true,
584 }
585 }
586}
587
588impl Default for GroupParticipatingRequest {
589 fn default() -> Self {
590 Self::new()
591 }
592}
593
594impl ProtocolNode for GroupParticipatingRequest {
595 fn tag(&self) -> &'static str {
596 "participating"
597 }
598
599 fn into_node(self) -> Node {
600 let mut children = Vec::new();
601 if self.include_participants {
602 children.push(NodeBuilder::new("participants").build());
603 }
604 if self.include_description {
605 children.push(NodeBuilder::new("description").build());
606 }
607 NodeBuilder::new("participating").children(children).build()
608 }
609
610 fn try_from_node(node: &Node) -> Result<Self> {
611 if node.tag != "participating" {
612 return Err(anyhow!("expected <participating>, got <{}>", node.tag));
613 }
614 Ok(Self::default())
615 }
616}
617
618#[derive(Debug, Clone, Default)]
620pub struct GroupParticipatingResponse {
621 pub groups: Vec<GroupInfoResponse>,
622}
623
624impl ProtocolNode for GroupParticipatingResponse {
625 fn tag(&self) -> &'static str {
626 "groups"
627 }
628
629 fn into_node(self) -> Node {
630 let children: Vec<Node> = self.groups.into_iter().map(|g| g.into_node()).collect();
631 NodeBuilder::new("groups").children(children).build()
632 }
633
634 fn try_from_node(node: &Node) -> Result<Self> {
635 if node.tag != "groups" {
636 return Err(anyhow!("expected <groups>, got <{}>", node.tag));
637 }
638
639 let groups = collect_children::<GroupInfoResponse>(node, "group")?;
640
641 Ok(Self { groups })
642 }
643}
644#[derive(Debug, Clone)]
646pub struct GroupQueryIq {
647 pub group_jid: Jid,
648}
649
650impl GroupQueryIq {
651 pub fn new(group_jid: &Jid) -> Self {
652 Self {
653 group_jid: group_jid.clone(),
654 }
655 }
656}
657
658impl IqSpec for GroupQueryIq {
659 type Response = GroupInfoResponse;
660
661 fn build_iq(&self) -> InfoQuery<'static> {
662 InfoQuery::get_ref(
663 GROUP_IQ_NAMESPACE,
664 &self.group_jid,
665 Some(NodeContent::Nodes(vec![
666 GroupQueryRequest::default().into_node(),
667 ])),
668 )
669 }
670
671 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
672 let group_node = required_child(response, "group")?;
673 GroupInfoResponse::try_from_node(group_node)
674 }
675}
676
677#[derive(Debug, Clone, Default)]
679pub struct GroupParticipatingIq;
680
681impl GroupParticipatingIq {
682 pub fn new() -> Self {
683 Self
684 }
685}
686
687impl IqSpec for GroupParticipatingIq {
688 type Response = GroupParticipatingResponse;
689
690 fn build_iq(&self) -> InfoQuery<'static> {
691 InfoQuery::get(
692 GROUP_IQ_NAMESPACE,
693 Jid::new("", GROUP_SERVER),
694 Some(NodeContent::Nodes(vec![
695 GroupParticipatingRequest::new().into_node(),
696 ])),
697 )
698 }
699
700 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
701 let groups_node = required_child(response, "groups")?;
702 GroupParticipatingResponse::try_from_node(groups_node)
703 }
704}
705
706#[derive(Debug, Clone)]
708pub struct GroupCreateIq {
709 pub options: GroupCreateOptions,
710}
711
712impl GroupCreateIq {
713 pub fn new(options: GroupCreateOptions) -> Self {
714 Self { options }
715 }
716}
717
718impl IqSpec for GroupCreateIq {
719 type Response = Jid;
720
721 fn build_iq(&self) -> InfoQuery<'static> {
722 InfoQuery::set(
723 GROUP_IQ_NAMESPACE,
724 Jid::new("", GROUP_SERVER),
725 Some(NodeContent::Nodes(vec![build_create_group_node(
726 &self.options,
727 )])),
728 )
729 }
730
731 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
732 let group_node = required_child(response, "group")?;
733 let group_id_str = required_attr(group_node, "id")?;
734
735 if group_id_str.contains('@') {
736 group_id_str.parse().map_err(Into::into)
737 } else {
738 Ok(Jid::group(group_id_str))
739 }
740 }
741}
742
743#[derive(Debug, Clone, crate::ProtocolNode)]
751#[protocol(tag = "participant")]
752pub struct ParticipantChangeResponse {
753 #[attr(name = "jid", jid)]
754 pub jid: Jid,
755 #[attr(name = "type")]
757 pub status: Option<String>,
758 #[attr(name = "error")]
759 pub error: Option<String>,
760}
761
762#[derive(Debug, Clone)]
771pub struct SetGroupSubjectIq {
772 pub group_jid: Jid,
773 pub subject: GroupSubject,
774}
775
776impl SetGroupSubjectIq {
777 pub fn new(group_jid: &Jid, subject: GroupSubject) -> Self {
778 Self {
779 group_jid: group_jid.clone(),
780 subject,
781 }
782 }
783}
784
785impl IqSpec for SetGroupSubjectIq {
786 type Response = ();
787
788 fn build_iq(&self) -> InfoQuery<'static> {
789 InfoQuery::set_ref(
790 GROUP_IQ_NAMESPACE,
791 &self.group_jid,
792 Some(NodeContent::Nodes(vec![
793 NodeBuilder::new("subject")
794 .string_content(self.subject.as_str())
795 .build(),
796 ])),
797 )
798 }
799
800 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
801 Ok(())
802 }
803}
804
805#[derive(Debug, Clone)]
818pub struct SetGroupDescriptionIq {
819 pub group_jid: Jid,
820 pub description: Option<GroupDescription>,
821 pub id: String,
823 pub prev: Option<String>,
825}
826
827impl SetGroupDescriptionIq {
828 pub fn new(
829 group_jid: &Jid,
830 description: Option<GroupDescription>,
831 prev: Option<String>,
832 ) -> Self {
833 use rand::Rng;
834 let id = format!("{:08X}", rand::rng().random::<u32>());
835 Self {
836 group_jid: group_jid.clone(),
837 description,
838 id,
839 prev,
840 }
841 }
842}
843
844impl IqSpec for SetGroupDescriptionIq {
845 type Response = ();
846
847 fn build_iq(&self) -> InfoQuery<'static> {
848 let desc_node = if let Some(ref desc) = self.description {
849 let mut builder = NodeBuilder::new("description").attr("id", &self.id);
850 if let Some(ref prev) = self.prev {
851 builder = builder.attr("prev", prev);
852 }
853 builder
854 .children([NodeBuilder::new("body")
855 .string_content(desc.as_str())
856 .build()])
857 .build()
858 } else {
859 let mut builder = NodeBuilder::new("description")
860 .attr("id", &self.id)
861 .attr("delete", "true");
862 if let Some(ref prev) = self.prev {
863 builder = builder.attr("prev", prev);
864 }
865 builder.build()
866 };
867
868 InfoQuery::set_ref(
869 GROUP_IQ_NAMESPACE,
870 &self.group_jid,
871 Some(NodeContent::Nodes(vec![desc_node])),
872 )
873 }
874
875 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
876 Ok(())
877 }
878}
879
880#[derive(Debug, Clone)]
889pub struct LeaveGroupIq {
890 pub group_jid: Jid,
891}
892
893impl LeaveGroupIq {
894 pub fn new(group_jid: &Jid) -> Self {
895 Self {
896 group_jid: group_jid.clone(),
897 }
898 }
899}
900
901impl IqSpec for LeaveGroupIq {
902 type Response = ();
903
904 fn build_iq(&self) -> InfoQuery<'static> {
905 let group_node = NodeBuilder::new("group")
906 .attr("id", self.group_jid.to_string())
907 .build();
908 let leave_node = NodeBuilder::new("leave").children([group_node]).build();
909
910 InfoQuery::set(
911 GROUP_IQ_NAMESPACE,
912 Jid::new("", GROUP_SERVER),
913 Some(NodeContent::Nodes(vec![leave_node])),
914 )
915 }
916
917 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
918 Ok(())
919 }
920}
921
922macro_rules! define_group_participant_iq {
925 (
926 $(#[$meta:meta])*
927 $name:ident, action = $action:literal, response = Vec<ParticipantChangeResponse>
928 ) => {
929 $(#[$meta])*
930 #[derive(Debug, Clone)]
931 pub struct $name {
932 pub group_jid: Jid,
933 pub participants: Vec<Jid>,
934 }
935
936 impl $name {
937 pub fn new(group_jid: &Jid, participants: &[Jid]) -> Self {
938 Self {
939 group_jid: group_jid.clone(),
940 participants: participants.to_vec(),
941 }
942 }
943 }
944
945 impl IqSpec for $name {
946 type Response = Vec<ParticipantChangeResponse>;
947
948 fn build_iq(&self) -> InfoQuery<'static> {
949 let children: Vec<Node> = self
950 .participants
951 .iter()
952 .map(|jid| {
953 NodeBuilder::new("participant")
954 .attr("jid", jid.to_string())
955 .build()
956 })
957 .collect();
958
959 let action_node = NodeBuilder::new($action).children(children).build();
960
961 InfoQuery::set_ref(
962 GROUP_IQ_NAMESPACE,
963 &self.group_jid,
964 Some(NodeContent::Nodes(vec![action_node])),
965 )
966 }
967
968 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
969 let action_node = required_child(response, $action)?;
970 collect_children::<ParticipantChangeResponse>(action_node, "participant")
971 }
972 }
973 };
974 (
975 $(#[$meta:meta])*
976 $name:ident, action = $action:literal, response = ()
977 ) => {
978 $(#[$meta])*
979 #[derive(Debug, Clone)]
980 pub struct $name {
981 pub group_jid: Jid,
982 pub participants: Vec<Jid>,
983 }
984
985 impl $name {
986 pub fn new(group_jid: &Jid, participants: &[Jid]) -> Self {
987 Self {
988 group_jid: group_jid.clone(),
989 participants: participants.to_vec(),
990 }
991 }
992 }
993
994 impl IqSpec for $name {
995 type Response = ();
996
997 fn build_iq(&self) -> InfoQuery<'static> {
998 let children: Vec<Node> = self
999 .participants
1000 .iter()
1001 .map(|jid| {
1002 NodeBuilder::new("participant")
1003 .attr("jid", jid.to_string())
1004 .build()
1005 })
1006 .collect();
1007
1008 let action_node = NodeBuilder::new($action).children(children).build();
1009
1010 InfoQuery::set_ref(
1011 GROUP_IQ_NAMESPACE,
1012 &self.group_jid,
1013 Some(NodeContent::Nodes(vec![action_node])),
1014 )
1015 }
1016
1017 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
1018 Ok(())
1019 }
1020 }
1021 };
1022}
1023
1024define_group_participant_iq!(
1025 AddParticipantsIq, action = "add", response = Vec<ParticipantChangeResponse>
1034);
1035
1036define_group_participant_iq!(
1037 RemoveParticipantsIq, action = "remove", response = Vec<ParticipantChangeResponse>
1046);
1047
1048define_group_participant_iq!(
1049 PromoteParticipantsIq, action = "promote", response = ()
1058);
1059
1060define_group_participant_iq!(
1061 DemoteParticipantsIq, action = "demote", response = ()
1070);
1071
1072#[derive(Debug, Clone)]
1079pub struct GetGroupInviteLinkIq {
1080 pub group_jid: Jid,
1081 pub reset: bool,
1082}
1083
1084impl GetGroupInviteLinkIq {
1085 pub fn new(group_jid: &Jid, reset: bool) -> Self {
1086 Self {
1087 group_jid: group_jid.clone(),
1088 reset,
1089 }
1090 }
1091}
1092
1093impl IqSpec for GetGroupInviteLinkIq {
1094 type Response = String;
1095
1096 fn build_iq(&self) -> InfoQuery<'static> {
1097 let content = Some(NodeContent::Nodes(vec![NodeBuilder::new("invite").build()]));
1098 if self.reset {
1099 InfoQuery::set_ref(GROUP_IQ_NAMESPACE, &self.group_jid, content)
1100 } else {
1101 InfoQuery::get_ref(GROUP_IQ_NAMESPACE, &self.group_jid, content)
1102 }
1103 }
1104
1105 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
1106 let invite_node = required_child(response, "invite")?;
1107 let code = required_attr(invite_node, "code")?;
1108 Ok(format!("https://chat.whatsapp.com/{code}"))
1109 }
1110}
1111
1112#[derive(Debug, Clone)]
1132pub struct SetGroupLockedIq {
1133 pub group_jid: Jid,
1134 pub locked: bool,
1135}
1136
1137impl SetGroupLockedIq {
1138 pub fn lock(group_jid: &Jid) -> Self {
1139 Self {
1140 group_jid: group_jid.clone(),
1141 locked: true,
1142 }
1143 }
1144
1145 pub fn unlock(group_jid: &Jid) -> Self {
1146 Self {
1147 group_jid: group_jid.clone(),
1148 locked: false,
1149 }
1150 }
1151}
1152
1153impl IqSpec for SetGroupLockedIq {
1154 type Response = ();
1155
1156 fn build_iq(&self) -> InfoQuery<'static> {
1157 let tag = if self.locked { "locked" } else { "unlocked" };
1158 InfoQuery::set_ref(
1159 GROUP_IQ_NAMESPACE,
1160 &self.group_jid,
1161 Some(NodeContent::Nodes(vec![NodeBuilder::new(tag).build()])),
1162 )
1163 }
1164
1165 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
1166 Ok(())
1167 }
1168}
1169
1170#[derive(Debug, Clone)]
1181pub struct SetGroupAnnouncementIq {
1182 pub group_jid: Jid,
1183 pub announce: bool,
1184}
1185
1186impl SetGroupAnnouncementIq {
1187 pub fn announce(group_jid: &Jid) -> Self {
1188 Self {
1189 group_jid: group_jid.clone(),
1190 announce: true,
1191 }
1192 }
1193
1194 pub fn unannounce(group_jid: &Jid) -> Self {
1195 Self {
1196 group_jid: group_jid.clone(),
1197 announce: false,
1198 }
1199 }
1200}
1201
1202impl IqSpec for SetGroupAnnouncementIq {
1203 type Response = ();
1204
1205 fn build_iq(&self) -> InfoQuery<'static> {
1206 let tag = if self.announce {
1207 "announcement"
1208 } else {
1209 "not_announcement"
1210 };
1211 InfoQuery::set_ref(
1212 GROUP_IQ_NAMESPACE,
1213 &self.group_jid,
1214 Some(NodeContent::Nodes(vec![NodeBuilder::new(tag).build()])),
1215 )
1216 }
1217
1218 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
1219 Ok(())
1220 }
1221}
1222
1223#[derive(Debug, Clone)]
1240pub struct SetGroupEphemeralIq {
1241 pub group_jid: Jid,
1242 pub expiration: Option<NonZeroU32>,
1244}
1245
1246impl SetGroupEphemeralIq {
1247 pub fn enable(group_jid: &Jid, expiration: NonZeroU32) -> Self {
1249 Self {
1250 group_jid: group_jid.clone(),
1251 expiration: Some(expiration),
1252 }
1253 }
1254
1255 pub fn disable(group_jid: &Jid) -> Self {
1257 Self {
1258 group_jid: group_jid.clone(),
1259 expiration: None,
1260 }
1261 }
1262}
1263
1264impl IqSpec for SetGroupEphemeralIq {
1265 type Response = ();
1266
1267 fn build_iq(&self) -> InfoQuery<'static> {
1268 let node = match self.expiration {
1269 Some(exp) => NodeBuilder::new("ephemeral")
1270 .attr("expiration", exp.to_string())
1271 .build(),
1272 None => NodeBuilder::new("not_ephemeral").build(),
1273 };
1274 InfoQuery::set_ref(
1275 GROUP_IQ_NAMESPACE,
1276 &self.group_jid,
1277 Some(NodeContent::Nodes(vec![node])),
1278 )
1279 }
1280
1281 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
1282 Ok(())
1283 }
1284}
1285
1286#[derive(Debug, Clone)]
1299pub struct SetGroupMembershipApprovalIq {
1300 pub group_jid: Jid,
1301 pub mode: MembershipApprovalMode,
1302}
1303
1304impl SetGroupMembershipApprovalIq {
1305 pub fn new(group_jid: &Jid, mode: MembershipApprovalMode) -> Self {
1306 Self {
1307 group_jid: group_jid.clone(),
1308 mode,
1309 }
1310 }
1311}
1312
1313impl IqSpec for SetGroupMembershipApprovalIq {
1314 type Response = ();
1315
1316 fn build_iq(&self) -> InfoQuery<'static> {
1317 let node = NodeBuilder::new("membership_approval_mode")
1318 .children([NodeBuilder::new("group_join")
1319 .attr("state", self.mode.as_str())
1320 .build()])
1321 .build();
1322 InfoQuery::set_ref(
1323 GROUP_IQ_NAMESPACE,
1324 &self.group_jid,
1325 Some(NodeContent::Nodes(vec![node])),
1326 )
1327 }
1328
1329 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
1330 Ok(())
1331 }
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336 use super::*;
1337 use crate::request::InfoQueryType;
1338
1339 #[test]
1340 fn test_group_subject_validation() {
1341 let subject = GroupSubject::new("Test Group").unwrap();
1342 assert_eq!(subject.as_str(), "Test Group");
1343
1344 let at_limit = "a".repeat(GROUP_SUBJECT_MAX_LENGTH);
1345 assert!(GroupSubject::new(&at_limit).is_ok());
1346
1347 let over_limit = "a".repeat(GROUP_SUBJECT_MAX_LENGTH + 1);
1348 assert!(GroupSubject::new(&over_limit).is_err());
1349 }
1350
1351 #[test]
1352 fn test_group_description_validation() {
1353 let desc = GroupDescription::new("Test Description").unwrap();
1354 assert_eq!(desc.as_str(), "Test Description");
1355
1356 let at_limit = "a".repeat(GROUP_DESCRIPTION_MAX_LENGTH);
1357 assert!(GroupDescription::new(&at_limit).is_ok());
1358
1359 let over_limit = "a".repeat(GROUP_DESCRIPTION_MAX_LENGTH + 1);
1360 assert!(GroupDescription::new(&over_limit).is_err());
1361 }
1362
1363 #[test]
1364 fn test_string_enum_member_add_mode() {
1365 assert_eq!(MemberAddMode::AdminAdd.as_str(), "admin_add");
1366 assert_eq!(MemberAddMode::AllMemberAdd.as_str(), "all_member_add");
1367 assert_eq!(
1368 MemberAddMode::try_from("admin_add").unwrap(),
1369 MemberAddMode::AdminAdd
1370 );
1371 assert!(MemberAddMode::try_from("invalid").is_err());
1372 }
1373
1374 #[test]
1375 fn test_string_enum_member_link_mode() {
1376 assert_eq!(MemberLinkMode::AdminLink.as_str(), "admin_link");
1377 assert_eq!(MemberLinkMode::AllMemberLink.as_str(), "all_member_link");
1378 assert_eq!(
1379 MemberLinkMode::try_from("admin_link").unwrap(),
1380 MemberLinkMode::AdminLink
1381 );
1382 }
1383
1384 #[test]
1385 fn test_participant_type_is_admin() {
1386 assert!(!ParticipantType::Member.is_admin());
1387 assert!(ParticipantType::Admin.is_admin());
1388 assert!(ParticipantType::SuperAdmin.is_admin());
1389 }
1390
1391 #[test]
1392 fn test_normalize_participants_drops_phone_for_pn() {
1393 let pn_jid: Jid = "15551234567@s.whatsapp.net".parse().unwrap();
1394 let lid_jid: Jid = "100000000000001@lid".parse().unwrap();
1395 let phone_jid: Jid = "15550000001@s.whatsapp.net".parse().unwrap();
1396
1397 let participants = vec![
1398 GroupParticipantOptions::new(pn_jid.clone()).with_phone_number(phone_jid.clone()),
1399 GroupParticipantOptions::new(lid_jid.clone()).with_phone_number(phone_jid.clone()),
1400 ];
1401
1402 let normalized = normalize_participants(&participants);
1403 assert!(normalized[0].phone_number.is_none());
1404 assert_eq!(normalized[0].jid, pn_jid);
1405 assert_eq!(normalized[1].phone_number.as_ref(), Some(&phone_jid));
1406 }
1407
1408 #[test]
1409 fn test_build_create_group_node() {
1410 let pn_jid: Jid = "15551234567@s.whatsapp.net".parse().unwrap();
1411 let options = GroupCreateOptions::new("Test Subject")
1412 .with_participant(GroupParticipantOptions::from_phone(pn_jid))
1413 .with_member_link_mode(MemberLinkMode::AllMemberLink)
1414 .with_member_add_mode(MemberAddMode::AdminAdd);
1415
1416 let node = build_create_group_node(&options);
1417 assert_eq!(node.tag, "create");
1418 assert_eq!(
1419 node.attrs().optional_string("subject"),
1420 Some("Test Subject")
1421 );
1422
1423 let link_mode = node.get_children_by_tag("member_link_mode").next().unwrap();
1424 assert_eq!(
1425 link_mode.content.as_ref().and_then(|c| match c {
1426 NodeContent::String(s) => Some(s.as_str()),
1427 _ => None,
1428 }),
1429 Some("all_member_link")
1430 );
1431 }
1432
1433 #[test]
1434 fn test_typed_builder() {
1435 let options: GroupCreateOptions = GroupCreateOptions::builder()
1436 .subject("My Group")
1437 .member_add_mode(MemberAddMode::AdminAdd)
1438 .build();
1439
1440 assert_eq!(options.subject, "My Group");
1441 assert_eq!(options.member_add_mode, Some(MemberAddMode::AdminAdd));
1442 }
1443
1444 #[test]
1445 fn test_set_group_description_with_id_and_prev() {
1446 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
1447 let desc = GroupDescription::new("New description").unwrap();
1448 let spec = SetGroupDescriptionIq::new(&jid, Some(desc), Some("AABBCCDD".to_string()));
1449 let iq = spec.build_iq();
1450
1451 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1452 let desc_node = &nodes[0];
1453 assert_eq!(desc_node.tag, "description");
1454 let id = desc_node.attrs().optional_string("id").unwrap();
1456 assert_eq!(id.len(), 8);
1457 assert_eq!(desc_node.attrs().optional_string("prev"), Some("AABBCCDD"));
1458 assert!(desc_node.get_children_by_tag("body").next().is_some());
1460 } else {
1461 panic!("expected nodes content");
1462 }
1463 }
1464
1465 #[test]
1466 fn test_set_group_description_delete() {
1467 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
1468 let spec = SetGroupDescriptionIq::new(&jid, None, Some("PREV1234".to_string()));
1469 let iq = spec.build_iq();
1470
1471 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1472 let desc_node = &nodes[0];
1473 assert_eq!(desc_node.tag, "description");
1474 assert_eq!(desc_node.attrs().optional_string("delete"), Some("true"));
1475 assert_eq!(desc_node.attrs().optional_string("prev"), Some("PREV1234"));
1476 assert!(desc_node.attrs().optional_string("id").is_some());
1478 } else {
1479 panic!("expected nodes content");
1480 }
1481 }
1482
1483 #[test]
1484 fn test_leave_group_iq() {
1485 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
1486 let spec = LeaveGroupIq::new(&jid);
1487 let iq = spec.build_iq();
1488
1489 assert_eq!(iq.namespace, GROUP_IQ_NAMESPACE);
1490 assert_eq!(iq.query_type, InfoQueryType::Set);
1491 assert_eq!(iq.to.server, GROUP_SERVER);
1493 }
1494
1495 #[test]
1496 fn test_add_participants_iq() {
1497 let group: Jid = "120363000000000001@g.us".parse().unwrap();
1498 let p1: Jid = "1234567890@s.whatsapp.net".parse().unwrap();
1499 let p2: Jid = "9876543210@s.whatsapp.net".parse().unwrap();
1500 let spec = AddParticipantsIq::new(&group, &[p1, p2]);
1501 let iq = spec.build_iq();
1502
1503 assert_eq!(iq.namespace, GROUP_IQ_NAMESPACE);
1504 assert_eq!(iq.to, group);
1505
1506 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1507 let add_node = &nodes[0];
1508 assert_eq!(add_node.tag, "add");
1509 let participants: Vec<_> = add_node.get_children_by_tag("participant").collect();
1510 assert_eq!(participants.len(), 2);
1511 } else {
1512 panic!("expected nodes content");
1513 }
1514 }
1515
1516 #[test]
1517 fn test_promote_demote_iq() {
1518 let group: Jid = "120363000000000001@g.us".parse().unwrap();
1519 let p1: Jid = "1234567890@s.whatsapp.net".parse().unwrap();
1520
1521 let promote = PromoteParticipantsIq::new(&group, std::slice::from_ref(&p1));
1522 let iq = promote.build_iq();
1523 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1524 assert_eq!(nodes[0].tag, "promote");
1525 } else {
1526 panic!("expected nodes content");
1527 }
1528
1529 let demote = DemoteParticipantsIq::new(&group, &[p1]);
1530 let iq = demote.build_iq();
1531 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1532 assert_eq!(nodes[0].tag, "demote");
1533 } else {
1534 panic!("expected nodes content");
1535 }
1536 }
1537
1538 #[test]
1539 fn test_get_group_invite_link_iq() {
1540 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
1541 let spec = GetGroupInviteLinkIq::new(&jid, false);
1542 let iq = spec.build_iq();
1543
1544 assert_eq!(iq.query_type, InfoQueryType::Get);
1545 assert_eq!(iq.to, jid);
1546
1547 let reset_spec = GetGroupInviteLinkIq::new(&jid, true);
1549 assert_eq!(reset_spec.build_iq().query_type, InfoQueryType::Set);
1550 }
1551
1552 #[test]
1553 fn test_get_group_invite_link_parse_response() {
1554 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
1555 let spec = GetGroupInviteLinkIq::new(&jid, false);
1556
1557 let response = NodeBuilder::new("response")
1558 .children([NodeBuilder::new("invite")
1559 .attr("code", "AbCdEfGhIjKl")
1560 .build()])
1561 .build();
1562
1563 let result = spec.parse_response(&response).unwrap();
1564 assert_eq!(result, "https://chat.whatsapp.com/AbCdEfGhIjKl");
1565 }
1566
1567 #[test]
1568 fn test_participant_change_response_parse() {
1569 let node = NodeBuilder::new("participant")
1570 .attr("jid", "1234567890@s.whatsapp.net")
1571 .attr("type", "200")
1572 .build();
1573
1574 let result = ParticipantChangeResponse::try_from_node(&node).unwrap();
1575 assert_eq!(result.jid.user, "1234567890");
1576 assert_eq!(result.status, Some("200".to_string()));
1577 }
1578
1579 #[test]
1580 fn test_set_group_locked_iq() {
1581 let group: Jid = "120363000000000001@g.us".parse().unwrap();
1582
1583 let lock = SetGroupLockedIq::lock(&group);
1584 let iq = lock.build_iq();
1585 assert_eq!(iq.query_type, InfoQueryType::Set);
1586 assert_eq!(iq.to, group);
1587 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1588 assert_eq!(nodes[0].tag, "locked");
1589 } else {
1590 panic!("expected nodes content");
1591 }
1592
1593 let unlock = SetGroupLockedIq::unlock(&group);
1594 let iq = unlock.build_iq();
1595 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1596 assert_eq!(nodes[0].tag, "unlocked");
1597 } else {
1598 panic!("expected nodes content");
1599 }
1600 }
1601
1602 #[test]
1603 fn test_set_group_announcement_iq() {
1604 let group: Jid = "120363000000000001@g.us".parse().unwrap();
1605
1606 let announce = SetGroupAnnouncementIq::announce(&group);
1607 let iq = announce.build_iq();
1608 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1609 assert_eq!(nodes[0].tag, "announcement");
1610 } else {
1611 panic!("expected nodes content");
1612 }
1613
1614 let not_announce = SetGroupAnnouncementIq::unannounce(&group);
1615 let iq = not_announce.build_iq();
1616 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1617 assert_eq!(nodes[0].tag, "not_announcement");
1618 } else {
1619 panic!("expected nodes content");
1620 }
1621 }
1622
1623 #[test]
1624 fn test_set_group_ephemeral_iq() {
1625 let group: Jid = "120363000000000001@g.us".parse().unwrap();
1626
1627 let enable = SetGroupEphemeralIq::enable(&group, NonZeroU32::new(86400).unwrap());
1628 let iq = enable.build_iq();
1629 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1630 assert_eq!(nodes[0].tag, "ephemeral");
1631 assert_eq!(
1632 nodes[0].attrs().optional_string("expiration"),
1633 Some("86400")
1634 );
1635 } else {
1636 panic!("expected nodes content");
1637 }
1638
1639 let disable = SetGroupEphemeralIq::disable(&group);
1640 let iq = disable.build_iq();
1641 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1642 assert_eq!(nodes[0].tag, "not_ephemeral");
1643 } else {
1644 panic!("expected nodes content");
1645 }
1646 }
1647
1648 #[test]
1649 fn test_set_group_membership_approval_iq() {
1650 let group: Jid = "120363000000000001@g.us".parse().unwrap();
1651
1652 let spec = SetGroupMembershipApprovalIq::new(&group, MembershipApprovalMode::On);
1653 let iq = spec.build_iq();
1654 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1655 assert_eq!(nodes[0].tag, "membership_approval_mode");
1656 let join = nodes[0].get_children_by_tag("group_join").next().unwrap();
1657 assert_eq!(join.attrs().optional_string("state"), Some("on"));
1658 } else {
1659 panic!("expected nodes content");
1660 }
1661 }
1662}