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 typed_builder::TypedBuilder;
8use wa_rs_binary::builder::NodeBuilder;
9use wa_rs_binary::jid::{GROUP_SERVER, Jid};
10use wa_rs_binary::node::{Node, NodeContent};
11
12pub use crate::types::message::AddressingMode;
14pub const GROUP_IQ_NAMESPACE: &str = "w:g2";
16
17pub const GROUP_SUBJECT_MAX_LENGTH: usize = 100;
19
20pub const GROUP_DESCRIPTION_MAX_LENGTH: usize = 2048;
22
23pub const GROUP_SIZE_LIMIT: usize = 257;
25#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
27pub enum MemberLinkMode {
28 #[str = "admin_link"]
29 AdminLink,
30 #[str = "all_member_link"]
31 AllMemberLink,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
36pub enum MemberAddMode {
37 #[str = "admin_add"]
38 AdminAdd,
39 #[str = "all_member_add"]
40 AllMemberAdd,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
45pub enum MembershipApprovalMode {
46 #[string_default]
47 #[str = "off"]
48 Off,
49 #[str = "on"]
50 On,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
55pub enum GroupQueryRequestType {
56 #[string_default]
57 #[str = "interactive"]
58 Interactive,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
63pub enum ParticipantType {
64 #[string_default]
65 #[str = "member"]
66 Member,
67 #[str = "admin"]
68 Admin,
69 #[str = "superadmin"]
70 SuperAdmin,
71}
72
73impl ParticipantType {
74 pub fn is_admin(&self) -> bool {
75 matches!(self, ParticipantType::Admin | ParticipantType::SuperAdmin)
76 }
77}
78
79impl TryFrom<Option<&str>> for ParticipantType {
80 type Error = anyhow::Error;
81
82 fn try_from(value: Option<&str>) -> Result<Self> {
83 match value {
84 Some("admin") => Ok(ParticipantType::Admin),
85 Some("superadmin") => Ok(ParticipantType::SuperAdmin),
86 Some("member") | None => Ok(ParticipantType::Member),
87 Some(other) => Err(anyhow!("unknown participant type: {other}")),
88 }
89 }
90}
91crate::define_validated_string! {
92 pub struct GroupSubject(max_len = GROUP_SUBJECT_MAX_LENGTH, name = "Group subject")
96}
97
98crate::define_validated_string! {
99 pub struct GroupDescription(max_len = GROUP_DESCRIPTION_MAX_LENGTH, name = "Group description")
103}
104#[derive(Debug, Clone, TypedBuilder)]
106#[builder(build_method(into))]
107pub struct GroupParticipantOptions {
108 pub jid: Jid,
109 #[builder(default, setter(strip_option))]
110 pub phone_number: Option<Jid>,
111 #[builder(default, setter(strip_option))]
112 pub privacy: Option<Vec<u8>>,
113}
114
115impl GroupParticipantOptions {
116 pub fn new(jid: Jid) -> Self {
117 Self {
118 jid,
119 phone_number: None,
120 privacy: None,
121 }
122 }
123
124 pub fn from_phone(phone_number: Jid) -> Self {
125 Self::new(phone_number)
126 }
127
128 pub fn from_lid_and_phone(lid: Jid, phone_number: Jid) -> Self {
129 Self::new(lid).with_phone_number(phone_number)
130 }
131
132 pub fn with_phone_number(mut self, phone_number: Jid) -> Self {
133 self.phone_number = Some(phone_number);
134 self
135 }
136
137 pub fn with_privacy(mut self, privacy: Vec<u8>) -> Self {
138 self.privacy = Some(privacy);
139 self
140 }
141}
142
143#[derive(Debug, Clone, TypedBuilder)]
145#[builder(build_method(into))]
146pub struct GroupCreateOptions {
147 #[builder(setter(into))]
148 pub subject: String,
149 #[builder(default)]
150 pub participants: Vec<GroupParticipantOptions>,
151 #[builder(default = Some(MemberLinkMode::AdminLink), setter(strip_option))]
152 pub member_link_mode: Option<MemberLinkMode>,
153 #[builder(default = Some(MemberAddMode::AllMemberAdd), setter(strip_option))]
154 pub member_add_mode: Option<MemberAddMode>,
155 #[builder(default = Some(MembershipApprovalMode::Off), setter(strip_option))]
156 pub membership_approval_mode: Option<MembershipApprovalMode>,
157 #[builder(default = Some(0), setter(strip_option))]
158 pub ephemeral_expiration: Option<u32>,
159}
160
161impl GroupCreateOptions {
162 pub fn new(subject: impl Into<String>) -> Self {
164 Self {
165 subject: subject.into(),
166 ..Default::default()
167 }
168 }
169
170 pub fn with_participant(mut self, participant: GroupParticipantOptions) -> Self {
171 self.participants.push(participant);
172 self
173 }
174
175 pub fn with_participants(mut self, participants: Vec<GroupParticipantOptions>) -> Self {
176 self.participants = participants;
177 self
178 }
179
180 pub fn with_member_link_mode(mut self, mode: MemberLinkMode) -> Self {
181 self.member_link_mode = Some(mode);
182 self
183 }
184
185 pub fn with_member_add_mode(mut self, mode: MemberAddMode) -> Self {
186 self.member_add_mode = Some(mode);
187 self
188 }
189
190 pub fn with_membership_approval_mode(mut self, mode: MembershipApprovalMode) -> Self {
191 self.membership_approval_mode = Some(mode);
192 self
193 }
194
195 pub fn with_ephemeral_expiration(mut self, expiration: u32) -> Self {
196 self.ephemeral_expiration = Some(expiration);
197 self
198 }
199}
200
201impl Default for GroupCreateOptions {
202 fn default() -> Self {
203 Self {
204 subject: String::new(),
205 participants: Vec::new(),
206 member_link_mode: Some(MemberLinkMode::AdminLink),
207 member_add_mode: Some(MemberAddMode::AllMemberAdd),
208 membership_approval_mode: Some(MembershipApprovalMode::Off),
209 ephemeral_expiration: Some(0),
210 }
211 }
212}
213
214pub fn normalize_participants(
216 participants: &[GroupParticipantOptions],
217) -> Vec<GroupParticipantOptions> {
218 participants
219 .iter()
220 .cloned()
221 .map(|p| {
222 if !p.jid.is_lid() && p.phone_number.is_some() {
223 GroupParticipantOptions {
224 phone_number: None,
225 ..p
226 }
227 } else {
228 p
229 }
230 })
231 .collect()
232}
233
234pub fn build_create_group_node(options: &GroupCreateOptions) -> Node {
236 let mut children = Vec::new();
237
238 if let Some(link_mode) = &options.member_link_mode {
239 children.push(
240 NodeBuilder::new("member_link_mode")
241 .string_content(link_mode.as_str())
242 .build(),
243 );
244 }
245
246 if let Some(add_mode) = &options.member_add_mode {
247 children.push(
248 NodeBuilder::new("member_add_mode")
249 .string_content(add_mode.as_str())
250 .build(),
251 );
252 }
253
254 let participants = normalize_participants(&options.participants);
256
257 for participant in &participants {
258 let mut attrs = vec![("jid", participant.jid.to_string())];
259 if let Some(pn) = &participant.phone_number {
260 attrs.push(("phone_number", pn.to_string()));
261 }
262
263 let participant_node = if let Some(privacy_bytes) = &participant.privacy {
264 NodeBuilder::new("participant")
265 .attrs(attrs)
266 .children([NodeBuilder::new("privacy")
267 .string_content(hex::encode(privacy_bytes))
268 .build()])
269 .build()
270 } else {
271 NodeBuilder::new("participant").attrs(attrs).build()
272 };
273 children.push(participant_node);
274 }
275
276 if let Some(expiration) = &options.ephemeral_expiration {
277 children.push(
278 NodeBuilder::new("ephemeral")
279 .attr("expiration", expiration.to_string())
280 .build(),
281 );
282 }
283
284 if let Some(approval_mode) = &options.membership_approval_mode {
285 children.push(
286 NodeBuilder::new("membership_approval_mode")
287 .children([NodeBuilder::new("group_join")
288 .attr("state", approval_mode.as_str())
289 .build()])
290 .build(),
291 );
292 }
293
294 NodeBuilder::new("create")
295 .attr("subject", &options.subject)
296 .children(children)
297 .build()
298}
299#[derive(Debug, Clone, Default)]
301pub struct GroupQueryRequest {
302 pub request: GroupQueryRequestType,
303}
304
305impl ProtocolNode for GroupQueryRequest {
306 fn tag(&self) -> &'static str {
307 "query"
308 }
309
310 fn into_node(self) -> Node {
311 NodeBuilder::new("query")
312 .attr("request", self.request.as_str())
313 .build()
314 }
315
316 fn try_from_node(node: &Node) -> Result<Self> {
317 if node.tag != "query" {
318 return Err(anyhow!("expected <query>, got <{}>", node.tag));
319 }
320 Ok(Self::default())
321 }
322}
323
324#[derive(Debug, Clone)]
326pub struct GroupParticipantResponse {
327 pub jid: Jid,
328 pub phone_number: Option<Jid>,
329 pub participant_type: ParticipantType,
330}
331
332impl ProtocolNode for GroupParticipantResponse {
333 fn tag(&self) -> &'static str {
334 "participant"
335 }
336
337 fn into_node(self) -> Node {
338 let mut builder = NodeBuilder::new("participant").attr("jid", self.jid.to_string());
339 if let Some(pn) = &self.phone_number {
340 builder = builder.attr("phone_number", pn.to_string());
341 }
342 if self.participant_type != ParticipantType::Member {
343 builder = builder.attr("type", self.participant_type.as_str());
344 }
345 builder.build()
346 }
347
348 fn try_from_node(node: &Node) -> Result<Self> {
349 if node.tag != "participant" {
350 return Err(anyhow!("expected <participant>, got <{}>", node.tag));
351 }
352 let jid = node
353 .attrs()
354 .optional_jid("jid")
355 .ok_or_else(|| anyhow!("participant missing required 'jid' attribute"))?;
356 let phone_number = node.attrs().optional_jid("phone_number");
357 let participant_type = ParticipantType::try_from(node.attrs().optional_string("type"))
359 .unwrap_or(ParticipantType::Member);
360
361 Ok(Self {
362 jid,
363 phone_number,
364 participant_type,
365 })
366 }
367}
368
369#[derive(Debug, Clone)]
371pub struct GroupInfoResponse {
372 pub id: Jid,
373 pub subject: GroupSubject,
374 pub addressing_mode: AddressingMode,
375 pub participants: Vec<GroupParticipantResponse>,
376}
377
378impl ProtocolNode for GroupInfoResponse {
379 fn tag(&self) -> &'static str {
380 "group"
381 }
382
383 fn into_node(self) -> Node {
384 let children: Vec<Node> = self
385 .participants
386 .into_iter()
387 .map(|p| p.into_node())
388 .collect();
389 NodeBuilder::new("group")
390 .attr("id", self.id.to_string())
391 .attr("subject", self.subject.as_str())
392 .attr("addressing_mode", self.addressing_mode.as_str())
393 .children(children)
394 .build()
395 }
396
397 fn try_from_node(node: &Node) -> Result<Self> {
398 if node.tag != "group" {
399 return Err(anyhow!("expected <group>, got <{}>", node.tag));
400 }
401
402 let id_str = required_attr(node, "id")?;
403 let id = if id_str.contains('@') {
404 id_str.parse()?
405 } else {
406 Jid::group(id_str)
407 };
408
409 let subject =
410 GroupSubject::new_unchecked(optional_attr(node, "subject").unwrap_or_default());
411
412 let addressing_mode =
413 AddressingMode::try_from(optional_attr(node, "addressing_mode").unwrap_or("pn"))?;
414
415 let participants = collect_children::<GroupParticipantResponse>(node, "participant")?;
416
417 Ok(Self {
418 id,
419 subject,
420 addressing_mode,
421 participants,
422 })
423 }
424}
425#[derive(Debug, Clone)]
427pub struct GroupParticipatingRequest {
428 pub include_participants: bool,
429 pub include_description: bool,
430}
431
432impl GroupParticipatingRequest {
433 pub fn new() -> Self {
434 Self {
435 include_participants: true,
436 include_description: true,
437 }
438 }
439}
440
441impl Default for GroupParticipatingRequest {
442 fn default() -> Self {
443 Self::new()
444 }
445}
446
447impl ProtocolNode for GroupParticipatingRequest {
448 fn tag(&self) -> &'static str {
449 "participating"
450 }
451
452 fn into_node(self) -> Node {
453 let mut children = Vec::new();
454 if self.include_participants {
455 children.push(NodeBuilder::new("participants").build());
456 }
457 if self.include_description {
458 children.push(NodeBuilder::new("description").build());
459 }
460 NodeBuilder::new("participating").children(children).build()
461 }
462
463 fn try_from_node(node: &Node) -> Result<Self> {
464 if node.tag != "participating" {
465 return Err(anyhow!("expected <participating>, got <{}>", node.tag));
466 }
467 Ok(Self::default())
468 }
469}
470
471#[derive(Debug, Clone, Default)]
473pub struct GroupParticipatingResponse {
474 pub groups: Vec<GroupInfoResponse>,
475}
476
477impl ProtocolNode for GroupParticipatingResponse {
478 fn tag(&self) -> &'static str {
479 "groups"
480 }
481
482 fn into_node(self) -> Node {
483 let children: Vec<Node> = self.groups.into_iter().map(|g| g.into_node()).collect();
484 NodeBuilder::new("groups").children(children).build()
485 }
486
487 fn try_from_node(node: &Node) -> Result<Self> {
488 if node.tag != "groups" {
489 return Err(anyhow!("expected <groups>, got <{}>", node.tag));
490 }
491
492 let groups = collect_children::<GroupInfoResponse>(node, "group")?;
493
494 Ok(Self { groups })
495 }
496}
497#[derive(Debug, Clone)]
499pub struct GroupQueryIq {
500 pub group_jid: Jid,
501}
502
503impl GroupQueryIq {
504 pub fn new(group_jid: &Jid) -> Self {
505 Self {
506 group_jid: group_jid.clone(),
507 }
508 }
509}
510
511impl IqSpec for GroupQueryIq {
512 type Response = GroupInfoResponse;
513
514 fn build_iq(&self) -> InfoQuery<'static> {
515 InfoQuery::get_ref(
516 GROUP_IQ_NAMESPACE,
517 &self.group_jid,
518 Some(NodeContent::Nodes(vec![
519 GroupQueryRequest::default().into_node(),
520 ])),
521 )
522 }
523
524 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
525 let group_node = required_child(response, "group")?;
526 GroupInfoResponse::try_from_node(group_node)
527 }
528}
529
530#[derive(Debug, Clone, Default)]
532pub struct GroupParticipatingIq;
533
534impl GroupParticipatingIq {
535 pub fn new() -> Self {
536 Self
537 }
538}
539
540impl IqSpec for GroupParticipatingIq {
541 type Response = GroupParticipatingResponse;
542
543 fn build_iq(&self) -> InfoQuery<'static> {
544 InfoQuery::get(
545 GROUP_IQ_NAMESPACE,
546 Jid::new("", GROUP_SERVER),
547 Some(NodeContent::Nodes(vec![
548 GroupParticipatingRequest::new().into_node(),
549 ])),
550 )
551 }
552
553 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
554 let groups_node = required_child(response, "groups")?;
555 GroupParticipatingResponse::try_from_node(groups_node)
556 }
557}
558
559#[derive(Debug, Clone)]
561pub struct GroupCreateIq {
562 pub options: GroupCreateOptions,
563}
564
565impl GroupCreateIq {
566 pub fn new(options: GroupCreateOptions) -> Self {
567 Self { options }
568 }
569}
570
571impl IqSpec for GroupCreateIq {
572 type Response = Jid;
573
574 fn build_iq(&self) -> InfoQuery<'static> {
575 InfoQuery::set(
576 GROUP_IQ_NAMESPACE,
577 Jid::new("", GROUP_SERVER),
578 Some(NodeContent::Nodes(vec![build_create_group_node(
579 &self.options,
580 )])),
581 )
582 }
583
584 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
585 let group_node = required_child(response, "group")?;
586 let group_id_str = required_attr(group_node, "id")?;
587
588 if group_id_str.contains('@') {
589 group_id_str.parse().map_err(Into::into)
590 } else {
591 Ok(Jid::group(group_id_str))
592 }
593 }
594}
595
596#[derive(Debug, Clone)]
602pub struct ParticipantChangeResponse {
603 pub jid: Jid,
604 pub status: Option<String>,
606 pub error: Option<String>,
607}
608
609impl ProtocolNode for ParticipantChangeResponse {
610 fn tag(&self) -> &'static str {
611 "participant"
612 }
613
614 fn into_node(self) -> Node {
615 let mut builder = NodeBuilder::new("participant").attr("jid", self.jid.to_string());
616 if let Some(ref status) = self.status {
617 builder = builder.attr("type", status);
618 }
619 if let Some(ref error) = self.error {
620 builder = builder.attr("error", error);
621 }
622 builder.build()
623 }
624
625 fn try_from_node(node: &Node) -> Result<Self> {
626 if node.tag != "participant" {
627 return Err(anyhow!("expected <participant>, got <{}>", node.tag));
628 }
629 let jid = node
630 .attrs()
631 .optional_jid("jid")
632 .ok_or_else(|| anyhow!("participant missing required 'jid' attribute"))?;
633 let status = optional_attr(node, "type").map(String::from);
634 let error = optional_attr(node, "error").map(String::from);
635 Ok(Self { jid, status, error })
636 }
637}
638
639#[derive(Debug, Clone)]
648pub struct SetGroupSubjectIq {
649 pub group_jid: Jid,
650 pub subject: GroupSubject,
651}
652
653impl SetGroupSubjectIq {
654 pub fn new(group_jid: &Jid, subject: GroupSubject) -> Self {
655 Self {
656 group_jid: group_jid.clone(),
657 subject,
658 }
659 }
660}
661
662impl IqSpec for SetGroupSubjectIq {
663 type Response = ();
664
665 fn build_iq(&self) -> InfoQuery<'static> {
666 InfoQuery::set_ref(
667 GROUP_IQ_NAMESPACE,
668 &self.group_jid,
669 Some(NodeContent::Nodes(vec![
670 NodeBuilder::new("subject")
671 .string_content(self.subject.as_str())
672 .build(),
673 ])),
674 )
675 }
676
677 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
678 Ok(())
679 }
680}
681
682#[derive(Debug, Clone)]
695pub struct SetGroupDescriptionIq {
696 pub group_jid: Jid,
697 pub description: Option<GroupDescription>,
698 pub id: String,
700 pub prev: Option<String>,
702}
703
704impl SetGroupDescriptionIq {
705 pub fn new(
706 group_jid: &Jid,
707 description: Option<GroupDescription>,
708 prev: Option<String>,
709 ) -> Self {
710 use rand::Rng;
711 let id = format!("{:08X}", rand::rng().random::<u32>());
712 Self {
713 group_jid: group_jid.clone(),
714 description,
715 id,
716 prev,
717 }
718 }
719}
720
721impl IqSpec for SetGroupDescriptionIq {
722 type Response = ();
723
724 fn build_iq(&self) -> InfoQuery<'static> {
725 let desc_node = if let Some(ref desc) = self.description {
726 let mut builder = NodeBuilder::new("description").attr("id", &self.id);
727 if let Some(ref prev) = self.prev {
728 builder = builder.attr("prev", prev);
729 }
730 builder
731 .children([NodeBuilder::new("body")
732 .string_content(desc.as_str())
733 .build()])
734 .build()
735 } else {
736 let mut builder = NodeBuilder::new("description")
737 .attr("id", &self.id)
738 .attr("delete", "true");
739 if let Some(ref prev) = self.prev {
740 builder = builder.attr("prev", prev);
741 }
742 builder.build()
743 };
744
745 InfoQuery::set_ref(
746 GROUP_IQ_NAMESPACE,
747 &self.group_jid,
748 Some(NodeContent::Nodes(vec![desc_node])),
749 )
750 }
751
752 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
753 Ok(())
754 }
755}
756
757#[derive(Debug, Clone)]
766pub struct LeaveGroupIq {
767 pub group_jid: Jid,
768}
769
770impl LeaveGroupIq {
771 pub fn new(group_jid: &Jid) -> Self {
772 Self {
773 group_jid: group_jid.clone(),
774 }
775 }
776}
777
778impl IqSpec for LeaveGroupIq {
779 type Response = ();
780
781 fn build_iq(&self) -> InfoQuery<'static> {
782 let group_node = NodeBuilder::new("group")
783 .attr("id", self.group_jid.to_string())
784 .build();
785 let leave_node = NodeBuilder::new("leave").children([group_node]).build();
786
787 InfoQuery::set(
788 GROUP_IQ_NAMESPACE,
789 Jid::new("", GROUP_SERVER),
790 Some(NodeContent::Nodes(vec![leave_node])),
791 )
792 }
793
794 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
795 Ok(())
796 }
797}
798
799#[derive(Debug, Clone)]
810pub struct AddParticipantsIq {
811 pub group_jid: Jid,
812 pub participants: Vec<Jid>,
813}
814
815impl AddParticipantsIq {
816 pub fn new(group_jid: &Jid, participants: &[Jid]) -> Self {
817 Self {
818 group_jid: group_jid.clone(),
819 participants: participants.to_vec(),
820 }
821 }
822}
823
824impl IqSpec for AddParticipantsIq {
825 type Response = Vec<ParticipantChangeResponse>;
826
827 fn build_iq(&self) -> InfoQuery<'static> {
828 let children: Vec<Node> = self
829 .participants
830 .iter()
831 .map(|jid| {
832 NodeBuilder::new("participant")
833 .attr("jid", jid.to_string())
834 .build()
835 })
836 .collect();
837
838 let add_node = NodeBuilder::new("add").children(children).build();
839
840 InfoQuery::set_ref(
841 GROUP_IQ_NAMESPACE,
842 &self.group_jid,
843 Some(NodeContent::Nodes(vec![add_node])),
844 )
845 }
846
847 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
848 let add_node = required_child(response, "add")?;
849 collect_children::<ParticipantChangeResponse>(add_node, "participant")
850 }
851}
852
853#[derive(Debug, Clone)]
864pub struct RemoveParticipantsIq {
865 pub group_jid: Jid,
866 pub participants: Vec<Jid>,
867}
868
869impl RemoveParticipantsIq {
870 pub fn new(group_jid: &Jid, participants: &[Jid]) -> Self {
871 Self {
872 group_jid: group_jid.clone(),
873 participants: participants.to_vec(),
874 }
875 }
876}
877
878impl IqSpec for RemoveParticipantsIq {
879 type Response = Vec<ParticipantChangeResponse>;
880
881 fn build_iq(&self) -> InfoQuery<'static> {
882 let children: Vec<Node> = self
883 .participants
884 .iter()
885 .map(|jid| {
886 NodeBuilder::new("participant")
887 .attr("jid", jid.to_string())
888 .build()
889 })
890 .collect();
891
892 let remove_node = NodeBuilder::new("remove").children(children).build();
893
894 InfoQuery::set_ref(
895 GROUP_IQ_NAMESPACE,
896 &self.group_jid,
897 Some(NodeContent::Nodes(vec![remove_node])),
898 )
899 }
900
901 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
902 let remove_node = required_child(response, "remove")?;
903 collect_children::<ParticipantChangeResponse>(remove_node, "participant")
904 }
905}
906
907#[derive(Debug, Clone)]
918pub struct PromoteParticipantsIq {
919 pub group_jid: Jid,
920 pub participants: Vec<Jid>,
921}
922
923impl PromoteParticipantsIq {
924 pub fn new(group_jid: &Jid, participants: &[Jid]) -> Self {
925 Self {
926 group_jid: group_jid.clone(),
927 participants: participants.to_vec(),
928 }
929 }
930}
931
932impl IqSpec for PromoteParticipantsIq {
933 type Response = ();
934
935 fn build_iq(&self) -> InfoQuery<'static> {
936 let children: Vec<Node> = self
937 .participants
938 .iter()
939 .map(|jid| {
940 NodeBuilder::new("participant")
941 .attr("jid", jid.to_string())
942 .build()
943 })
944 .collect();
945
946 let promote_node = NodeBuilder::new("promote").children(children).build();
947
948 InfoQuery::set_ref(
949 GROUP_IQ_NAMESPACE,
950 &self.group_jid,
951 Some(NodeContent::Nodes(vec![promote_node])),
952 )
953 }
954
955 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
956 Ok(())
957 }
958}
959
960#[derive(Debug, Clone)]
971pub struct DemoteParticipantsIq {
972 pub group_jid: Jid,
973 pub participants: Vec<Jid>,
974}
975
976impl DemoteParticipantsIq {
977 pub fn new(group_jid: &Jid, participants: &[Jid]) -> Self {
978 Self {
979 group_jid: group_jid.clone(),
980 participants: participants.to_vec(),
981 }
982 }
983}
984
985impl IqSpec for DemoteParticipantsIq {
986 type Response = ();
987
988 fn build_iq(&self) -> InfoQuery<'static> {
989 let children: Vec<Node> = self
990 .participants
991 .iter()
992 .map(|jid| {
993 NodeBuilder::new("participant")
994 .attr("jid", jid.to_string())
995 .build()
996 })
997 .collect();
998
999 let demote_node = NodeBuilder::new("demote").children(children).build();
1000
1001 InfoQuery::set_ref(
1002 GROUP_IQ_NAMESPACE,
1003 &self.group_jid,
1004 Some(NodeContent::Nodes(vec![demote_node])),
1005 )
1006 }
1007
1008 fn parse_response(&self, _response: &Node) -> Result<Self::Response> {
1009 Ok(())
1010 }
1011}
1012
1013#[derive(Debug, Clone)]
1020pub struct GetGroupInviteLinkIq {
1021 pub group_jid: Jid,
1022 pub reset: bool,
1023}
1024
1025impl GetGroupInviteLinkIq {
1026 pub fn new(group_jid: &Jid, reset: bool) -> Self {
1027 Self {
1028 group_jid: group_jid.clone(),
1029 reset,
1030 }
1031 }
1032}
1033
1034impl IqSpec for GetGroupInviteLinkIq {
1035 type Response = String;
1036
1037 fn build_iq(&self) -> InfoQuery<'static> {
1038 let content = Some(NodeContent::Nodes(vec![NodeBuilder::new("invite").build()]));
1039 if self.reset {
1040 InfoQuery::set_ref(GROUP_IQ_NAMESPACE, &self.group_jid, content)
1041 } else {
1042 InfoQuery::get_ref(GROUP_IQ_NAMESPACE, &self.group_jid, content)
1043 }
1044 }
1045
1046 fn parse_response(&self, response: &Node) -> Result<Self::Response> {
1047 let invite_node = required_child(response, "invite")?;
1048 let code = required_attr(invite_node, "code")?;
1049 Ok(format!("https://chat.whatsapp.com/{code}"))
1050 }
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055 use super::*;
1056 use crate::request::InfoQueryType;
1057
1058 #[test]
1059 fn test_group_subject_validation() {
1060 let subject = GroupSubject::new("Test Group").unwrap();
1061 assert_eq!(subject.as_str(), "Test Group");
1062
1063 let at_limit = "a".repeat(GROUP_SUBJECT_MAX_LENGTH);
1064 assert!(GroupSubject::new(&at_limit).is_ok());
1065
1066 let over_limit = "a".repeat(GROUP_SUBJECT_MAX_LENGTH + 1);
1067 assert!(GroupSubject::new(&over_limit).is_err());
1068 }
1069
1070 #[test]
1071 fn test_group_description_validation() {
1072 let desc = GroupDescription::new("Test Description").unwrap();
1073 assert_eq!(desc.as_str(), "Test Description");
1074
1075 let at_limit = "a".repeat(GROUP_DESCRIPTION_MAX_LENGTH);
1076 assert!(GroupDescription::new(&at_limit).is_ok());
1077
1078 let over_limit = "a".repeat(GROUP_DESCRIPTION_MAX_LENGTH + 1);
1079 assert!(GroupDescription::new(&over_limit).is_err());
1080 }
1081
1082 #[test]
1083 fn test_string_enum_member_add_mode() {
1084 assert_eq!(MemberAddMode::AdminAdd.as_str(), "admin_add");
1085 assert_eq!(MemberAddMode::AllMemberAdd.as_str(), "all_member_add");
1086 assert_eq!(
1087 MemberAddMode::try_from("admin_add").unwrap(),
1088 MemberAddMode::AdminAdd
1089 );
1090 assert!(MemberAddMode::try_from("invalid").is_err());
1091 }
1092
1093 #[test]
1094 fn test_string_enum_member_link_mode() {
1095 assert_eq!(MemberLinkMode::AdminLink.as_str(), "admin_link");
1096 assert_eq!(MemberLinkMode::AllMemberLink.as_str(), "all_member_link");
1097 assert_eq!(
1098 MemberLinkMode::try_from("admin_link").unwrap(),
1099 MemberLinkMode::AdminLink
1100 );
1101 }
1102
1103 #[test]
1104 fn test_participant_type_is_admin() {
1105 assert!(!ParticipantType::Member.is_admin());
1106 assert!(ParticipantType::Admin.is_admin());
1107 assert!(ParticipantType::SuperAdmin.is_admin());
1108 }
1109
1110 #[test]
1111 fn test_normalize_participants_drops_phone_for_pn() {
1112 let pn_jid: Jid = "15551234567@s.whatsapp.net".parse().unwrap();
1113 let lid_jid: Jid = "100000000000001@lid".parse().unwrap();
1114 let phone_jid: Jid = "15550000001@s.whatsapp.net".parse().unwrap();
1115
1116 let participants = vec![
1117 GroupParticipantOptions::new(pn_jid.clone()).with_phone_number(phone_jid.clone()),
1118 GroupParticipantOptions::new(lid_jid.clone()).with_phone_number(phone_jid.clone()),
1119 ];
1120
1121 let normalized = normalize_participants(&participants);
1122 assert!(normalized[0].phone_number.is_none());
1123 assert_eq!(normalized[0].jid, pn_jid);
1124 assert_eq!(normalized[1].phone_number.as_ref(), Some(&phone_jid));
1125 }
1126
1127 #[test]
1128 fn test_build_create_group_node() {
1129 let pn_jid: Jid = "15551234567@s.whatsapp.net".parse().unwrap();
1130 let options = GroupCreateOptions::new("Test Subject")
1131 .with_participant(GroupParticipantOptions::from_phone(pn_jid))
1132 .with_member_link_mode(MemberLinkMode::AllMemberLink)
1133 .with_member_add_mode(MemberAddMode::AdminAdd);
1134
1135 let node = build_create_group_node(&options);
1136 assert_eq!(node.tag, "create");
1137 assert_eq!(
1138 node.attrs().optional_string("subject"),
1139 Some("Test Subject")
1140 );
1141
1142 let link_mode = node.get_children_by_tag("member_link_mode").next().unwrap();
1143 assert_eq!(
1144 link_mode.content.as_ref().and_then(|c| match c {
1145 NodeContent::String(s) => Some(s.as_str()),
1146 _ => None,
1147 }),
1148 Some("all_member_link")
1149 );
1150 }
1151
1152 #[test]
1153 fn test_typed_builder() {
1154 let options: GroupCreateOptions = GroupCreateOptions::builder()
1155 .subject("My Group")
1156 .member_add_mode(MemberAddMode::AdminAdd)
1157 .build();
1158
1159 assert_eq!(options.subject, "My Group");
1160 assert_eq!(options.member_add_mode, Some(MemberAddMode::AdminAdd));
1161 }
1162
1163 #[test]
1164 fn test_set_group_description_with_id_and_prev() {
1165 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
1166 let desc = GroupDescription::new("New description").unwrap();
1167 let spec = SetGroupDescriptionIq::new(&jid, Some(desc), Some("AABBCCDD".to_string()));
1168 let iq = spec.build_iq();
1169
1170 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1171 let desc_node = &nodes[0];
1172 assert_eq!(desc_node.tag, "description");
1173 let id = desc_node.attrs().optional_string("id").unwrap();
1175 assert_eq!(id.len(), 8);
1176 assert_eq!(desc_node.attrs().optional_string("prev"), Some("AABBCCDD"));
1177 assert!(desc_node.get_children_by_tag("body").next().is_some());
1179 } else {
1180 panic!("expected nodes content");
1181 }
1182 }
1183
1184 #[test]
1185 fn test_set_group_description_delete() {
1186 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
1187 let spec = SetGroupDescriptionIq::new(&jid, None, Some("PREV1234".to_string()));
1188 let iq = spec.build_iq();
1189
1190 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1191 let desc_node = &nodes[0];
1192 assert_eq!(desc_node.tag, "description");
1193 assert_eq!(desc_node.attrs().optional_string("delete"), Some("true"));
1194 assert_eq!(desc_node.attrs().optional_string("prev"), Some("PREV1234"));
1195 assert!(desc_node.attrs().optional_string("id").is_some());
1197 } else {
1198 panic!("expected nodes content");
1199 }
1200 }
1201
1202 #[test]
1203 fn test_leave_group_iq() {
1204 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
1205 let spec = LeaveGroupIq::new(&jid);
1206 let iq = spec.build_iq();
1207
1208 assert_eq!(iq.namespace, GROUP_IQ_NAMESPACE);
1209 assert_eq!(iq.query_type, InfoQueryType::Set);
1210 assert_eq!(iq.to.server, GROUP_SERVER);
1212 }
1213
1214 #[test]
1215 fn test_add_participants_iq() {
1216 let group: Jid = "120363000000000001@g.us".parse().unwrap();
1217 let p1: Jid = "1234567890@s.whatsapp.net".parse().unwrap();
1218 let p2: Jid = "9876543210@s.whatsapp.net".parse().unwrap();
1219 let spec = AddParticipantsIq::new(&group, &[p1, p2]);
1220 let iq = spec.build_iq();
1221
1222 assert_eq!(iq.namespace, GROUP_IQ_NAMESPACE);
1223 assert_eq!(iq.to, group);
1224
1225 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1226 let add_node = &nodes[0];
1227 assert_eq!(add_node.tag, "add");
1228 let participants: Vec<_> = add_node.get_children_by_tag("participant").collect();
1229 assert_eq!(participants.len(), 2);
1230 } else {
1231 panic!("expected nodes content");
1232 }
1233 }
1234
1235 #[test]
1236 fn test_promote_demote_iq() {
1237 let group: Jid = "120363000000000001@g.us".parse().unwrap();
1238 let p1: Jid = "1234567890@s.whatsapp.net".parse().unwrap();
1239
1240 let promote = PromoteParticipantsIq::new(&group, std::slice::from_ref(&p1));
1241 let iq = promote.build_iq();
1242 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1243 assert_eq!(nodes[0].tag, "promote");
1244 } else {
1245 panic!("expected nodes content");
1246 }
1247
1248 let demote = DemoteParticipantsIq::new(&group, &[p1]);
1249 let iq = demote.build_iq();
1250 if let Some(NodeContent::Nodes(nodes)) = &iq.content {
1251 assert_eq!(nodes[0].tag, "demote");
1252 } else {
1253 panic!("expected nodes content");
1254 }
1255 }
1256
1257 #[test]
1258 fn test_get_group_invite_link_iq() {
1259 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
1260 let spec = GetGroupInviteLinkIq::new(&jid, false);
1261 let iq = spec.build_iq();
1262
1263 assert_eq!(iq.query_type, InfoQueryType::Get);
1264 assert_eq!(iq.to, jid);
1265
1266 let reset_spec = GetGroupInviteLinkIq::new(&jid, true);
1268 assert_eq!(reset_spec.build_iq().query_type, InfoQueryType::Set);
1269 }
1270
1271 #[test]
1272 fn test_get_group_invite_link_parse_response() {
1273 let jid: Jid = "120363000000000001@g.us".parse().unwrap();
1274 let spec = GetGroupInviteLinkIq::new(&jid, false);
1275
1276 let response = NodeBuilder::new("response")
1277 .children([NodeBuilder::new("invite")
1278 .attr("code", "AbCdEfGhIjKl")
1279 .build()])
1280 .build();
1281
1282 let result = spec.parse_response(&response).unwrap();
1283 assert_eq!(result, "https://chat.whatsapp.com/AbCdEfGhIjKl");
1284 }
1285
1286 #[test]
1287 fn test_participant_change_response_parse() {
1288 let node = NodeBuilder::new("participant")
1289 .attr("jid", "1234567890@s.whatsapp.net")
1290 .attr("type", "200")
1291 .build();
1292
1293 let result = ParticipantChangeResponse::try_from_node(&node).unwrap();
1294 assert_eq!(result.jid.user, "1234567890");
1295 assert_eq!(result.status, Some("200".to_string()));
1296 }
1297}