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