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