1use wacore::WireEnum;
2use wacore_binary::Jid;
3use waproto::whatsapp as wa;
4
5use crate::cache::Freshness;
6use crate::client::Client;
7use crate::send::{SendError, SendResult};
8use crate::upload::UploadResponse;
9use wacore_binary::Node;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, WireEnum)]
14#[non_exhaustive]
15pub enum StatusPrivacySetting {
16 #[wire_default]
18 #[wire = "contacts"]
19 Contacts,
20 #[wire = "allowlist"]
22 AllowList,
23 #[wire = "denylist"]
25 DenyList,
26}
27
28#[derive(Debug, Clone, Default)]
30pub struct StatusSendOptions {
31 pub privacy: StatusPrivacySetting,
33 pub message_id: Option<String>,
35 pub extra_stanza_nodes: Vec<Node>,
37 pub device_freshness: Freshness,
39}
40
41pub struct Status<'a> {
43 client: &'a Client,
44}
45
46impl<'a> Status<'a> {
47 pub(crate) fn new(client: &'a Client) -> Self {
48 Self { client }
49 }
50
51 pub async fn send_text(
57 &self,
58 text: &str,
59 background_argb: u32,
60 font: wa::message::extended_text_message::FontType,
61 recipients: &[Jid],
62 options: StatusSendOptions,
63 ) -> Result<SendResult, SendError> {
64 let message = wa::Message {
65 extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
66 text: Some(text.to_string()),
67 background_argb: Some(background_argb),
68 font: Some(font),
69 ..Default::default()
70 }),
71 ..Default::default()
72 };
73
74 self.client
75 .send_status_message(message, recipients, options)
76 .await
77 }
78
79 pub async fn send_image(
84 &self,
85 upload: UploadResponse,
86 thumbnail: Vec<u8>,
87 caption: Option<&str>,
88 recipients: &[Jid],
89 options: StatusSendOptions,
90 ) -> Result<SendResult, SendError> {
91 let message = crate::media::image_message(
92 upload,
93 crate::media::ImageOptions {
94 caption: caption.map(|c| c.to_string()),
95 jpeg_thumbnail: Some(thumbnail),
96 ..Default::default()
97 },
98 );
99
100 self.client
101 .send_status_message(message, recipients, options)
102 .await
103 }
104
105 pub async fn send_video(
110 &self,
111 upload: UploadResponse,
112 thumbnail: Vec<u8>,
113 duration_seconds: u32,
114 caption: Option<&str>,
115 recipients: &[Jid],
116 options: StatusSendOptions,
117 ) -> Result<SendResult, SendError> {
118 let message = crate::media::video_message(
119 upload,
120 crate::media::VideoOptions {
121 caption: caption.map(|c| c.to_string()),
122 jpeg_thumbnail: Some(thumbnail),
123 duration_seconds: Some(duration_seconds),
124 ..Default::default()
125 },
126 );
127
128 self.client
129 .send_status_message(message, recipients, options)
130 .await
131 }
132
133 pub async fn send_raw(
137 &self,
138 message: wa::Message,
139 recipients: &[Jid],
140 options: StatusSendOptions,
141 ) -> Result<SendResult, SendError> {
142 self.client
143 .send_status_message(message, recipients, options)
144 .await
145 }
146
147 pub async fn revoke(
152 &self,
153 message_id: impl Into<String>,
154 recipients: &[Jid],
155 options: StatusSendOptions,
156 ) -> Result<SendResult, SendError> {
157 let message_id = message_id.into();
158 let to = Jid::status_broadcast();
159
160 let revoke_message = wa::Message {
161 protocol_message: buffa::MessageField::some(wa::message::ProtocolMessage {
162 key: buffa::MessageField::some(wa::MessageKey {
163 remote_jid: Some(to.to_string()),
164 from_me: Some(true),
165 id: Some(message_id),
166 ..Default::default()
167 }),
168 r#type: Some(wa::message::protocol_message::Type::REVOKE),
169 ..Default::default()
170 }),
171 ..Default::default()
172 };
173
174 self.client
175 .send_status_message(revoke_message, recipients, options)
176 .await
177 }
178}
179
180impl Client {
181 pub fn status(&self) -> Status<'_> {
196 Status::new(self)
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_status_privacy_setting_values() {
206 assert_eq!(StatusPrivacySetting::Contacts.as_str(), "contacts");
208 assert_eq!(StatusPrivacySetting::AllowList.as_str(), "allowlist");
209 assert_eq!(StatusPrivacySetting::DenyList.as_str(), "denylist");
210 }
211
212 #[test]
213 fn test_status_privacy_default_is_contacts() {
214 let default = StatusPrivacySetting::default();
215 assert_eq!(default.as_str(), "contacts");
216 }
217
218 #[test]
219 fn test_status_send_options_default() {
220 let opts = StatusSendOptions::default();
221 assert_eq!(opts.privacy.as_str(), "contacts");
222 }
223
224 #[test]
225 fn test_status_text_message_structure() {
226 use waproto::whatsapp::message::extended_text_message::FontType;
228 let text = "Hello from Rust!";
229 let bg = 0xFF1E6E4F_u32;
230 let font = FontType::FB_SCRIPT;
231
232 let message = waproto::whatsapp::Message {
233 extended_text_message: buffa::MessageField::some(
234 waproto::whatsapp::message::ExtendedTextMessage {
235 text: Some(text.to_string()),
236 background_argb: Some(bg),
237 font: Some(font),
238 ..Default::default()
239 },
240 ),
241 ..Default::default()
242 };
243
244 let ext = message.extended_text_message.as_option().unwrap();
245 assert_eq!(ext.text.as_deref(), Some(text));
246 assert_eq!(ext.background_argb, Some(bg));
247 assert_eq!(ext.font, Some(font));
248 }
249
250 #[test]
251 fn test_status_revoke_message_structure() {
252 use waproto::whatsapp as wa;
253
254 let original_id = "3EB06D00CAB92340790621";
255 let to = Jid::status_broadcast();
256
257 let revoke_message = wa::Message {
258 protocol_message: buffa::MessageField::some(wa::message::ProtocolMessage {
259 key: wa::MessageKey {
260 remote_jid: Some(to.to_string()),
261 from_me: Some(true),
262 id: Some(original_id.to_string()),
263 ..Default::default()
264 }
265 .into(),
266 r#type: Some(wa::message::protocol_message::Type::REVOKE),
267 ..Default::default()
268 }),
269 ..Default::default()
270 };
271
272 let pm = revoke_message.protocol_message.as_option().unwrap();
273 assert_eq!(pm.r#type, Some(wa::message::protocol_message::Type::REVOKE));
274 let key = pm.key.as_option().unwrap();
275 assert_eq!(key.remote_jid.as_deref(), Some("status@broadcast"));
276 assert_eq!(key.from_me, Some(true));
277 assert_eq!(key.id.as_deref(), Some(original_id));
278 }
279
280 #[test]
281 fn test_revoke_is_detected_as_revoke() {
282 use waproto::whatsapp as wa;
283
284 let text_msg = wa::Message {
286 extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
287 text: Some("hello".to_string()),
288 ..Default::default()
289 }),
290 ..Default::default()
291 };
292 let is_revoke = text_msg
293 .protocol_message
294 .as_option()
295 .is_some_and(|pm| pm.r#type == Some(wa::message::protocol_message::Type::REVOKE));
296 assert!(!is_revoke, "text message should not be detected as revoke");
297
298 let revoke_msg = wa::Message {
300 protocol_message: buffa::MessageField::some(wa::message::ProtocolMessage {
301 r#type: Some(wa::message::protocol_message::Type::REVOKE),
302 ..Default::default()
303 }),
304 ..Default::default()
305 };
306 let is_revoke = revoke_msg
307 .protocol_message
308 .as_option()
309 .is_some_and(|pm| pm.r#type == Some(wa::message::protocol_message::Type::REVOKE));
310 assert!(is_revoke, "revoke message should be detected as revoke");
311 }
312}