1use super::*;
4
5impl Client {
6 pub async fn send_raw_bytes(&self, plaintext: Vec<u8>) -> Result<(), ClientError> {
15 let noise_socket = self.get_noise_socket().await?;
16 noise_socket
19 .encrypt_and_send(bytes::Bytes::from(plaintext))
20 .await?;
21 Ok(())
22 }
23
24 pub(crate) const MAX_INLINE_BURST: usize = 4;
28
29 pub(crate) async fn send_raw_bytes_burst(
51 &self,
52 frames: &mut Vec<Vec<u8>>,
53 results: &mut Vec<crate::socket::error::EncryptSendResult>,
54 ) -> Result<(), ClientError> {
55 results.clear();
56 let noise_socket = match self.get_noise_socket().await {
57 Ok(socket) => socket,
58 Err(error) => {
59 frames.clear();
60 return Err(error);
61 }
62 };
63 if frames.len() == 1 {
64 let plaintext = frames.pop().expect("length checked");
65 results.push(
66 noise_socket
67 .encrypt_and_send(bytes::Bytes::from(plaintext))
68 .await,
69 );
70 return Ok(());
71 }
72 let mut receivers: smallvec::SmallVec<[_; Self::MAX_INLINE_BURST]> =
79 smallvec::SmallVec::new();
80 let mut frames_after_enqueue_failed = 0usize;
85 for plaintext in frames.drain(..) {
86 if frames_after_enqueue_failed > 0 {
87 frames_after_enqueue_failed += 1;
88 continue;
89 }
90 match noise_socket
91 .enqueue_send(bytes::Bytes::from(plaintext))
92 .await
93 {
94 Ok(receiver) => receivers.push(receiver),
95 Err(_) => frames_after_enqueue_failed = 1,
96 }
97 }
98
99 for receiver in receivers {
100 results.push(NoiseSocket::await_send(receiver).await);
101 }
102 for _ in 0..frames_after_enqueue_failed {
103 results.push(Err(EncryptSendError::channel_closed()));
104 }
105 Ok(())
106 }
107
108 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.send.node", level = "debug", skip_all, fields(tag = %node.tag), err(Debug)))]
109 pub async fn send_node(&self, node: Node) -> Result<(), ClientError> {
110 let plaintext_buf = self.marshal_node_for_send(node)?;
111 self.send_raw_bytes(plaintext_buf).await
112 }
113
114 pub(crate) fn marshal_node_for_send(&self, node: Node) -> Result<Vec<u8>, ClientError> {
119 debug!(target: "Client/Send", "{}", DisplayableNode(&node));
120 if self.sent_node_waiter_count.load(Ordering::Acquire) > 0 {
121 self.resolve_sent_node_waiters(&Arc::new(node.clone()));
122 }
123
124 wacore_binary::marshal::marshal_exact(&node).map_err(|e| {
127 error!("Failed to marshal node: {e:?}");
128 SocketError::Marshal(e).into()
129 })
130 }
131
132 #[cfg_attr(
133 feature = "tracing",
134 tracing::instrument(name = "wa.send.unified_session", level = "debug", skip_all)
135 )]
136 pub(crate) async fn send_unified_session(&self) {
137 if !self.is_connected() {
138 debug!(target: "Client/UnifiedSession", "Skipping: not connected");
139 return;
140 }
141
142 let Some((node, _sequence)) = self.unified_session.prepare_send().await else {
143 return;
144 };
145
146 if let Err(e) = self.send_node(node).await {
147 debug!(target: "Client/UnifiedSession", "Send failed: {e}");
148 self.unified_session.clear_last_sent().await;
149 }
150 }
151
152 pub async fn edit_message(
153 &self,
154 to: impl Into<Jid>,
155 original_id: impl Into<String>,
156 new_content: wa::Message,
157 ) -> Result<String, crate::send::SendError> {
158 self.edit_message_inner(to.into(), original_id.into(), new_content, None)
159 .await
160 }
161
162 pub async fn edit_message_with_options(
175 &self,
176 to: impl Into<Jid>,
177 original_id: impl Into<String>,
178 new_content: wa::Message,
179 options: crate::send::EditOptions,
180 ) -> Result<String, crate::send::SendError> {
181 self.edit_message_inner(
182 to.into(),
183 original_id.into(),
184 new_content,
185 options.stanza_id,
186 )
187 .await
188 }
189
190 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.send.edit", level = "debug", skip_all, fields(to = %to.observe()), err(Debug)))]
195 async fn edit_message_inner(
196 &self,
197 to: Jid,
198 original_id: String,
199 new_content: wa::Message,
200 request_id: Option<String>,
201 ) -> Result<String, crate::send::SendError> {
202 let participant = if to.is_group() {
205 Some(
206 self.get_own_jid_for_group(&to)
207 .await
208 .map_err(crate::send::SendError::from_anyhow)?
209 .to_non_ad()
210 .to_string(),
211 )
212 } else {
213 if self.pn().is_none() {
214 return Err(crate::send::SendError::NotLoggedIn);
215 }
216 None
217 };
218
219 let edit_container_message = crate::send::build_edit_message(
220 &to,
221 original_id.clone(),
222 participant,
223 new_content,
224 wacore::time::now_millis(),
225 );
226
227 let borrowed_message_id = request_id.is_some();
236 self.send_message_impl(
237 to,
238 &edit_container_message,
239 crate::send::SendPipelineOptions {
240 edit: Some(crate::types::message::EditAttribute::MessageEdit),
241 request_id: request_id.as_deref(),
242 borrowed_message_id,
243 ..Default::default()
244 },
245 )
246 .await
247 .map_err(crate::send::SendError::from_anyhow)?;
248
249 Ok(original_id)
250 }
251
252 pub async fn edit_message_encrypted(
261 &self,
262 to: impl Into<Jid>,
263 original_id: impl Into<String>,
264 message_secret: &[u8],
265 new_content: wa::Message,
266 ) -> Result<String, crate::send::SendError> {
267 self.edit_message_encrypted_inner(
268 to.into(),
269 original_id.into(),
270 message_secret,
271 new_content,
272 )
273 .await
274 }
275
276 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.send.edit_encrypted", level = "debug", skip_all, fields(to = %to.observe()), err(Debug)))]
277 async fn edit_message_encrypted_inner(
278 &self,
279 to: Jid,
280 original_id: String,
281 message_secret: &[u8],
282 new_content: wa::Message,
283 ) -> Result<String, crate::send::SendError> {
284 use crate::send::SendError;
285 if to.is_newsletter() {
289 return Err(SendError::InvalidRequest(
290 "edit_message_encrypted is not valid for newsletters/channels; use newsletter().edit_message"
291 .into(),
292 ));
293 }
294 if message_secret.len() != 32 {
295 return Err(SendError::InvalidRequest(format!(
296 "message_secret must be exactly 32 bytes, got {}",
297 message_secret.len()
298 )));
299 }
300
301 let self_jid = if to.is_group() {
302 self.get_own_jid_for_group(&to)
303 .await
304 .map_err(SendError::from_anyhow)?
305 .to_non_ad()
306 } else {
307 self.pn().ok_or(SendError::NotLoggedIn)?.to_non_ad()
308 };
309 let participant = if to.is_group() {
310 Some(self_jid.to_string())
311 } else {
312 None
313 };
314
315 let envelope = build_secret_message_edit(
316 &to,
317 &original_id,
318 participant,
319 &self_jid.to_string(),
320 message_secret,
321 new_content,
322 )?;
323
324 self.send_message_impl(
325 to,
326 &envelope,
327 crate::send::SendPipelineOptions {
328 edit: Some(crate::types::message::EditAttribute::MessageEdit),
329 ..Default::default()
330 },
331 )
332 .await
333 .map_err(SendError::from_anyhow)?;
334
335 Ok(original_id)
336 }
337
338 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.send.server_reaction", level = "debug", skip_all, fields(to = %to.observe()), err(Debug)))]
340 pub(crate) async fn send_server_reaction(
341 &self,
342 to: &Jid,
343 server_id: u64,
344 reaction: &str,
345 ) -> Result<(), anyhow::Error> {
346 let request_id = self.generate_message_id();
347
348 let stanza = NodeBuilder::new("message")
349 .attr("to", to)
350 .attr("type", "reaction")
351 .attr("id", &request_id)
352 .attr("server_id", server_id)
353 .children([NodeBuilder::new("reaction").attr("code", reaction).build()])
354 .build();
355
356 self.send_node(stanza).await?;
357 Ok(())
358 }
359
360 #[cfg(feature = "voip-runtime")]
370 pub(crate) fn register_ack_waiter(
371 &self,
372 message_id: &str,
373 ) -> futures::channel::oneshot::Receiver<Arc<wacore_binary::OwnedNodeRef>> {
374 let (tx, rx) = futures::channel::oneshot::channel();
375 self.response_waiters_guard()
376 .insert(message_id.to_string(), ResponseWaiter::Iq(tx));
377 rx
378 }
379
380 pub(crate) fn register_phash_waiter(
386 &self,
387 message_id: &str,
388 expected: wacore_binary::CompactString,
389 jid: Jid,
390 invalidate_group_cache: bool,
391 ) {
392 let mut waiters = self.response_waiters_guard();
393 let registered_epoch = waiters.current_epoch();
397 waiters.insert(
398 message_id.to_string(),
399 ResponseWaiter::Phash(PhashWaiter {
400 expected,
401 jid,
402 invalidate_group_cache,
403 registered_epoch,
404 }),
405 );
406 }
407
408 pub(crate) async fn make_chat_message_id(&self, chat: &Jid, id: &str) -> ChatMessageId {
410 let chat = self.resolve_encryption_jid(chat).await;
412
413 ChatMessageId {
414 chat,
415 id: id.to_owned(),
416 }
417 }
418
419 #[cfg_attr(
420 feature = "tracing",
421 tracing::instrument(name = "wa.send.protocol_receipt", level = "debug", skip_all)
422 )]
423 pub(crate) async fn send_protocol_receipt(
424 &self,
425 id: String,
426 receipt_type: crate::types::presence::ReceiptType,
427 ) {
428 if id.is_empty() {
429 return;
430 }
431 let device_snapshot = self.persistence_manager.get_device_snapshot();
432 if let Some(own_jid) = &device_snapshot.pn {
433 let type_str = receipt_type.as_wire_str();
436
437 let node = NodeBuilder::new("receipt")
441 .attr("id", id.as_str())
442 .attr("type", type_str)
443 .attr("to", own_jid.to_non_ad_string())
444 .build();
445
446 if let Err(e) = self.send_node(node).await {
447 warn!(
448 "Failed to send protocol receipt of type {:?} for message ID {}: {:?}",
449 receipt_type, id, e
450 );
451 }
452 }
453 }
454
455 pub async fn register_chatstate_handler(
459 &self,
460 handler: Arc<dyn Fn(ChatStateEvent) + Send + Sync>,
461 ) {
462 self.chatstate_handlers.write().await.push(handler);
463 }
464
465 #[cfg_attr(
469 feature = "tracing",
470 tracing::instrument(name = "wa.notif.chatstate", level = "debug", skip_all)
471 )]
472 pub(crate) async fn dispatch_chatstate_event(
473 &self,
474 stanza: wacore::iq::chatstate::ChatstateStanza,
475 ) {
476 use wacore::iq::chatstate::{ChatstateSource, ReceivedChatState};
477 use wacore::types::events::ChatPresenceUpdate;
478 use wacore::types::message::MessageSource;
479 use wacore::types::presence::{ChatPresence, ChatPresenceMedia};
480
481 let (chat, sender, is_group) = match &stanza.source {
483 ChatstateSource::User { from } => (from.clone(), from.clone(), false),
484 ChatstateSource::Group { from, participant } => {
485 (from.clone(), participant.clone(), true)
486 }
487 };
488
489 let (state, media) = match stanza.state {
490 ReceivedChatState::Typing => (ChatPresence::Composing, ChatPresenceMedia::Text),
491 ReceivedChatState::RecordingAudio => {
492 (ChatPresence::Composing, ChatPresenceMedia::Audio)
493 }
494 ReceivedChatState::Idle => (ChatPresence::Paused, ChatPresenceMedia::Text),
495 };
496
497 self.core.event_bus.dispatch(Event::ChatPresence(
498 ChatPresenceUpdate::builder()
499 .source(MessageSource {
500 chat,
501 sender,
502 is_from_me: false,
503 is_group,
504 addressing_mode: None,
505 sender_alt: None,
506 recipient_alt: None,
507 broadcast_list_owner: None,
508 recipient: None,
509 })
510 .state(state)
511 .media(media)
512 .build(),
513 ));
514
515 let event = ChatStateEvent::from_stanza(stanza);
517 let handlers = self.chatstate_handlers.read().await.clone();
518 for handler in handlers {
519 let event_clone = event.clone();
520 self.runtime
521 .spawn(Box::pin(async move {
522 (handler)(event_clone);
523 }))
524 .detach();
525 }
526 }
527
528 pub(crate) fn receipts_are_active(&self) -> bool {
531 self.send_active_receipts.load(Ordering::Acquire) != 0
532 }
533
534 pub fn set_force_active_delivery_receipts(&self, active: bool) {
537 self.send_active_receipts
538 .store(if active { 2 } else { 0 }, Ordering::Release);
539 }
540
541 pub(crate) fn mark_receipts_active_on_presence(&self) {
543 let _ =
544 self.send_active_receipts
545 .compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire);
546 }
547
548 pub(crate) fn mark_receipts_inactive_on_presence(&self) {
549 let _ =
550 self.send_active_receipts
551 .compare_exchange(1, 0, Ordering::AcqRel, Ordering::Acquire);
552 }
553}
554
555fn build_secret_message_edit(
559 to: &Jid,
560 original_id: &str,
561 participant: Option<String>,
562 self_jid_str: &str,
563 message_secret: &[u8],
564 new_content: wa::Message,
565) -> Result<wa::Message, anyhow::Error> {
566 let inner = crate::send::build_edit_message(
567 to,
568 original_id.to_string(),
569 participant.clone(),
570 new_content,
571 wacore::time::now_millis(),
572 );
573
574 let ctx = wacore::message_edit::MessageEditContext {
576 original_msg_id: original_id,
577 original_sender_jid: self_jid_str,
578 editor_jid: self_jid_str,
579 };
580 let (enc_payload, iv) =
581 wacore::message_edit::encrypt_message_edit(&inner, message_secret, &ctx)?;
582
583 Ok(wa::Message {
584 secret_encrypted_message: buffa::MessageField::some(wa::message::SecretEncryptedMessage {
585 target_message_key: buffa::MessageField::some(wa::MessageKey {
586 remote_jid: Some(to.to_string()),
587 from_me: Some(true),
588 id: Some(original_id.to_string()),
589 participant,
590 }),
591 enc_payload: Some(enc_payload),
592 enc_iv: Some(iv.to_vec()),
593 secret_enc_type: Some(
594 wa::message::secret_encrypted_message::SecretEncType::MessageEdit,
595 ),
596 remote_key_id: None,
597 }),
598 message_context_info: buffa::MessageField::some(wa::MessageContextInfo {
599 message_secret: Some(message_secret.to_vec()),
600 ..Default::default()
601 }),
602 ..Default::default()
603 })
604}
605
606#[cfg(test)]
607mod secret_message_edit_tests {
608 use super::*;
609
610 #[test]
611 fn secret_message_edit_roundtrip() {
612 let secret = [0x33u8; 32];
613 let to: Jid = "5511777777777@s.whatsapp.net".parse().unwrap();
614 let self_str = "5511999999999@s.whatsapp.net";
615 let new_content = wa::Message {
616 conversation: Some("edited!".into()),
617 ..Default::default()
618 };
619
620 let envelope =
621 build_secret_message_edit(&to, "ORIGID", None, self_str, &secret, new_content).unwrap();
622
623 let sem = envelope.secret_encrypted_message.as_option().unwrap();
624 assert_eq!(
625 sem.secret_enc_type,
626 Some(wa::message::secret_encrypted_message::SecretEncType::MessageEdit)
627 );
628 assert_eq!(
630 envelope
631 .message_context_info
632 .as_option()
633 .and_then(|c| c.message_secret.as_deref()),
634 Some(&secret[..])
635 );
636
637 let ctx = wacore::message_edit::MessageEditContext {
639 original_msg_id: "ORIGID",
640 original_sender_jid: self_str,
641 editor_jid: self_str,
642 };
643 let inner = wacore::message_edit::decrypt_message_edit(
644 sem.enc_payload.as_deref().unwrap(),
645 sem.enc_iv.as_deref().unwrap(),
646 &secret,
647 &ctx,
648 )
649 .unwrap();
650 let edited = inner
651 .protocol_message
652 .into_option()
653 .and_then(|pm| pm.edited_message.into_option())
654 .and_then(|m| m.conversation);
655 assert_eq!(edited.as_deref(), Some("edited!"));
656 }
657
658 #[test]
659 fn secret_message_edit_wrong_secret_fails_to_decrypt() {
660 let secret = [0x33u8; 32];
661 let to: Jid = "5511777777777@s.whatsapp.net".parse().unwrap();
662 let self_str = "5511999999999@s.whatsapp.net";
663 let envelope = build_secret_message_edit(
664 &to,
665 "ORIGID",
666 None,
667 self_str,
668 &secret,
669 wa::Message {
670 conversation: Some("edited!".into()),
671 ..Default::default()
672 },
673 )
674 .unwrap();
675 let sem = envelope.secret_encrypted_message.as_option().unwrap();
676 let ctx = wacore::message_edit::MessageEditContext {
677 original_msg_id: "ORIGID",
678 original_sender_jid: self_str,
679 editor_jid: self_str,
680 };
681 assert!(
682 wacore::message_edit::decrypt_message_edit(
683 sem.enc_payload.as_deref().unwrap(),
684 sem.enc_iv.as_deref().unwrap(),
685 &[0x00u8; 32],
686 &ctx,
687 )
688 .is_err()
689 );
690 }
691}