Skip to main content

whatsapp_rust/features/
groups.rs

1use crate::client::Client;
2use std::collections::HashMap;
3use wacore::client::context::GroupInfo;
4use wacore::iq::groups::{
5    AddParticipantsIq, DemoteParticipantsIq, GetGroupInviteLinkIq, GroupCreateIq,
6    GroupParticipantResponse, GroupParticipatingIq, GroupQueryIq, LeaveGroupIq,
7    PromoteParticipantsIq, RemoveParticipantsIq, SetGroupDescriptionIq, SetGroupSubjectIq,
8    normalize_participants,
9};
10use wacore::types::message::AddressingMode;
11use wacore_binary::jid::Jid;
12
13pub use wacore::iq::groups::{
14    GroupCreateOptions, GroupDescription, GroupParticipantOptions, GroupSubject, MemberAddMode,
15    MemberLinkMode, MembershipApprovalMode, ParticipantChangeResponse,
16};
17
18#[derive(Debug, Clone)]
19pub struct GroupMetadata {
20    pub id: Jid,
21    pub subject: String,
22    pub participants: Vec<GroupParticipant>,
23    pub addressing_mode: AddressingMode,
24}
25
26#[derive(Debug, Clone)]
27pub struct GroupParticipant {
28    pub jid: Jid,
29    pub phone_number: Option<Jid>,
30    pub is_admin: bool,
31}
32
33impl From<GroupParticipantResponse> for GroupParticipant {
34    fn from(p: GroupParticipantResponse) -> Self {
35        Self {
36            jid: p.jid,
37            phone_number: p.phone_number,
38            is_admin: p.participant_type.is_admin(),
39        }
40    }
41}
42
43#[derive(Debug, Clone)]
44pub struct CreateGroupResult {
45    pub gid: Jid,
46}
47
48pub struct Groups<'a> {
49    client: &'a Client,
50}
51
52impl<'a> Groups<'a> {
53    pub(crate) fn new(client: &'a Client) -> Self {
54        Self { client }
55    }
56
57    pub async fn query_info(&self, jid: &Jid) -> Result<GroupInfo, anyhow::Error> {
58        if let Some(cached) = self.client.get_group_cache().await.get(jid).await {
59            return Ok(cached);
60        }
61
62        let group = self.client.execute(GroupQueryIq::new(jid)).await?;
63
64        let participants: Vec<Jid> = group.participants.iter().map(|p| p.jid.clone()).collect();
65
66        let lid_to_pn_map: HashMap<String, Jid> = if group.addressing_mode == AddressingMode::Lid {
67            group
68                .participants
69                .iter()
70                .filter_map(|p| {
71                    p.phone_number
72                        .as_ref()
73                        .map(|pn| (p.jid.user.clone(), pn.clone()))
74                })
75                .collect()
76        } else {
77            HashMap::new()
78        };
79
80        let mut info = GroupInfo::new(participants, group.addressing_mode);
81        if !lid_to_pn_map.is_empty() {
82            info.set_lid_to_pn_map(lid_to_pn_map);
83        }
84
85        self.client
86            .get_group_cache()
87            .await
88            .insert(jid.clone(), info.clone())
89            .await;
90
91        Ok(info)
92    }
93
94    pub async fn get_participating(&self) -> Result<HashMap<String, GroupMetadata>, anyhow::Error> {
95        let response = self.client.execute(GroupParticipatingIq::new()).await?;
96
97        let result = response
98            .groups
99            .into_iter()
100            .map(|group| {
101                let key = group.id.to_string();
102                let metadata = GroupMetadata {
103                    id: group.id,
104                    subject: group.subject.into_string(),
105                    participants: group.participants.into_iter().map(Into::into).collect(),
106                    addressing_mode: group.addressing_mode,
107                };
108                (key, metadata)
109            })
110            .collect();
111
112        Ok(result)
113    }
114
115    pub async fn get_metadata(&self, jid: &Jid) -> Result<GroupMetadata, anyhow::Error> {
116        let group = self.client.execute(GroupQueryIq::new(jid)).await?;
117
118        Ok(GroupMetadata {
119            id: group.id,
120            subject: group.subject.into_string(),
121            participants: group.participants.into_iter().map(Into::into).collect(),
122            addressing_mode: group.addressing_mode,
123        })
124    }
125
126    pub async fn create_group(
127        &self,
128        mut options: GroupCreateOptions,
129    ) -> Result<CreateGroupResult, anyhow::Error> {
130        // Resolve phone numbers for LID participants that don't have one
131        let mut resolved_participants = Vec::with_capacity(options.participants.len());
132
133        for participant in options.participants {
134            let resolved = if participant.jid.is_lid() && participant.phone_number.is_none() {
135                let phone_number = self
136                    .client
137                    .get_phone_number_from_lid(&participant.jid.user)
138                    .await
139                    .ok_or_else(|| {
140                        anyhow::anyhow!("Missing phone number mapping for LID {}", participant.jid)
141                    })?;
142                participant.with_phone_number(Jid::pn(phone_number))
143            } else {
144                participant
145            };
146            resolved_participants.push(resolved);
147        }
148
149        options.participants = normalize_participants(&resolved_participants);
150
151        let gid = self.client.execute(GroupCreateIq::new(options)).await?;
152
153        Ok(CreateGroupResult { gid })
154    }
155
156    pub async fn set_subject(&self, jid: &Jid, subject: GroupSubject) -> Result<(), anyhow::Error> {
157        Ok(self
158            .client
159            .execute(SetGroupSubjectIq::new(jid, subject))
160            .await?)
161    }
162
163    /// Sets or deletes a group's description.
164    ///
165    /// `prev` is the current description ID (from group metadata) used for
166    /// conflict detection. Pass `None` if unknown.
167    pub async fn set_description(
168        &self,
169        jid: &Jid,
170        description: Option<GroupDescription>,
171        prev: Option<String>,
172    ) -> Result<(), anyhow::Error> {
173        Ok(self
174            .client
175            .execute(SetGroupDescriptionIq::new(jid, description, prev))
176            .await?)
177    }
178
179    pub async fn leave(&self, jid: &Jid) -> Result<(), anyhow::Error> {
180        Ok(self.client.execute(LeaveGroupIq::new(jid)).await?)
181    }
182
183    pub async fn add_participants(
184        &self,
185        jid: &Jid,
186        participants: &[Jid],
187    ) -> Result<Vec<ParticipantChangeResponse>, anyhow::Error> {
188        Ok(self
189            .client
190            .execute(AddParticipantsIq::new(jid, participants))
191            .await?)
192    }
193
194    pub async fn remove_participants(
195        &self,
196        jid: &Jid,
197        participants: &[Jid],
198    ) -> Result<Vec<ParticipantChangeResponse>, anyhow::Error> {
199        Ok(self
200            .client
201            .execute(RemoveParticipantsIq::new(jid, participants))
202            .await?)
203    }
204
205    pub async fn promote_participants(
206        &self,
207        jid: &Jid,
208        participants: &[Jid],
209    ) -> Result<(), anyhow::Error> {
210        Ok(self
211            .client
212            .execute(PromoteParticipantsIq::new(jid, participants))
213            .await?)
214    }
215
216    pub async fn demote_participants(
217        &self,
218        jid: &Jid,
219        participants: &[Jid],
220    ) -> Result<(), anyhow::Error> {
221        Ok(self
222            .client
223            .execute(DemoteParticipantsIq::new(jid, participants))
224            .await?)
225    }
226
227    pub async fn get_invite_link(&self, jid: &Jid, reset: bool) -> Result<String, anyhow::Error> {
228        Ok(self
229            .client
230            .execute(GetGroupInviteLinkIq::new(jid, reset))
231            .await?)
232    }
233}
234
235impl Client {
236    pub fn groups(&self) -> Groups<'_> {
237        Groups::new(self)
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn test_group_metadata_struct() {
247        let jid: Jid = "123456789@g.us"
248            .parse()
249            .expect("test group JID should be valid");
250        let participant_jid: Jid = "1234567890@s.whatsapp.net"
251            .parse()
252            .expect("test participant JID should be valid");
253
254        let metadata = GroupMetadata {
255            id: jid.clone(),
256            subject: "Test Group".to_string(),
257            participants: vec![GroupParticipant {
258                jid: participant_jid,
259                phone_number: None,
260                is_admin: true,
261            }],
262            addressing_mode: AddressingMode::Pn,
263        };
264
265        assert_eq!(metadata.subject, "Test Group");
266        assert_eq!(metadata.participants.len(), 1);
267        assert!(metadata.participants[0].is_admin);
268    }
269
270    // Protocol-level tests (node building, parsing, validation) are in wacore/src/iq/groups.rs
271}