1use wacore::WireEnum;
2use wacore_binary::Jid;
3use waproto::whatsapp as wa;
4
5use crate::client::Client;
6use crate::send::SendResult;
7use crate::upload::UploadResponse;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, WireEnum)]
12#[non_exhaustive]
13pub enum StatusPrivacySetting {
14 #[wire_default]
16 #[wire = "contacts"]
17 Contacts,
18 #[wire = "allowlist"]
20 AllowList,
21 #[wire = "denylist"]
23 DenyList,
24}
25
26#[derive(Debug, Clone, Default)]
28pub struct StatusSendOptions {
29 pub privacy: StatusPrivacySetting,
31}
32
33pub struct Status<'a> {
35 client: &'a Client,
36}
37
38impl<'a> Status<'a> {
39 pub(crate) fn new(client: &'a Client) -> Self {
40 Self { client }
41 }
42
43 pub async fn send_text(
48 &self,
49 text: &str,
50 background_argb: u32,
51 font: i32,
52 recipients: &[Jid],
53 options: StatusSendOptions,
54 ) -> Result<SendResult, anyhow::Error> {
55 let message = wa::Message {
56 extended_text_message: Some(Box::new(wa::message::ExtendedTextMessage {
57 text: Some(text.to_string()),
58 background_argb: Some(background_argb),
59 font: Some(font),
60 ..Default::default()
61 })),
62 ..Default::default()
63 };
64
65 self.client
66 .send_status_message(message, recipients, options)
67 .await
68 }
69
70 pub async fn send_image(
75 &self,
76 upload: UploadResponse,
77 thumbnail: Vec<u8>,
78 caption: Option<&str>,
79 recipients: &[Jid],
80 options: StatusSendOptions,
81 ) -> Result<SendResult, anyhow::Error> {
82 let message = wa::Message {
83 image_message: Some(Box::new(wa::message::ImageMessage {
84 url: Some(upload.url),
85 direct_path: Some(upload.direct_path),
86 media_key: Some(upload.media_key.to_vec()),
87 file_sha256: Some(upload.file_sha256.to_vec()),
88 file_enc_sha256: Some(upload.file_enc_sha256.to_vec()),
89 file_length: Some(upload.file_length),
90 mimetype: Some("image/jpeg".to_string()),
91 jpeg_thumbnail: Some(thumbnail),
92 caption: caption.map(|c| c.to_string()),
93 ..Default::default()
94 })),
95 ..Default::default()
96 };
97
98 self.client
99 .send_status_message(message, recipients, options)
100 .await
101 }
102
103 pub async fn send_video(
108 &self,
109 upload: UploadResponse,
110 thumbnail: Vec<u8>,
111 duration_seconds: u32,
112 caption: Option<&str>,
113 recipients: &[Jid],
114 options: StatusSendOptions,
115 ) -> Result<SendResult, anyhow::Error> {
116 let message = wa::Message {
117 video_message: Some(Box::new(wa::message::VideoMessage {
118 url: Some(upload.url),
119 direct_path: Some(upload.direct_path),
120 media_key: Some(upload.media_key.to_vec()),
121 file_sha256: Some(upload.file_sha256.to_vec()),
122 file_enc_sha256: Some(upload.file_enc_sha256.to_vec()),
123 file_length: Some(upload.file_length),
124 mimetype: Some("video/mp4".to_string()),
125 jpeg_thumbnail: Some(thumbnail),
126 seconds: Some(duration_seconds),
127 caption: caption.map(|c| c.to_string()),
128 ..Default::default()
129 })),
130 ..Default::default()
131 };
132
133 self.client
134 .send_status_message(message, recipients, options)
135 .await
136 }
137
138 pub async fn send_raw(
142 &self,
143 message: wa::Message,
144 recipients: &[Jid],
145 options: StatusSendOptions,
146 ) -> Result<SendResult, anyhow::Error> {
147 self.client
148 .send_status_message(message, recipients, options)
149 .await
150 }
151
152 pub async fn revoke(
157 &self,
158 message_id: impl Into<String>,
159 recipients: &[Jid],
160 options: StatusSendOptions,
161 ) -> Result<SendResult, anyhow::Error> {
162 let message_id = message_id.into();
163 let to = Jid::status_broadcast();
164
165 let revoke_message = wa::Message {
166 protocol_message: Some(Box::new(wa::message::ProtocolMessage {
167 key: Some(wa::MessageKey {
168 remote_jid: Some(to.to_string()),
169 from_me: Some(true),
170 id: Some(message_id),
171 participant: None,
172 }),
173 r#type: Some(wa::message::protocol_message::Type::Revoke as i32),
174 ..Default::default()
175 })),
176 ..Default::default()
177 };
178
179 self.client
180 .send_status_message(revoke_message, recipients, options)
181 .await
182 }
183}
184
185impl Client {
186 pub fn status(&self) -> Status<'_> {
200 Status::new(self)
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn test_status_privacy_setting_values() {
210 assert_eq!(StatusPrivacySetting::Contacts.as_str(), "contacts");
212 assert_eq!(StatusPrivacySetting::AllowList.as_str(), "allowlist");
213 assert_eq!(StatusPrivacySetting::DenyList.as_str(), "denylist");
214 }
215
216 #[test]
217 fn test_status_privacy_default_is_contacts() {
218 let default = StatusPrivacySetting::default();
219 assert_eq!(default.as_str(), "contacts");
220 }
221
222 #[test]
223 fn test_status_send_options_default() {
224 let opts = StatusSendOptions::default();
225 assert_eq!(opts.privacy.as_str(), "contacts");
226 }
227
228 #[test]
229 fn test_status_text_message_structure() {
230 let text = "Hello from Rust!";
232 let bg = 0xFF1E6E4F_u32;
233 let font = 2_i32;
234
235 let message = waproto::whatsapp::Message {
236 extended_text_message: Some(Box::new(
237 waproto::whatsapp::message::ExtendedTextMessage {
238 text: Some(text.to_string()),
239 background_argb: Some(bg),
240 font: Some(font),
241 ..Default::default()
242 },
243 )),
244 ..Default::default()
245 };
246
247 let ext = message.extended_text_message.as_ref().unwrap();
248 assert_eq!(ext.text.as_deref(), Some(text));
249 assert_eq!(ext.background_argb, Some(bg));
250 assert_eq!(ext.font, Some(font));
251 }
252
253 #[test]
254 fn test_status_revoke_message_structure() {
255 use waproto::whatsapp as wa;
256
257 let original_id = "3EB06D00CAB92340790621";
258 let to = Jid::status_broadcast();
259
260 let revoke_message = wa::Message {
261 protocol_message: Some(Box::new(wa::message::ProtocolMessage {
262 key: Some(wa::MessageKey {
263 remote_jid: Some(to.to_string()),
264 from_me: Some(true),
265 id: Some(original_id.to_string()),
266 participant: None,
267 }),
268 r#type: Some(wa::message::protocol_message::Type::Revoke as i32),
269 ..Default::default()
270 })),
271 ..Default::default()
272 };
273
274 let pm = revoke_message.protocol_message.as_ref().unwrap();
275 assert_eq!(
276 pm.r#type,
277 Some(wa::message::protocol_message::Type::Revoke as i32)
278 );
279 let key = pm.key.as_ref().unwrap();
280 assert_eq!(key.remote_jid.as_deref(), Some("status@broadcast"));
281 assert_eq!(key.from_me, Some(true));
282 assert_eq!(key.id.as_deref(), Some(original_id));
283 }
284
285 #[test]
286 fn test_revoke_is_detected_as_revoke() {
287 use waproto::whatsapp as wa;
288
289 let text_msg = wa::Message {
291 extended_text_message: Some(Box::new(wa::message::ExtendedTextMessage {
292 text: Some("hello".to_string()),
293 ..Default::default()
294 })),
295 ..Default::default()
296 };
297 let is_revoke = text_msg.protocol_message.as_ref().is_some_and(|pm| {
298 pm.r#type == Some(wa::message::protocol_message::Type::Revoke as i32)
299 });
300 assert!(!is_revoke, "text message should not be detected as revoke");
301
302 let revoke_msg = wa::Message {
304 protocol_message: Some(Box::new(wa::message::ProtocolMessage {
305 r#type: Some(wa::message::protocol_message::Type::Revoke as i32),
306 ..Default::default()
307 })),
308 ..Default::default()
309 };
310 let is_revoke = revoke_msg.protocol_message.as_ref().is_some_and(|pm| {
311 pm.r#type == Some(wa::message::protocol_message::Type::Revoke as i32)
312 });
313 assert!(is_revoke, "revoke message should be detected as revoke");
314 }
315}