1use crate::client::Client;
9use crate::types::message::EditAttribute;
10use anyhow::anyhow;
11use log::debug;
12use wacore::libsignal::protocol::SignalProtocolError;
13use wacore::send::StanzaType;
14use wacore::types::jid::JidExt;
15use wacore::types::message::AddressingMode;
16#[cfg(test)]
17use wacore_binary::DeviceKey;
18use wacore_binary::Node;
19use wacore_binary::builder::NodeBuilder;
20use wacore_binary::{Jid, JidExt as _, Server};
21use waproto::whatsapp as wa;
22
23use crate::client::ClientError;
24use crate::features::GroupError;
25use crate::request::IqError;
26use thiserror::Error;
27
28mod actions;
29mod tctoken_lifecycle;
30
31#[derive(Debug, Error)]
40#[non_exhaustive]
41pub enum SendError {
42 #[error("{0}")]
46 Client(#[source] ClientError),
47 #[error("client is not logged in")]
49 NotLoggedIn,
50 #[error("IQ request failed: {0}")]
52 Iq(#[from] IqError),
53 #[error("invalid send request: {0}")]
56 InvalidRequest(String),
57 #[error("{0}")]
61 Internal(#[from] anyhow::Error),
62}
63
64impl SendError {
65 pub(crate) fn from_anyhow(err: anyhow::Error) -> Self {
71 let err = match err.downcast::<SendError>() {
75 Ok(send) => return send,
76 Err(other) => other,
77 };
78 let err = match err.downcast::<GroupError>() {
82 Ok(group) => return group.into(),
83 Err(other) => other,
84 };
85 match err.downcast::<ClientError>() {
86 Ok(client) => client.into(),
87 Err(other) => match other.downcast::<IqError>() {
88 Ok(iq) => SendError::Iq(iq),
89 Err(other) => SendError::Internal(other),
90 },
91 }
92 }
93}
94
95impl From<ClientError> for SendError {
96 fn from(err: ClientError) -> Self {
97 match err {
98 ClientError::NotLoggedIn => SendError::NotLoggedIn,
99 ClientError::Iq(iq) => SendError::Iq(iq),
100 client => SendError::Client(client),
101 }
102 }
103}
104
105impl From<GroupError> for SendError {
106 fn from(err: GroupError) -> Self {
107 match err {
108 GroupError::Iq(iq) => SendError::Iq(iq),
109 GroupError::InvalidRequest(msg) => SendError::InvalidRequest(msg),
110 GroupError::Internal(e) => SendError::from_anyhow(e),
111 group @ (GroupError::Mex(_) | GroupError::DescriptionConflict) => {
115 SendError::Internal(group.into())
116 }
117 }
118 }
119}
120
121fn ensure_self_in_group(
125 info: std::sync::Arc<wacore::client::context::GroupInfo>,
126 own_sending_jid: &Jid,
127) -> std::sync::Arc<wacore::client::context::GroupInfo> {
128 if info
129 .participants
130 .iter()
131 .any(|participant| participant.is_same_user_as(own_sending_jid))
132 {
133 info
134 } else {
135 let mut owned = (*info).clone();
136 owned.participants.push(own_sending_jid.to_non_ad());
137 std::sync::Arc::new(owned)
138 }
139}
140
141struct SkdmUpdate {
145 to_str: String,
146 devices: Vec<Jid>,
147 stale_users: Vec<String>,
148}
149
150struct SendBranchOutput {
154 node: Node,
155 msg_secret: Option<[u8; 32]>,
157 group_sender_identity: Option<Jid>,
160 skdm_update: Option<SkdmUpdate>,
161 distribution_guard: Option<async_lock::MutexGuardArc<()>>,
165 issue_tc_token_after_send: bool,
166 dm_phash: Option<wacore_binary::CompactString>,
167}
168
169struct GroupBranchRequest<'a> {
170 to: Jid,
171 message: &'a wa::Message,
172 request_id: &'a str,
173 force_key_distribution: bool,
174 edit: Option<EditAttribute>,
175 extra_stanza_nodes: &'a [Node],
176 group_metadata_freshness: crate::cache::Freshness,
177 device_freshness: crate::cache::Freshness,
178 borrowed_message_id: bool,
179}
180
181struct DmBranchRequest<'a> {
182 to: Jid,
183 message: &'a wa::Message,
184 request_id: &'a str,
185 sent_at: SendInstant,
186 edit: Option<EditAttribute>,
187 extra_stanza_nodes: Vec<Node>,
188 is_status_addon: bool,
189 device_freshness: crate::cache::Freshness,
190 borrowed_message_id: bool,
191}
192
193enum GroupDeviceSnapshot {
194 Owned(wacore::send::ResolvedGroupDevices),
195 Shared(std::sync::Arc<wacore::send::ResolvedGroupDevices>),
196}
197
198impl AsRef<wacore::send::ResolvedGroupDevices> for GroupDeviceSnapshot {
199 fn as_ref(&self) -> &wacore::send::ResolvedGroupDevices {
200 match self {
201 Self::Owned(devices) => devices,
202 Self::Shared(devices) => devices,
203 }
204 }
205}
206
207#[inline]
211#[cfg_attr(
212 feature = "tracing",
213 tracing::instrument(
214 name = "wa.send.branch_box",
215 level = "debug",
216 skip_all,
217 fields(future_bytes = size_of::<F>())
218 )
219)]
220fn box_send_branch<F>(future: F) -> std::pin::Pin<Box<F>>
221where
222 F: Future,
223{
224 Box::pin(future)
225}
226
227fn skdm_needs_only_own_devices(needs: &[Jid], own_pn: Option<&Jid>, own_lid: Option<&Jid>) -> bool {
232 !needs.is_empty()
233 && needs.iter().all(|j| {
234 own_pn.is_some_and(|p| j.is_same_user_as(p))
235 || own_lid.is_some_and(|l| j.is_same_user_as(l))
236 })
237}
238
239const RESERVED_EXTRA_STANZA_CHILDREN: &[&str] =
240 &["enc", "participants", "device-identity", "plaintext"];
241
242fn validate_extra_stanza_nodes(nodes: &[Node]) -> Result<(), SendError> {
243 if let Some(node) = nodes.iter().find(|node| {
244 RESERVED_EXTRA_STANZA_CHILDREN
245 .iter()
246 .any(|reserved| node.tag == *reserved)
247 }) {
248 return Err(SendError::InvalidRequest(format!(
249 "extra stanza child <{}> is reserved by the send pipeline",
250 node.tag
251 )));
252 }
253 Ok(())
254}
255
256impl SendBranchOutput {
257 fn stanza_only(node: Node) -> Self {
258 Self {
259 node,
260 msg_secret: None,
261 group_sender_identity: None,
262 skdm_update: None,
263 distribution_guard: None,
264 issue_tc_token_after_send: false,
265 dm_phash: None,
266 }
267 }
268}
269
270#[derive(Debug, Clone, Default)]
281#[non_exhaustive]
282pub struct SendOptions {
283 pub message_id: Option<String>,
286 pub extra_stanza_nodes: Vec<Node>,
288 pub ephemeral_expiration: Option<u32>,
292 pub stanza_type_override: Option<StanzaType>,
295 pub group_metadata_freshness: crate::cache::Freshness,
297 pub device_freshness: crate::cache::Freshness,
299}
300
301impl SendOptions {
302 #[must_use]
304 pub fn with_message_id(mut self, message_id: impl Into<String>) -> Self {
305 self.message_id = Some(message_id.into());
306 self
307 }
308
309 #[must_use]
311 pub fn with_extra_stanza_nodes(mut self, nodes: Vec<Node>) -> Self {
312 self.extra_stanza_nodes = nodes;
313 self
314 }
315
316 #[must_use]
318 pub fn with_ephemeral_expiration(mut self, seconds: u32) -> Self {
319 self.ephemeral_expiration = Some(seconds);
320 self
321 }
322
323 #[must_use]
325 pub fn with_stanza_type_override(mut self, stanza_type: StanzaType) -> Self {
326 self.stanza_type_override = Some(stanza_type);
327 self
328 }
329
330 #[must_use]
332 pub fn with_group_metadata_freshness(mut self, freshness: crate::cache::Freshness) -> Self {
333 self.group_metadata_freshness = freshness;
334 self
335 }
336
337 #[must_use]
339 pub fn with_device_freshness(mut self, freshness: crate::cache::Freshness) -> Self {
340 self.device_freshness = freshness;
341 self
342 }
343}
344
345#[derive(Debug, Clone, Default)]
351#[non_exhaustive]
352pub struct EditOptions {
353 pub stanza_id: Option<String>,
363}
364
365impl EditOptions {
366 #[must_use]
368 pub fn with_stanza_id(mut self, stanza_id: impl Into<String>) -> Self {
369 self.stanza_id = Some(stanza_id.into());
370 self
371 }
372}
373
374#[derive(Debug, Clone, Copy)]
381pub(crate) struct SendInstant(i64);
382
383impl SendInstant {
384 pub(crate) fn now() -> Self {
385 Self(wacore::time::now_secs())
386 }
387
388 pub(crate) fn unix_secs(self) -> i64 {
389 self.0
390 }
391
392 pub(crate) fn unix_secs_u64(self) -> u64 {
394 self.0.max(0) as u64
395 }
396}
397
398#[derive(Default)]
399pub(crate) struct SendPipelineOptions<'a> {
400 pub(crate) sent_at: Option<SendInstant>,
403 pub(crate) request_id: Option<&'a str>,
407 pub(crate) peer: bool,
408 pub(crate) force_key_distribution: bool,
409 pub(crate) edit: Option<EditAttribute>,
410 pub(crate) extra_stanza_nodes: Vec<Node>,
411 pub(crate) stanza_type: Option<StanzaType>,
412 pub(crate) group_metadata_freshness: crate::cache::Freshness,
413 pub(crate) device_freshness: crate::cache::Freshness,
414 pub(crate) borrowed_message_id: bool,
420}
421
422#[derive(Debug, Clone, PartialEq, Eq)]
424#[non_exhaustive]
425pub struct SendResult {
426 pub message_id: String,
427 pub to: Jid,
428}
429
430impl SendResult {
431 pub fn message_key(&self) -> wa::MessageKey {
433 wa::MessageKey {
434 remote_jid: Some(self.to.to_string()),
435 from_me: Some(true),
436 id: Some(self.message_id.clone()),
437 participant: None,
438 }
439 }
440}
441
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
444#[non_exhaustive]
445pub enum PinDuration {
446 Hours24,
447 #[default]
448 Days7,
449 Days30,
450}
451
452impl PinDuration {
453 fn as_secs(self) -> u32 {
454 match self {
455 Self::Hours24 => 86_400,
456 Self::Days7 => 604_800,
457 Self::Days30 => 2_592_000,
458 }
459 }
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, Default)]
464#[non_exhaustive]
465pub enum RevokeType {
466 #[default]
468 Sender,
469 Admin { original_sender: Jid },
472}
473
474pub(crate) fn infer_stanza_metadata(msg: &wa::Message) -> (Option<EditAttribute>, Option<Node>) {
482 use wacore::proto_helpers::MessageExt;
483 let edit = EditAttribute::infer_from_message(msg);
484
485 let mut meta = NodeBuilder::new("meta");
488 let mut has_attr = false;
489
490 if msg.poll_creation_message.is_set()
491 || msg.poll_creation_message_v2.is_set()
492 || msg.poll_creation_message_v3.is_set()
493 {
494 meta = meta.attr("polltype", "creation");
495 has_attr = true;
496 } else if let Some(poll_update) = msg.poll_update_message.as_option()
497 && poll_update.vote.is_set()
498 {
499 meta = meta.attr("polltype", "vote");
500 has_attr = true;
501 } else if msg.event_message.is_set() {
503 meta = meta.attr("event_type", "creation");
504 has_attr = true;
505 } else if msg.enc_event_response_message.is_set() {
506 meta = meta.attr("event_type", "response");
507 has_attr = true;
508 } else if let Some(sec) = msg.secret_encrypted_message.as_option()
509 && sec.secret_enc_type
510 == Some(wa::message::secret_encrypted_message::SecretEncType::EventEdit)
511 {
512 meta = meta.attr("event_type", "edit");
513 has_attr = true;
514 } else if let Some(ml) = msg
515 .protocol_message
516 .as_option()
517 .and_then(|pm| pm.member_label.as_option())
518 {
519 let tag_reason = if ml.label.as_deref().unwrap_or("").is_empty() {
523 "user_delete"
524 } else {
525 "user_update"
526 };
527 meta = meta
528 .attr("appdata", "member_tag")
529 .attr("tag_reason", tag_reason);
530 has_attr = true;
531 }
532
533 if msg.is_view_once() {
536 meta = meta.attr("view_once", "true");
537 has_attr = true;
538 }
539
540 (edit, has_attr.then(|| meta.build()))
541}
542
543fn validate_status_message_id(
544 message: &wa::Message,
545 outer_id: Option<&str>,
546) -> Result<(), SendError> {
547 let Some(outer_id) = outer_id else {
548 return Ok(());
549 };
550 if outer_id.is_empty() {
551 return Err(SendError::InvalidRequest(
552 "status message ID must not be empty".into(),
553 ));
554 }
555 if wacore::send::status_revoke_target_id(message) == Some(outer_id) {
556 return Err(SendError::InvalidRequest(
557 "status revoke stanza ID must differ from the revoked message ID".into(),
558 ));
559 }
560 Ok(())
561}
562
563const BIZ_PRIVACY_MODE_TS_OFFSET: u64 = 77_980_457;
567
568enum BizCategory<'a> {
569 PaymentSimple(&'a str),
574 Mixed,
576}
577
578fn classify_button(button_name: &str) -> BizCategory<'_> {
599 match button_name {
600 "payment_info" => BizCategory::PaymentSimple("payment_info"),
604 "review_and_pay" => BizCategory::PaymentSimple("order_details"),
605 "review_order" | "order_status" => BizCategory::PaymentSimple("order_status"),
606 "payment_status" => BizCategory::PaymentSimple("payment_status"),
607 "payment_method" => BizCategory::PaymentSimple("payment_method"),
608 "payment_reminder" => BizCategory::PaymentSimple("payment_reminder"),
609
610 _ => BizCategory::Mixed,
611 }
612}
613
614fn has_renderable_envelope(im: &wa::message::InteractiveMessage) -> bool {
621 let non_empty = |text: Option<&str>| text.is_some_and(|t| !t.is_empty());
622
623 if non_empty(im.body.as_option().and_then(|b| b.text.as_deref()))
624 || non_empty(im.footer.as_option().and_then(|f| f.text.as_deref()))
625 {
626 return true;
627 }
628 let Some(header) = im.header.as_option() else {
629 return false;
630 };
631 non_empty(header.title.as_deref())
632 || matches!(
633 header.media,
634 Some(wa::message::interactive_message::header::Media::ImageMessage(_))
635 )
636}
637
638fn classify_interactive(im: &wa::message::InteractiveMessage) -> Option<BizCategory<'_>> {
649 use wa::message::interactive_message::InteractiveMessage as Payload;
650 match im.interactive_message.as_ref()? {
651 Payload::NativeFlowMessage(nf) if !nf.buttons.is_empty() => {
652 nf.buttons.first()?.name.as_deref().map(classify_button)
653 }
654 Payload::ShopStorefrontMessage(_) => None,
655 _ => has_renderable_envelope(im).then_some(BizCategory::Mixed),
656 }
657}
658
659fn infer_biz_node(msg: &wa::Message, now_unix_secs: u64) -> Option<Node> {
672 let category = classify_interactive(extract_interactive_message(msg)?)?;
673 let privacy_mode_ts = now_unix_secs
674 .saturating_sub(BIZ_PRIVACY_MODE_TS_OFFSET)
675 .to_string();
676
677 Some(match category {
678 BizCategory::PaymentSimple(flow_name) => NodeBuilder::new("biz")
679 .attr("actual_actors", "2")
680 .attr("host_storage", "2")
681 .attr("privacy_mode_ts", &privacy_mode_ts)
682 .attr("native_flow_name", flow_name)
683 .build(),
684 BizCategory::Mixed => build_nested_biz(&privacy_mode_ts, "mixed"),
685 })
686}
687
688fn build_nested_biz(privacy_mode_ts: &str, flow_name: &str) -> Node {
689 NodeBuilder::new("biz")
690 .attr("actual_actors", "2")
691 .attr("host_storage", "2")
692 .attr("privacy_mode_ts", privacy_mode_ts)
693 .children([
694 NodeBuilder::new("interactive")
695 .attr("type", "native_flow")
696 .attr("v", "1")
697 .children([NodeBuilder::new("native_flow")
698 .attr("v", "9")
699 .attr("name", flow_name)
700 .build()])
701 .build(),
702 NodeBuilder::new("quality_control")
703 .attr("source_type", "third_party")
704 .build(),
705 ])
706 .build()
707}
708
709fn extract_interactive_message(msg: &wa::Message) -> Option<&wa::message::InteractiveMessage> {
710 if let Some(doc) = msg.document_with_caption_message.as_option()
713 && let Some(inner) = doc.message.as_option()
714 && let Some(im) = inner.interactive_message.as_option()
715 {
716 return Some(im);
717 }
718 msg.interactive_message.as_option()
719}
720
721fn build_extra_stanza_nodes(
727 to: &Jid,
728 inferred_meta: Option<Node>,
729 biz: Option<Node>,
730 user_nodes: Vec<Node>,
731) -> Vec<Node> {
732 if inferred_meta.is_none() && biz.is_none() {
733 return user_nodes;
734 }
735 let bot_emitted = biz.is_some() && !to.is_group();
736 let extra = inferred_meta.is_some() as usize + biz.is_some() as usize + bot_emitted as usize;
737 let mut nodes = Vec::with_capacity(user_nodes.len() + extra);
738 nodes.extend(inferred_meta);
739 if let Some(node) = biz {
740 if bot_emitted {
741 nodes.push(NodeBuilder::new("bot").attr("biz_bot", "1").build());
742 }
743 nodes.push(node);
744 }
745 nodes.extend(user_nodes);
746 nodes
747}
748
749fn build_revoke_message(
750 remote_jid: &Jid,
751 from_me: bool,
752 message_id: String,
753 participant: Option<String>,
754) -> wa::Message {
755 wa::Message {
756 protocol_message: buffa::MessageField::some(wa::message::ProtocolMessage {
757 key: buffa::MessageField::some(wa::MessageKey {
758 remote_jid: Some(remote_jid.to_string()),
759 from_me: Some(from_me),
760 id: Some(message_id),
761 participant,
762 }),
763 r#type: Some(wa::message::protocol_message::Type::Revoke),
764 ..Default::default()
765 }),
766 ..Default::default()
767 }
768}
769
770pub(crate) enum NewsletterEdit<'a> {
774 Edit(&'a wa::Message),
775 Revoke,
776}
777
778pub(crate) fn build_newsletter_edit_node(
784 to: &Jid,
785 message_id: &str,
786 op: NewsletterEdit<'_>,
787) -> Node {
788 use crate::types::message::EditAttribute;
789 let mut plaintext = NodeBuilder::new("plaintext");
790 let (edit, stanza_type, body) = match op {
791 NewsletterEdit::Edit(m) => {
792 if let Some(mt) = wacore::send::media_type_from_message(m) {
793 plaintext = plaintext.attr("mediatype", mt);
794 }
795 (
796 EditAttribute::AdminEdit,
797 wacore::send::stanza_type_from_message(m),
798 waproto::codec::message_to_vec(m),
799 )
800 }
801 NewsletterEdit::Revoke => (EditAttribute::AdminRevoke, "text", Vec::new()),
802 };
803 NodeBuilder::new("message")
804 .attr("to", to)
805 .attr("id", message_id)
806 .attr("type", stanza_type)
807 .attr("edit", edit.to_string_val())
808 .children([plaintext.bytes(body).build()])
809 .build()
810}
811
812pub(crate) fn build_edit_message(
818 remote_jid: &Jid,
819 message_id: String,
820 participant: Option<String>,
821 new_content: wa::Message,
822 timestamp_ms: i64,
823) -> wa::Message {
824 wa::Message {
825 protocol_message: buffa::MessageField::some(wa::message::ProtocolMessage {
826 key: buffa::MessageField::some(wa::MessageKey {
827 remote_jid: Some(remote_jid.to_string()),
828 from_me: Some(true),
829 id: Some(message_id),
830 participant,
831 }),
832 r#type: Some(wa::message::protocol_message::Type::MessageEdit),
833 edited_message: buffa::MessageField::some(new_content),
834 timestamp_ms: Some(timestamp_ms),
835 ..Default::default()
836 }),
837 ..Default::default()
838 }
839}
840
841impl Client {
842 pub fn send_message(
847 &self,
848 to: impl Into<Jid>,
849 message: wa::Message,
850 ) -> impl Future<Output = Result<SendResult, SendError>> + '_ {
851 let to = to.into();
854 let message = Box::new(message);
855 async move {
856 Box::pin(self.send_message_with_options_inner(to, message, SendOptions::default()))
858 .await
859 }
860 }
861
862 pub fn send_text(
864 &self,
865 to: impl Into<Jid>,
866 text: impl Into<String>,
867 ) -> impl Future<Output = Result<SendResult, SendError>> + '_ {
868 use wacore::proto_helpers::MessageBuilderExt;
869 let to = to.into();
870 let message = Box::new(wa::Message::text(text));
871 async move {
872 Box::pin(self.send_message_with_options_inner(to, message, SendOptions::default()))
873 .await
874 }
875 }
876
877 pub fn forward_message(
888 &self,
889 to: impl Into<Jid>,
890 message: &wa::Message,
891 ) -> impl Future<Output = Result<SendResult, SendError>> + '_ {
892 use wacore::proto_helpers::MessageExt;
893 let to = to.into();
894 let body = message.get_base_message().prepare_for_forward();
895 async move {
896 Box::pin(self.send_message_with_options_inner(to, body, SendOptions::default())).await
897 }
898 }
899
900 pub fn send_message_with_options(
902 &self,
903 to: impl Into<Jid>,
904 message: wa::Message,
905 options: SendOptions,
906 ) -> impl Future<Output = Result<SendResult, SendError>> + '_ {
907 let to = to.into();
911 let message = Box::new(message);
912 async move { Box::pin(self.send_message_with_options_inner(to, message, options)).await }
913 }
914
915 #[cfg_attr(
916 feature = "tracing",
917 tracing::instrument(
918 name = "wa.send.message",
919 level = "debug",
920 skip_all,
921 fields(
922 to = %to.observe(),
923 lid = tracing::field::Empty,
924 pn = tracing::field::Empty
925 ),
926 err(Debug)
927 )
928 )]
929 async fn send_message_with_options_inner(
930 &self,
931 to: Jid,
932 mut message: Box<wa::Message>,
933 options: SendOptions,
934 ) -> Result<SendResult, SendError> {
935 #[cfg(feature = "tracing")]
936 self.record_identity_on_span(&tracing::Span::current());
937
938 validate_extra_stanza_nodes(&options.extra_stanza_nodes)?;
939 if options.message_id.as_ref().is_some_and(String::is_empty) {
940 return Err(SendError::InvalidRequest(
941 "message ID must not be empty".into(),
942 ));
943 }
944
945 let _t = wacore::telemetry::timer(wacore::telemetry::SEND_DURATION);
946 self.stats.record_message_sent();
947 wacore::telemetry::send(match to.server {
948 Server::Group => "group",
949 Server::Broadcast => "status",
950 Server::Newsletter => "newsletter",
951 _ => "dm",
952 });
953 if let Some(exp) = options.ephemeral_expiration
954 && exp > 0
955 {
956 use wacore::proto_helpers::MessageExt;
957 if !message.set_ephemeral_expiration(exp) {
958 log::warn!("Could not set contextInfo.expiration on this message type");
960 }
961 }
962
963 let stanza_type_override = options.stanza_type_override;
964 let group_metadata_freshness = options.group_metadata_freshness;
965 let device_freshness = options.device_freshness;
966 let sent_at = SendInstant::now();
967 let request_id = match options.message_id {
968 Some(id) => id,
969 None => self.generate_message_id_at(sent_at.unix_secs_u64()),
970 };
971 let result_to = to.clone();
975
976 if to.is_newsletter() {
979 let stanza_type = stanza_type_override
980 .map(StanzaType::as_wire)
981 .unwrap_or_else(|| wacore::send::stanza_type_from_message(&message));
982 let (_, meta_node) = infer_stanza_metadata(&message);
983 let mut plaintext_builder = NodeBuilder::new("plaintext");
984 if let Some(mt) = wacore::send::media_type_from_message(&message) {
985 plaintext_builder = plaintext_builder.attr("mediatype", mt);
986 }
987 let mut children = vec![
988 plaintext_builder
989 .bytes(waproto::codec::message_to_vec(&message))
990 .build(),
991 ];
992 children.extend(meta_node);
993 children.extend(options.extra_stanza_nodes);
994 let stanza = NodeBuilder::new("message")
995 .attr("to", to)
996 .attr("type", stanza_type)
997 .attr("id", &request_id)
998 .children(children)
999 .build();
1000 self.send_node(stanza).await?;
1001 return Ok(SendResult {
1002 message_id: request_id,
1003 to: result_to,
1004 });
1005 }
1006
1007 let (edit, inferred_meta) = infer_stanza_metadata(&message);
1008 let biz = infer_biz_node(&message, sent_at.unix_secs_u64());
1009
1010 let extra_nodes =
1011 build_extra_stanza_nodes(&to, inferred_meta, biz, options.extra_stanza_nodes);
1012 self.send_message_impl(
1017 to,
1018 &message,
1019 SendPipelineOptions {
1020 sent_at: Some(sent_at),
1021 request_id: Some(&request_id),
1022 edit,
1023 extra_stanza_nodes: extra_nodes,
1024 stanza_type: stanza_type_override,
1025 group_metadata_freshness,
1026 device_freshness,
1027 ..Default::default()
1028 },
1029 )
1030 .await
1031 .map_err(SendError::from_anyhow)?;
1032 Ok(SendResult {
1033 message_id: request_id,
1034 to: result_to,
1035 })
1036 }
1037
1038 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.send.status", level = "debug", skip_all, fields(count = recipients.len()), err(Debug)))]
1048 pub(crate) async fn send_status_message(
1049 &self,
1050 message: wa::Message,
1051 recipients: &[Jid],
1052 mut options: crate::features::status::StatusSendOptions,
1053 ) -> Result<SendResult, SendError> {
1054 use wacore::client::context::GroupInfo;
1055 use wacore_binary::builder::NodeBuilder;
1056
1057 if recipients.is_empty() {
1058 return Err(SendError::InvalidRequest(
1059 "cannot send status with no recipients".into(),
1060 ));
1061 }
1062 validate_extra_stanza_nodes(&options.extra_stanza_nodes)?;
1063 validate_status_message_id(&message, options.message_id.as_deref())?;
1064
1065 let _t = wacore::telemetry::timer(wacore::telemetry::SEND_DURATION);
1067 self.stats.record_message_sent();
1068 wacore::telemetry::send("status");
1069
1070 let to = Jid::status_broadcast();
1071 let request_id = options
1072 .message_id
1073 .take()
1074 .unwrap_or_else(|| self.generate_message_id());
1075
1076 let device_snapshot = self.persistence_manager.get_device_snapshot();
1078 let account_info = &device_snapshot.account;
1079 let own_jid = device_snapshot.pn.as_ref().ok_or(SendError::NotLoggedIn)?;
1080 let own_lid = device_snapshot.lid.as_ref().ok_or_else(|| {
1084 SendError::InvalidRequest(
1085 "cannot send status: device has no LID yet. Finish pairing / LID \
1086 migration before posting status."
1087 .into(),
1088 )
1089 })?;
1090
1091 for jid in recipients {
1095 if !(jid.is_pn() || jid.is_lid()) {
1096 return Err(SendError::InvalidRequest(format!(
1097 "invalid status recipient {jid}: must be a user JID (PN or LID), \
1098 not a group/broadcast/newsletter/hosted/etc."
1099 )));
1100 }
1101 }
1102
1103 use futures::StreamExt;
1104 use std::collections::HashMap;
1105 const STATUS_LID_RESOLVE_CONCURRENCY: usize = 16;
1109 let resolved_indexed: Vec<(usize, Option<Jid>)> =
1110 futures::stream::iter(0..recipients.len())
1111 .map(|i| async move { (i, self.resolve_recipient_to_lid(&recipients[i]).await) })
1112 .buffer_unordered(STATUS_LID_RESOLVE_CONCURRENCY)
1113 .collect()
1114 .await;
1115 let mut resolved: Vec<Option<Jid>> = vec![None; recipients.len()];
1116 let mut lid_to_pn_map: HashMap<wacore_binary::CompactString, Jid> =
1117 HashMap::with_capacity(recipients.len() + 1);
1118 for (i, lid) in resolved_indexed {
1119 if let Some(lid_jid) = lid {
1120 if recipients[i].is_pn() {
1121 lid_to_pn_map.insert(lid_jid.user.clone(), recipients[i].to_non_ad());
1122 }
1123 resolved[i] = Some(lid_jid);
1124 }
1125 }
1126 lid_to_pn_map.insert(own_lid.user.clone(), own_jid.to_non_ad());
1127
1128 let participants = wacore::send::assemble_status_participants(resolved, own_lid)?;
1129 let mut group_info =
1130 GroupInfo::with_lid_to_pn_map(participants, AddressingMode::Lid, lid_to_pn_map);
1131
1132 let shared_content = message
1134 .message_context_info
1135 .is_unset()
1136 .then(|| std::sync::Arc::new(waproto::codec::message_to_vec(&message)));
1137 self.add_recent_message(&to, &request_id, &message, shared_content.clone())
1138 .await;
1139
1140 let device_store_arc = self.persistence_manager.get_device_arc().await;
1141 let to_str = to.to_string();
1142 let distribution_guard = self.group_distribution_lock(&to).await;
1143
1144 let force_skdm = {
1145 use wacore::libsignal::store::sender_key_name::SenderKeyName;
1146 let sender_address = own_lid.to_protocol_address();
1151 let sender_key_name = SenderKeyName::from_parts(&to_str, sender_address.as_str());
1152
1153 let key_exists = self
1154 .signal_cache
1155 .get_sender_key(&sender_key_name, &*device_snapshot.backend)
1156 .await?
1157 .is_some();
1158
1159 if !key_exists {
1160 self.reset_sender_key_device_tracking(&to_str).await?;
1161 }
1162
1163 !key_exists
1164 };
1165
1166 let mut store_adapter = self.signal_adapter_from(device_store_arc.clone());
1167 let mut stores = store_adapter.as_signal_stores();
1168
1169 let skdm_target_devices =
1173 if !force_skdm || options.device_freshness == crate::cache::Freshness::Refresh {
1174 self.resolve_status_skdm_targets(
1175 &to_str,
1176 &group_info,
1177 own_lid,
1178 options.device_freshness,
1179 force_skdm,
1180 )
1181 .await?
1182 } else {
1183 None
1184 };
1185
1186 let own_status_base = own_lid.to_non_ad();
1190 if !group_info
1191 .participants
1192 .iter()
1193 .any(|participant| participant.is_same_user_as(&own_status_base))
1194 {
1195 group_info.participants.push(own_status_base);
1196 }
1197
1198 let mut extra_stanza_nodes = options.extra_stanza_nodes;
1203 if wacore::send::status_carries_privacy_meta(&message) {
1204 extra_stanza_nodes.push(
1205 NodeBuilder::new("meta")
1206 .attr("status_setting", options.privacy.as_str())
1207 .build(),
1208 );
1209 }
1210
1211 let prepared = match wacore::send::prepare_group_stanza(
1212 &*self.runtime,
1213 &mut stores,
1214 self,
1215 wacore::send::GroupStanzaRequest {
1216 group: &group_info,
1217 own_jid,
1218 own_lid,
1219 account: account_info.as_deref(),
1220 to: &to,
1221 message: &message,
1222 message_id: &request_id,
1223 force_distribution: force_skdm,
1224 distribution_targets: skdm_target_devices,
1225 distribution_policy: wacore::send::SenderKeyDistributionPolicy::BestEffort,
1226 phash_devices: None,
1227 edit: None,
1228 extra_nodes: &extra_stanza_nodes,
1229 pre_encoded: shared_content.as_deref().map(Vec::as_slice),
1230 },
1231 )
1232 .await
1233 {
1234 Ok(prepared) => prepared,
1235 Err(e) => {
1236 if let Some(SignalProtocolError::NoSenderKeyState(_)) =
1237 e.downcast_ref::<SignalProtocolError>()
1238 {
1239 log::warn!("No sender key for status broadcast, forcing distribution.");
1240
1241 self.reset_sender_key_device_tracking(&to_str).await?;
1242
1243 let mut store_adapter_retry =
1244 self.signal_adapter_from(device_store_arc.clone());
1245 let mut stores_retry = store_adapter_retry.as_signal_stores();
1246
1247 wacore::send::prepare_group_stanza(
1248 &*self.runtime,
1249 &mut stores_retry,
1250 self,
1251 wacore::send::GroupStanzaRequest {
1252 group: &group_info,
1253 own_jid,
1254 own_lid,
1255 account: account_info.as_deref(),
1256 to: &to,
1257 message: &message,
1258 message_id: &request_id,
1259 force_distribution: true,
1260 distribution_targets: None,
1261 distribution_policy:
1262 wacore::send::SenderKeyDistributionPolicy::BestEffort,
1263 phash_devices: None,
1264 edit: None,
1265 extra_nodes: &extra_stanza_nodes,
1266 pre_encoded: shared_content.as_deref().map(Vec::as_slice),
1267 },
1268 )
1269 .await?
1270 } else {
1271 return Err(e.into());
1272 }
1273 }
1274 };
1275
1276 let stanza = self
1277 .ensure_status_participants(prepared.node, &group_info)
1278 .await?;
1279
1280 self.persist_signal_state_pre_wire().await?;
1283
1284 let ack = stanza
1285 .attrs()
1286 .optional_string("phash")
1287 .map(|s| wacore_binary::CompactString::from(s.as_ref()));
1288 if let Some(phash) = ack.clone() {
1289 self.register_phash_waiter(&request_id, phash, to.clone(), true);
1290 }
1291
1292 if let Err(e) = self.send_node(stanza).await {
1293 if ack.is_some() {
1294 self.response_waiters_guard().remove(&request_id);
1295 }
1296 return Err(e.into());
1297 }
1298
1299 self.update_sender_key_devices(&to_str, &prepared.skdm_devices)
1300 .await;
1301 drop(distribution_guard);
1302
1303 for user in &prepared.stale_device_users {
1304 self.invalidate_device_cache(user).await;
1305 }
1306
1307 Ok(SendResult {
1308 message_id: request_id,
1309 to,
1310 })
1311 }
1312
1313 async fn skdm_device_map(
1329 &self,
1330 group_jid: &str,
1331 ) -> std::sync::Arc<crate::sender_key_device_cache::SenderKeyDeviceMap> {
1332 use crate::sender_key_device_cache::SenderKeyDeviceMap;
1333 let pm = self.persistence_manager.clone();
1334 self.sender_key_device_cache
1335 .get_or_init(group_jid, async {
1336 let db_rows = pm
1337 .get_sender_key_devices(group_jid)
1338 .await
1339 .unwrap_or_else(|e| {
1340 log::warn!(
1341 "Failed to read sender key devices for {}: {:?}",
1342 group_jid,
1343 e
1344 );
1345 vec![]
1346 });
1347 std::sync::Arc::new(SenderKeyDeviceMap::from_db_rows(&db_rows))
1348 })
1349 .await
1350 }
1351
1352 fn filter_skdm_targets(
1357 &self,
1358 group_jid: &str,
1359 all_devices: &[Jid],
1360 cached_map: &crate::sender_key_device_cache::SenderKeyDeviceMap,
1361 own_sending_jid: &Jid,
1362 ) -> Vec<Jid> {
1363 let needs_skdm: Vec<Jid> = all_devices
1364 .iter()
1365 .filter(|device| {
1366 if device.is_hosted() {
1367 return false;
1368 }
1369 if device.user == own_sending_jid.user && device.device == own_sending_jid.device {
1370 return false;
1371 }
1372 !cached_map.device_and_primary_warm(&device.user, device.device)
1378 })
1379 .cloned()
1380 .collect();
1381
1382 log::debug!(
1383 "Resolved {} devices ({} need SKDM) for {}",
1384 all_devices.len(),
1385 needs_skdm.len(),
1386 group_jid
1387 );
1388 needs_skdm
1389 }
1390
1391 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.send.resolve_skdm_targets", level = "debug", skip_all, fields(group = %wacore_binary::jid::observe_str(group_jid))))]
1394 async fn resolve_status_skdm_targets(
1395 &self,
1396 group_jid: &str,
1397 group_info: &wacore::client::context::GroupInfo,
1398 own_sending_jid: &Jid,
1399 freshness: crate::cache::Freshness,
1400 force_distribution: bool,
1401 ) -> Result<Option<Vec<Jid>>, anyhow::Error> {
1402 let cached_map = if force_distribution {
1403 None
1404 } else {
1405 Some(self.skdm_device_map(group_jid).await)
1406 };
1407
1408 let is_lid_mode = group_info.addressing_mode == AddressingMode::Lid;
1409 let jids_to_resolve: Vec<Jid> = group_info
1410 .participants
1411 .iter()
1412 .map(|jid| {
1413 if is_lid_mode
1414 && jid.is_lid()
1415 && let Some(pn) = group_info.phone_jid_for_lid_user(&jid.user)
1416 {
1417 return pn.to_non_ad();
1418 }
1419 jid.to_non_ad()
1420 })
1421 .collect();
1422
1423 let resolved = match freshness {
1424 crate::cache::Freshness::CachePreferred => {
1425 self.get_user_devices_owned(jids_to_resolve).await
1426 }
1427 crate::cache::Freshness::Refresh => self.refresh_user_devices(jids_to_resolve).await,
1428 };
1429 match resolved {
1430 Ok(mut devices) => {
1431 if is_lid_mode {
1432 for device in &mut devices {
1433 *device = group_info.phone_device_jid_into_lid(std::mem::take(device));
1434 }
1435 }
1436 if force_distribution {
1437 wacore::send::retain_skdm_distribution_targets(&mut devices, own_sending_jid);
1438 } else if let Some(cached_map) = cached_map {
1439 devices.retain(|device| {
1440 !device.is_hosted()
1441 && !(device.user == own_sending_jid.user
1442 && device.device == own_sending_jid.device)
1443 && !cached_map.device_and_primary_warm(&device.user, device.device)
1444 });
1445 }
1446 log::debug!(
1447 "Resolved {} status devices needing SKDM for {}",
1448 devices.len(),
1449 group_jid
1450 );
1451 Ok(Some(devices))
1452 }
1453 Err(error) if freshness == crate::cache::Freshness::CachePreferred => {
1454 log::warn!(
1455 "Failed to resolve devices for SKDM check in {}: {:?}",
1456 group_jid,
1457 error
1458 );
1459 Ok(None)
1460 }
1461 Err(error) => Err(error),
1462 }
1463 }
1464
1465 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.send.resolve_skdm_targets_memoized", level = "debug", skip_all, fields(group = %group_jid)))]
1469 async fn resolve_skdm_targets_memoized(
1470 &self,
1471 group: &Jid,
1472 group_jid: &str,
1473 group_info: &std::sync::Arc<wacore::client::context::GroupInfo>,
1474 own_sending_jid: &Jid,
1475 ) -> Option<(std::sync::Arc<wacore::send::ResolvedGroupDevices>, Vec<Jid>)> {
1476 let cached_map = self.skdm_device_map(group_jid).await;
1477 match self
1478 .resolve_group_devices_memoized(group, group_info, own_sending_jid)
1479 .await
1480 {
1481 Ok(all_devices) => {
1482 let cached_map_gen = cached_map.generation();
1490 if self.device_memos_enabled
1500 && let Some((dw, cw, memo_gen, memo_sender, memo_needs)) =
1501 self.skdm_warm_memo.get(group).await
1502 && std::ptr::eq(dw.as_ptr(), std::sync::Arc::as_ptr(&all_devices))
1503 && std::ptr::eq(cw.as_ptr(), std::sync::Arc::as_ptr(&cached_map))
1504 && memo_gen == cached_map_gen
1505 && &memo_sender == own_sending_jid
1506 {
1507 return Some((all_devices, memo_needs));
1508 }
1509 let needs_skdm = self.filter_skdm_targets(
1510 group_jid,
1511 all_devices.devices(),
1512 &cached_map,
1513 own_sending_jid,
1514 );
1515 if self.device_memos_enabled
1516 && (needs_skdm.is_empty() || {
1517 let snapshot = self.persistence_manager.get_device_snapshot();
1518 skdm_needs_only_own_devices(
1519 &needs_skdm,
1520 snapshot.pn.as_ref(),
1521 snapshot.lid.as_ref(),
1522 )
1523 })
1524 {
1525 self.skdm_warm_memo
1526 .insert(
1527 group.clone(),
1528 (
1529 std::sync::Arc::downgrade(&all_devices),
1530 std::sync::Arc::downgrade(&cached_map),
1531 cached_map_gen,
1532 own_sending_jid.clone(),
1533 needs_skdm.clone(),
1534 ),
1535 )
1536 .await;
1537 }
1538 Some((all_devices, needs_skdm))
1539 }
1540 Err(e) => {
1541 log::warn!(
1542 "Failed to resolve devices for SKDM check in {}: {:?}",
1543 group_jid,
1544 e
1545 );
1546 None
1547 }
1548 }
1549 }
1550
1551 pub(crate) async fn update_sender_key_devices(&self, group_jid: &str, devices: &[Jid]) {
1571 if devices.is_empty() {
1572 return;
1573 }
1574
1575 if let Err(e) = self
1580 .set_sender_key_status_for_devices(group_jid, devices, true, true)
1581 .await
1582 {
1583 log::warn!(
1584 "Failed to update sender key devices for {}: {:?}",
1585 group_jid,
1586 e
1587 );
1588 self.sender_key_device_cache.invalidate(group_jid).await;
1592 }
1593 }
1594
1595 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.send.phash_mismatch", level = "debug", skip_all, fields(jid = %jid.observe())))]
1600 pub(crate) async fn handle_phash_mismatch(
1601 &self,
1602 jid: &Jid,
1603 our_phash: &str,
1604 server_phash: &str,
1605 invalidate_group_cache: bool,
1606 ) {
1607 log::warn!(
1608 "Phash mismatch for {}: ours={our_phash}, server={server_phash}. Invalidating caches.",
1609 jid.observe()
1610 );
1611 if !jid.is_group() && !jid.is_status_broadcast() {
1614 self.invalidate_device_cache(&jid.user).await;
1615 if let Some(own_pn) = &self.persistence_manager.get_device_snapshot().pn {
1616 self.invalidate_device_cache(&own_pn.user).await;
1617 }
1618 }
1619 let jid_str = jid.to_string();
1620 let mut flush_fallback = false;
1626 if jid.is_group() || jid.is_status_broadcast() {
1627 let distribution_guard = self.group_distribution_lock(jid).await;
1628 if let Err(e) = self.reset_sender_key_device_tracking(&jid_str).await {
1629 log::warn!(
1630 "phash mismatch: clear_sender_key_devices failed: {e} — \
1631 deleting own sender key as fallback to force redistribution"
1632 );
1633 use wacore::libsignal::store::sender_key_name::SenderKeyName;
1634 use wacore::types::jid::JidExt;
1635 let snapshot = self.persistence_manager.get_device_snapshot();
1636 for own in snapshot.lid.iter().chain(snapshot.pn.iter()) {
1637 let sk =
1638 SenderKeyName::from_parts(&jid_str, own.to_protocol_address().as_str());
1639 self.signal_cache.delete_sender_key(sk.cache_key()).await;
1640 }
1641 flush_fallback = true;
1642 }
1643 drop(distribution_guard);
1644 } else {
1645 self.sender_key_device_cache.invalidate(&jid_str).await;
1646 }
1647 if flush_fallback {
1648 let _ = self
1649 .flush_signal_cache_batch_safe_logged("phash-mismatch-fallback", None)
1650 .await;
1651 }
1652 if invalidate_group_cache {
1653 self.lock_group_metadata(jid).await.invalidate().await;
1654 }
1655 }
1656
1657 async fn ensure_status_participants(
1663 &self,
1664 stanza: Node,
1665 group_info: &wacore::client::context::GroupInfo,
1666 ) -> Result<Node, anyhow::Error> {
1667 Ok(wacore::send::ensure_status_participants(stanza, group_info))
1668 }
1669
1670 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.send.impl", level = "debug", skip_all, fields(to = %to.observe()), err(Debug)))]
1671 pub(crate) async fn send_message_impl(
1672 &self,
1673 to: Jid,
1674 message: &wa::Message,
1675 options: SendPipelineOptions<'_>,
1676 ) -> Result<(), anyhow::Error> {
1677 let SendPipelineOptions {
1678 sent_at,
1679 request_id: request_id_override,
1680 peer,
1681 force_key_distribution,
1682 edit,
1683 extra_stanza_nodes,
1684 stanza_type: stanza_type_override,
1685 group_metadata_freshness,
1686 device_freshness,
1687 borrowed_message_id,
1688 } = options;
1689 let sent_at = sent_at.unwrap_or_else(SendInstant::now);
1692 validate_extra_stanza_nodes(&extra_stanza_nodes)?;
1693 if request_id_override.is_some_and(str::is_empty) {
1694 return Err(SendError::InvalidRequest("message ID must not be empty".into()).into());
1695 }
1696 if to.is_newsletter() {
1702 return Err(SendError::InvalidRequest(
1703 "newsletter JIDs are not valid on the E2E send path; use \
1704 newsletter().edit_message/revoke_message (pin is unsupported on channels)"
1705 .into(),
1706 )
1707 .into());
1708 }
1709
1710 let (to, is_status_addon) = if to.is_status_broadcast() {
1713 let author = message
1714 .reaction_message
1715 .as_option()
1716 .and_then(|rm| rm.key.as_option())
1717 .and_then(|k| k.participant.as_ref())
1718 .and_then(|p| p.parse::<Jid>().ok())
1719 .filter(|jid| jid.is_pn() || jid.is_lid())
1720 .ok_or_else(|| {
1721 SendError::InvalidRequest(
1722 "send_message to status@broadcast requires \
1723 reaction_message.key.participant = status author (user JID). \
1724 Use client.status() for posting new statuses."
1725 .into(),
1726 )
1727 })?;
1728 (author, true)
1729 } else {
1730 (to, false)
1731 };
1732
1733 let generated_request_id;
1738 let request_id: &str = match request_id_override {
1739 Some(id) => id,
1740 None => {
1741 generated_request_id = self.generate_message_id_at(sent_at.unix_secs_u64());
1742 &generated_request_id
1743 }
1744 };
1745 let tc_issue_target = to.clone();
1746
1747 let SendBranchOutput {
1752 node: stanza_to_send,
1753 msg_secret: outbound_msg_secret,
1754 group_sender_identity: outbound_group_sender_identity,
1755 skdm_update,
1756 distribution_guard,
1757 issue_tc_token_after_send: should_issue_tc_token_after_send,
1758 dm_phash,
1759 } = if peer && !to.is_group() {
1760 box_send_branch(self.send_peer_branch(to, message, request_id)).await?
1761 } else if to.is_group() {
1762 box_send_branch(self.send_group_branch(GroupBranchRequest {
1763 to,
1764 message,
1765 request_id,
1766 force_key_distribution,
1767 edit,
1768 extra_stanza_nodes: &extra_stanza_nodes,
1769 group_metadata_freshness,
1770 device_freshness,
1771 borrowed_message_id,
1772 }))
1773 .await?
1774 } else {
1775 box_send_branch(self.send_dm_branch(DmBranchRequest {
1776 to,
1777 message,
1778 request_id,
1779 sent_at,
1780 edit,
1781 extra_stanza_nodes,
1782 is_status_addon,
1783 device_freshness,
1784 borrowed_message_id,
1785 }))
1786 .await?
1787 };
1788
1789 self.persist_signal_state_pre_wire().await?;
1797
1798 debug_assert_eq!(
1809 stanza_to_send.attrs().optional_string("id").as_deref(),
1810 Some(request_id),
1811 "branch stanza must carry the id this send was named with"
1812 );
1813 let ack_message_id = if !borrowed_message_id && let Some(phash) = dm_phash {
1814 let invalidate_group = tc_issue_target.is_group();
1817 self.register_phash_waiter(
1818 request_id,
1819 phash,
1820 tc_issue_target.clone(),
1821 invalidate_group,
1822 );
1823 Some(request_id)
1824 } else {
1825 None
1826 };
1827
1828 let mut stanza_to_send = stanza_to_send;
1831 if is_status_addon {
1832 stanza_to_send.attrs.insert("to", Jid::status_broadcast());
1833 }
1834 if let Some(t) = stanza_type_override {
1835 stanza_to_send.attrs.insert("type", t.as_wire());
1836 }
1837
1838 if let Err(e) = self.send_node(stanza_to_send).await {
1839 if let Some(msg_id) = ack_message_id {
1840 self.response_waiters_guard().remove(msg_id);
1841 }
1842 return Err(e.into());
1843 }
1844 if !borrowed_message_id && let Some(secret) = outbound_msg_secret.as_ref() {
1848 let sender = match outbound_group_sender_identity {
1849 Some(s) => Some(s),
1850 None => self.dm_sender_identity_for(&tc_issue_target).await,
1851 };
1852 if let Some(sender) = sender {
1853 let is_bot_chat = tc_issue_target.is_bot();
1854 let class = wacore::msg_secret::classify(message, is_bot_chat);
1855 self.persist_outbound_msg_secret(
1856 &tc_issue_target,
1857 &sender,
1858 request_id,
1859 secret,
1860 class,
1861 sent_at,
1862 )
1863 .await;
1864 }
1865 }
1866
1867 if let Some(update) = skdm_update {
1868 self.update_sender_key_devices(&update.to_str, &update.devices)
1869 .await;
1870 for user in &update.stale_users {
1871 self.invalidate_device_cache(user).await;
1872 }
1873 }
1874 drop(distribution_guard);
1876
1877 if should_issue_tc_token_after_send {
1880 if let Some(client) = self.self_weak.get().and_then(|w| w.upgrade()) {
1881 let target = tc_issue_target;
1882 self.runtime
1883 .spawn(Box::pin(async move {
1884 client.issue_tc_token_after_send(&target).await;
1885 }))
1886 .detach();
1887 } else {
1888 log::debug!(target: "Client/TcToken", "Skipping fire-and-forget issuance: client dropped");
1889 }
1890 }
1891
1892 Ok(())
1893 }
1894
1895 async fn send_peer_branch(
1898 &self,
1899 to: Jid,
1900 message: &wa::Message,
1901 request_id: &str,
1902 ) -> Result<SendBranchOutput, anyhow::Error> {
1903 let node = {
1904 let encryption_jid = self.resolve_encryption_jid(&to).await;
1907 let signal_addr = encryption_jid.to_protocol_address();
1908
1909 let session_mutex = self.session_lock_for(signal_addr.as_str()).await;
1910 let _session_guard = session_mutex.lock().await;
1911
1912 let mut store_adapter = self.signal_adapter().await;
1913
1914 let device_snapshot = self.persistence_manager.get_device_snapshot();
1915 wacore::send::prepare_peer_stanza(
1916 &mut store_adapter.session_store,
1917 &mut store_adapter.identity_store,
1918 to,
1919 &signal_addr,
1920 message,
1921 request_id,
1922 device_snapshot.account.as_deref(),
1923 )
1924 .await?
1925 };
1926 Ok(SendBranchOutput::stanza_only(node))
1927 }
1928
1929 async fn send_group_branch(
1932 &self,
1933 request: GroupBranchRequest<'_>,
1934 ) -> Result<SendBranchOutput, anyhow::Error> {
1935 let GroupBranchRequest {
1936 to,
1937 message,
1938 request_id,
1939 force_key_distribution,
1940 edit,
1941 extra_stanza_nodes,
1942 group_metadata_freshness,
1943 device_freshness,
1944 borrowed_message_id,
1945 } = request;
1946 let outbound_msg_secret: Option<[u8; 32]>;
1948 let outbound_group_sender_identity: Option<Jid>;
1949 let skdm_update: Option<SkdmUpdate>;
1950 let mut distribution_guard: Option<async_lock::MutexGuardArc<()>> = None;
1951 let node = {
1952 let group_info = self
1955 .groups()
1956 .query_info_with_freshness(&to, group_metadata_freshness)
1957 .await?;
1958
1959 let device_snapshot = self.persistence_manager.get_device_snapshot();
1961 let account_info = &device_snapshot.account;
1962 let own_jid = device_snapshot
1963 .pn
1964 .as_ref()
1965 .ok_or(ClientError::NotLoggedIn)?;
1966 let own_lid = device_snapshot
1967 .lid
1968 .as_ref()
1969 .ok_or_else(|| anyhow!("LID not set, cannot send to group"))?;
1970
1971 let shared_content = message
1973 .message_context_info
1974 .is_unset()
1975 .then(|| std::sync::Arc::new(waproto::codec::message_to_vec(message)));
1976 if !borrowed_message_id {
1980 self.add_recent_message(&to, request_id, message, shared_content.clone())
1981 .await;
1982 }
1983
1984 let device_store_arc = self.persistence_manager.get_device_arc().await;
1985 let to_str = to.to_string();
1986
1987 let (own_sending_jid, _) = match group_info.addressing_mode {
1988 AddressingMode::Lid => (own_lid.clone(), "lid"),
1989 AddressingMode::Pn => (own_jid.clone(), "pn"),
1990 };
1991
1992 let group_info_for_memo = std::sync::Arc::clone(&group_info);
1997 let refreshed_devices = if device_freshness == crate::cache::Freshness::Refresh {
1998 Some(
1999 self.resolve_group_devices_uncached(
2000 &group_info_for_memo,
2001 &own_sending_jid,
2002 crate::cache::Freshness::Refresh,
2003 )
2004 .await?,
2005 )
2006 } else {
2007 None
2008 };
2009 let group_info = ensure_self_in_group(group_info, &own_sending_jid);
2012
2013 use wacore::libsignal::store::sender_key_name::SenderKeyName;
2019 let sender_address = own_sending_jid.to_protocol_address();
2020 let sender_key_name = SenderKeyName::from_parts(&to_str, sender_address.as_str());
2021 const SENDER_KEY_ROTATION_THRESHOLD: u32 = 1000;
2025 let read_sender_key_state = || async {
2026 let record = self
2027 .signal_cache
2028 .get_sender_key(&sender_key_name, &*device_snapshot.backend)
2029 .await?;
2030 let key_exists = record.is_some();
2031 let needs_rotation = record
2034 .as_ref()
2035 .and_then(|r| r.sender_key_state().ok())
2036 .and_then(|state| state.sender_chain_key())
2037 .map(|ck| ck.iteration())
2038 .is_some_and(|iter| iter >= SENDER_KEY_ROTATION_THRESHOLD);
2039 Ok::<(bool, bool), anyhow::Error>((key_exists, needs_rotation))
2040 };
2041
2042 let (key_exists, needs_rotation) = read_sender_key_state().await?;
2043 let mut force_skdm = force_key_distribution || !key_exists || needs_rotation;
2044 if force_skdm {
2045 distribution_guard = Some(self.group_distribution_lock(&to).await);
2051 let (key_exists, needs_rotation) = read_sender_key_state().await?;
2052 force_skdm = force_key_distribution || !key_exists || needs_rotation;
2053 if !key_exists || needs_rotation {
2054 self.reset_sender_key_device_tracking(&to_str).await?;
2055 }
2056 if needs_rotation {
2057 log::info!(
2058 "Periodic sender-key rotation for {} (chain iteration >= {SENDER_KEY_ROTATION_THRESHOLD})",
2059 to.observe()
2060 );
2061 self.signal_cache
2062 .delete_sender_key(sender_key_name.cache_key())
2063 .await;
2064 }
2065 if !force_skdm {
2066 distribution_guard = None;
2067 }
2068 }
2069
2070 let mut store_adapter = self.signal_adapter_from(device_store_arc.clone());
2071
2072 let mut stores = store_adapter.as_signal_stores();
2073
2074 let (all_devices_for_phash, skdm_target_devices): (
2082 Option<GroupDeviceSnapshot>,
2083 Option<Vec<Jid>>,
2084 ) = if force_skdm {
2085 match refreshed_devices {
2086 Some(mut targets) => {
2087 wacore::send::retain_skdm_distribution_targets(
2088 &mut targets,
2089 &own_sending_jid,
2090 );
2091 (None, Some(targets))
2092 }
2093 None => (None, None),
2094 }
2095 } else {
2096 let initial_targets = match refreshed_devices {
2097 Some(all) => {
2098 let all = GroupDeviceSnapshot::Owned(
2099 wacore::send::ResolvedGroupDevices::new(all),
2100 );
2101 let cached_map = self.skdm_device_map(&to_str).await;
2102 let needs = self.filter_skdm_targets(
2103 &to_str,
2104 all.as_ref().devices(),
2105 &cached_map,
2106 &own_sending_jid,
2107 );
2108 Some((all, needs))
2109 }
2110 None => self
2111 .resolve_skdm_targets_memoized(
2112 &to,
2113 &to_str,
2114 &group_info_for_memo,
2115 &own_sending_jid,
2116 )
2117 .await
2118 .map(|(all, needs)| (GroupDeviceSnapshot::Shared(all), needs)),
2119 };
2120 match initial_targets {
2121 Some((all, needs)) if needs.is_empty() => (Some(all), Some(needs)),
2122 Some((all, needs))
2127 if skdm_needs_only_own_devices(&needs, Some(own_jid), Some(own_lid)) =>
2128 {
2129 (Some(all), Some(needs))
2130 }
2131 Some((first_all, first_needs)) => {
2132 distribution_guard = Some(self.group_distribution_lock(&to).await);
2138 self.sender_key_device_cache.invalidate(&to_str).await;
2144 match self
2145 .resolve_skdm_targets_memoized(
2146 &to,
2147 &to_str,
2148 &group_info_for_memo,
2149 &own_sending_jid,
2150 )
2151 .await
2152 {
2153 Some((all, needs)) => {
2154 if needs.is_empty()
2158 || skdm_needs_only_own_devices(
2159 &needs,
2160 Some(own_jid),
2161 Some(own_lid),
2162 )
2163 {
2164 distribution_guard = None;
2165 }
2166 (Some(GroupDeviceSnapshot::Shared(all)), Some(needs))
2167 }
2168 None => (Some(first_all), Some(first_needs)),
2173 }
2174 }
2175 None => (None, None),
2176 }
2177 };
2178
2179 match wacore::send::prepare_group_stanza(
2180 &*self.runtime,
2181 &mut stores,
2182 self,
2183 wacore::send::GroupStanzaRequest {
2184 group: &group_info,
2185 own_jid,
2186 own_lid,
2187 account: account_info.as_deref(),
2188 to: &to,
2189 message,
2190 message_id: request_id,
2191 force_distribution: force_skdm,
2192 distribution_targets: skdm_target_devices,
2193 distribution_policy: wacore::send::SenderKeyDistributionPolicy::BestEffort,
2194 phash_devices: all_devices_for_phash.as_ref().map(AsRef::as_ref),
2195 edit: edit.as_ref(),
2196 extra_nodes: extra_stanza_nodes,
2197 pre_encoded: shared_content.as_deref().map(Vec::as_slice),
2198 },
2199 )
2200 .await
2201 {
2202 Ok(prepared) => {
2203 skdm_update = Some(SkdmUpdate {
2204 to_str: to_str.clone(),
2205 devices: prepared.skdm_devices,
2206 stale_users: prepared.stale_device_users,
2207 });
2208 outbound_msg_secret = prepared.message_secret;
2209 outbound_group_sender_identity = Some(prepared.sender_identity);
2210 prepared.node
2211 }
2212 Err(e) => {
2213 if let Some(SignalProtocolError::NoSenderKeyState(_)) =
2214 e.downcast_ref::<SignalProtocolError>()
2215 {
2216 log::warn!(
2217 "No sender key for group {}, forcing distribution.",
2218 to.observe()
2219 );
2220
2221 if distribution_guard.is_none() {
2225 distribution_guard = Some(self.group_distribution_lock(&to).await);
2226 }
2227
2228 let (key_recreated, _) = read_sender_key_state().await?;
2233 let warm_targets = if key_recreated {
2234 self.sender_key_device_cache.invalidate(&to_str).await;
2235 self.resolve_skdm_targets_memoized(
2236 &to,
2237 &to_str,
2238 &group_info_for_memo,
2239 &own_sending_jid,
2240 )
2241 .await
2242 } else {
2243 None
2244 };
2245 let (retry_force, retry_targets, retry_all) = match warm_targets {
2246 Some((all, needs)) => {
2247 (false, Some(needs), Some(GroupDeviceSnapshot::Shared(all)))
2248 }
2249 None => {
2250 self.reset_sender_key_device_tracking(&to_str).await?;
2251 (true, None, None)
2252 }
2253 };
2254
2255 let mut store_adapter_retry =
2256 self.signal_adapter_from(device_store_arc.clone());
2257 let mut stores_retry = store_adapter_retry.as_signal_stores();
2258
2259 let retry_prepared = wacore::send::prepare_group_stanza(
2260 &*self.runtime,
2261 &mut stores_retry,
2262 self,
2263 wacore::send::GroupStanzaRequest {
2264 group: &group_info,
2265 own_jid,
2266 own_lid,
2267 account: account_info.as_deref(),
2268 to: &to,
2269 message,
2270 message_id: request_id,
2271 force_distribution: retry_force,
2272 distribution_targets: retry_targets,
2273 distribution_policy:
2274 wacore::send::SenderKeyDistributionPolicy::BestEffort,
2275 phash_devices: retry_all.as_ref().map(AsRef::as_ref),
2276 edit: edit.as_ref(),
2277 extra_nodes: extra_stanza_nodes,
2278 pre_encoded: shared_content.as_deref().map(Vec::as_slice),
2279 },
2280 )
2281 .await?;
2282
2283 skdm_update = Some(SkdmUpdate {
2284 to_str,
2285 devices: retry_prepared.skdm_devices,
2286 stale_users: retry_prepared.stale_device_users,
2287 });
2288 outbound_msg_secret = retry_prepared.message_secret;
2289 outbound_group_sender_identity = Some(retry_prepared.sender_identity);
2290 retry_prepared.node
2291 } else {
2292 return Err(e);
2293 }
2294 }
2295 }
2296 };
2297 Ok(SendBranchOutput {
2298 node,
2299 msg_secret: outbound_msg_secret,
2300 group_sender_identity: outbound_group_sender_identity,
2301 skdm_update,
2302 distribution_guard,
2303 issue_tc_token_after_send: false,
2304 dm_phash: None,
2305 })
2306 }
2307
2308 async fn send_dm_branch(
2311 &self,
2312 request: DmBranchRequest<'_>,
2313 ) -> Result<SendBranchOutput, anyhow::Error> {
2314 let DmBranchRequest {
2315 to,
2316 message,
2317 request_id,
2318 sent_at,
2319 edit,
2320 extra_stanza_nodes,
2321 is_status_addon,
2322 device_freshness,
2323 borrowed_message_id,
2324 } = request;
2325 let mut should_issue_tc_token_after_send = false;
2326 let prepared = {
2327 let shared_content = message
2332 .message_context_info
2333 .is_unset()
2334 .then(|| std::sync::Arc::new(waproto::codec::message_to_vec(message)));
2335 if !borrowed_message_id {
2340 if is_status_addon {
2341 self.add_recent_message(
2342 &Jid::status_broadcast(),
2343 request_id,
2344 message,
2345 shared_content.clone(),
2346 )
2347 .await;
2348 } else {
2349 self.add_recent_message(&to, request_id, message, shared_content.clone())
2350 .await;
2351 }
2352 }
2353
2354 let device_snapshot = self.persistence_manager.get_device_snapshot();
2355 let own_jid = device_snapshot
2356 .pn
2357 .as_ref()
2358 .ok_or(ClientError::NotLoggedIn)?;
2359
2360 if to.is_pn() && self.lid_pn_cache.get_current_lid(&to.user).await.is_none() {
2362 let sid = self.generate_request_id();
2363 let spec = wacore::iq::usync::LidQuerySpec::new(vec![to.to_non_ad()], sid);
2364 match self.execute(spec).await {
2366 Ok(resp) => {
2367 for mapping in &resp.lid_mappings {
2368 if let Err(e) = self
2369 .add_lid_pn_mapping(
2370 &mapping.lid,
2371 &mapping.phone_number,
2372 crate::lid_pn_cache::LearningSource::Usync,
2373 )
2374 .await
2375 {
2376 log::warn!(
2377 "Failed to persist LID mapping {} -> {}: {e:?}",
2378 mapping.phone_number,
2379 mapping.lid
2380 );
2381 }
2382 }
2383 }
2384 Err(e) => {
2385 log::warn!(
2386 "LID query failed for {}, falling back to PN: {e:?}",
2387 to.observe()
2388 );
2389 }
2390 }
2391 }
2392
2393 let recipient_bare = self.resolve_dm_wire_jid(&to).await;
2397
2398 let stanza_to = dm_stanza_to(&recipient_bare, &to);
2399
2400 let dm_devices = self
2404 .resolve_dm_devices_memoized(
2405 &to,
2406 &recipient_bare,
2407 own_jid,
2408 device_snapshot.lid.as_ref(),
2409 device_freshness,
2410 )
2411 .await?;
2412
2413 self.ensure_e2e_sessions(dm_devices.devices()).await?;
2414
2415 let mut extra_stanza_nodes = extra_stanza_nodes;
2416 if !to.is_group() && !to.is_newsletter() && !is_status_addon {
2419 should_issue_tc_token_after_send = self
2420 .maybe_include_tc_token(&to, &mut extra_stanza_nodes, sent_at)
2421 .await;
2422 }
2423 if should_issue_tc_token_after_send {
2424 debug!(target: "Client/TcToken", "Scheduled tc token issuance after send for {}", to.observe());
2425 }
2426
2427 let lock_jids = self.build_session_lock_keys(dm_devices.devices()).await;
2428 let _session_guards = self.session_guards_for(&lock_jids).await;
2429
2430 let mut store_adapter = self.signal_adapter().await;
2431
2432 let mut stores = store_adapter.as_signal_stores();
2433
2434 wacore::send::prepare_dm_stanza(
2435 &*self.runtime,
2436 &mut stores,
2437 self,
2438 wacore::send::DmStanzaRequest {
2439 own_jid,
2440 account: device_snapshot.account.as_deref(),
2441 to: &stanza_to,
2442 message,
2443 message_id: request_id,
2444 edit: edit.as_ref(),
2445 extra_nodes: &extra_stanza_nodes,
2446 devices: &dm_devices,
2447 pre_encoded: shared_content.as_deref().map(Vec::as_slice),
2448 },
2449 )
2450 .await?
2451 };
2452 Ok(SendBranchOutput {
2453 node: prepared.node,
2454 msg_secret: prepared.message_secret,
2455 group_sender_identity: None,
2456 skdm_update: None,
2457 distribution_guard: None,
2458 issue_tc_token_after_send: should_issue_tc_token_after_send,
2459 dm_phash: prepared.phash,
2460 })
2461 }
2462
2463 pub(crate) async fn persist_outbound_msg_secret(
2468 &self,
2469 chat: &Jid,
2470 sender: &Jid,
2471 msg_id: &str,
2472 secret: &[u8; wacore::reporting_token::MESSAGE_SECRET_SIZE],
2473 class: wacore::msg_secret::RetentionClass,
2474 sent_at: SendInstant,
2475 ) {
2476 let policy = self.cache_config.msg_secret_policy;
2477 if !policy.persists() {
2478 return;
2479 }
2480 if policy.bot_only() && class != wacore::msg_secret::RetentionClass::Bot {
2483 return;
2484 }
2485 let now = sent_at.unix_secs();
2488 let expires_at = wacore::msg_secret::expires_at(
2489 policy,
2490 &self.cache_config.msg_secret_retention,
2491 class,
2492 u64::try_from(now).ok(),
2493 now,
2494 );
2495 let entry = wacore::store::traits::MsgSecretEntry::new(
2496 chat, sender, msg_id, *secret, expires_at, now,
2497 );
2498 self.msg_secret_buffer.queue_one(entry).await;
2501 }
2502
2503 pub(crate) async fn dm_sender_identity_for(&self, to: &Jid) -> Option<Jid> {
2507 if to.server == Server::Bot {
2508 self.lid()
2509 } else {
2510 self.pn()
2511 }
2512 }
2513
2514 pub(crate) async fn build_session_lock_keys(&self, device_jids: &[Jid]) -> Vec<Jid> {
2519 let mut keys: Vec<Jid> = Vec::with_capacity(device_jids.len());
2520 for jid in device_jids {
2521 keys.push(self.resolve_encryption_jid(jid).await);
2522 }
2523 keys.sort_unstable_by(wacore::types::jid::cmp_for_lock_order);
2524 keys.dedup_by(|a, b| wacore::types::jid::cmp_for_lock_order(a, b).is_eq());
2525 keys
2526 }
2527
2528 pub(crate) async fn session_guards_for(
2540 &self,
2541 jids: &[Jid],
2542 ) -> Vec<async_lock::MutexGuardArc<()>> {
2543 debug_assert!(
2549 jids.windows(2).all(|pair| pair[0] != pair[1]),
2550 "session lock keys must be deduped before acquisition, or the loop deadlocks on itself"
2551 );
2552
2553 let mut guards = Vec::with_capacity(jids.len());
2554 let mut addr = wacore::types::jid::make_reusable_protocol_address();
2558 for jid in jids {
2559 jid.reset_protocol_address(&mut addr);
2560 let mutex = self.session_lock_for(addr.as_str()).await;
2561 guards.push(mutex.lock_arc().await);
2562 }
2563 guards
2564 }
2565
2566 #[cfg(test)]
2570 pub(crate) async fn session_mutexes_for(
2571 &self,
2572 jids: &[Jid],
2573 ) -> Vec<std::sync::Arc<async_lock::Mutex<()>>> {
2574 let mut mutexes = Vec::with_capacity(jids.len());
2575 let mut addr = wacore::types::jid::make_reusable_protocol_address();
2576 for jid in jids {
2577 jid.reset_protocol_address(&mut addr);
2578 mutexes.push(self.session_lock_for(addr.as_str()).await);
2579 }
2580 mutexes
2581 }
2582}
2583
2584pub(crate) fn is_self_dm_recipient(
2589 recipient_bare: &Jid,
2590 own_pn: &Jid,
2591 own_lid: Option<&Jid>,
2592) -> bool {
2593 match recipient_bare.server {
2594 Server::Lid => own_lid.is_some_and(|lid| recipient_bare.user == lid.user),
2595 Server::Pn => recipient_bare.user == own_pn.user,
2596 _ => false,
2597 }
2598}
2599
2600pub(crate) fn dm_stanza_to(recipient_bare: &Jid, to: &Jid) -> Jid {
2609 if recipient_bare.is_lid() || to.is_lid() {
2610 recipient_bare.clone()
2611 } else {
2612 to.to_non_ad()
2613 }
2614}
2615
2616#[cfg(test)]
2617#[allow(clippy::disallowed_methods)]
2618mod tests {
2619 use super::*;
2620 use crate::test_utils::wait_for_lock_waiter;
2621 use std::str::FromStr;
2622
2623 #[test]
2624 fn status_revoke_requires_a_distinct_outer_stanza_id() {
2625 let target_id = "3EB0REVOKETARGET";
2626 let revoke = wa::Message {
2627 protocol_message: buffa::MessageField::some(wa::message::ProtocolMessage {
2628 r#type: Some(wa::message::protocol_message::Type::Revoke),
2629 key: buffa::MessageField::some(wa::MessageKey {
2630 id: Some(target_id.into()),
2631 ..Default::default()
2632 }),
2633 ..Default::default()
2634 }),
2635 ..Default::default()
2636 };
2637
2638 assert!(matches!(
2639 validate_status_message_id(&revoke, Some(target_id)),
2640 Err(SendError::InvalidRequest(_))
2641 ));
2642 assert!(validate_status_message_id(&revoke, Some("3EB0NEWSTANZAID")).is_ok());
2643 assert!(validate_status_message_id(&revoke, None).is_ok());
2644 }
2645
2646 #[test]
2647 fn dm_stanza_to_follows_resolved_wire_namespace() {
2648 let pn: Jid = "5511987650001@s.whatsapp.net".parse().unwrap();
2649 let lid: Jid = "111000011112222@lid".parse().unwrap();
2650
2651 assert_eq!(dm_stanza_to(&pn, &pn), pn);
2653 assert_eq!(dm_stanza_to(&lid, &pn), lid);
2655 assert_eq!(dm_stanza_to(&lid, &lid), lid);
2657 assert_eq!(dm_stanza_to(&pn, &lid), pn);
2660 let pn_device: Jid = "5511987650001:5@s.whatsapp.net".parse().unwrap();
2662 assert_eq!(dm_stanza_to(&pn, &pn_device), pn);
2663 }
2664
2665 #[test]
2666 fn ensure_self_in_group_shares_when_present_and_appends_when_absent() {
2667 use wacore::client::context::GroupInfo;
2668 use wacore::types::message::AddressingMode;
2669
2670 let own: Jid = "999999999999@s.whatsapp.net".parse().unwrap();
2671 let other: Jid = "111111111111@s.whatsapp.net".parse().unwrap();
2672
2673 let with_self = Arc::new(GroupInfo::new(
2676 vec![other.to_non_ad(), own.to_non_ad()],
2677 AddressingMode::Pn,
2678 ));
2679 let out = ensure_self_in_group(with_self.clone(), &own);
2680 assert!(Arc::ptr_eq(&with_self, &out));
2681
2682 let without_self = Arc::new(GroupInfo::new(vec![other.to_non_ad()], AddressingMode::Pn));
2684 let out = ensure_self_in_group(without_self.clone(), &own);
2685 assert!(!Arc::ptr_eq(&without_self, &out));
2686 assert_eq!(out.participants.len(), 2);
2687 assert!(out.participants.iter().any(|p| p.is_same_user_as(&own)));
2688 }
2689
2690 #[tokio::test]
2694 async fn group_skdm_lock_shares_dm_per_device_session_mutex() {
2695 use wacore::client::context::SendContextResolver;
2696
2697 let client = crate::test_utils::create_test_client().await;
2698 let device: Jid = "15551234567:3@s.whatsapp.net".parse().unwrap();
2699
2700 let keys = client
2702 .build_session_lock_keys(std::slice::from_ref(&device))
2703 .await;
2704 let dm_mutexes = client.session_mutexes_for(&keys).await;
2705 assert_eq!(dm_mutexes.len(), 1);
2706 assert!(
2707 dm_mutexes[0].try_lock().is_some(),
2708 "uncontended before the group lock"
2709 );
2710
2711 let guard = client
2713 .lock_device_sessions(std::slice::from_ref(&device))
2714 .await;
2715 assert!(
2716 dm_mutexes[0].try_lock().is_none(),
2717 "group SKDM fan-out must block the DM per-device session lock"
2718 );
2719
2720 drop(guard);
2721 assert!(
2722 dm_mutexes[0].try_lock().is_some(),
2723 "the per-device session lock releases when the group guard drops"
2724 );
2725 }
2726
2727 #[tokio::test]
2728 async fn send_message_to_status_without_reaction_errors() {
2729 let client = crate::test_utils::create_test_client().await;
2730 let to = Jid::status_broadcast();
2731 let err = client
2732 .send_message(
2733 to,
2734 wa::Message {
2735 conversation: Some("hi".into()),
2736 ..Default::default()
2737 },
2738 )
2739 .await
2740 .expect_err("status@broadcast without reaction must error");
2741 let msg = format!("{err}");
2742 assert!(
2743 msg.contains("reaction_message") || msg.contains("status"),
2744 "unexpected error: {msg}"
2745 );
2746 }
2747
2748 #[tokio::test]
2749 async fn status_send_waits_for_distribution_guard() {
2750 let client = crate::test_utils::create_test_client().await;
2751 let own_pn: Jid = "15551234001@s.whatsapp.net".parse().unwrap();
2752 let own_lid: Jid = "100000000000001@lid".parse().unwrap();
2753 client
2754 .persistence_manager
2755 .process_command(DeviceCommand::SetId(Some(own_pn)))
2756 .await;
2757 client
2758 .persistence_manager
2759 .process_command(DeviceCommand::SetLid(Some(own_lid)))
2760 .await;
2761
2762 let status = Jid::status_broadcast();
2763 let held = client.group_distribution_lock(&status).await;
2764 let lock = client
2765 .group_distribution_locks
2766 .get(&status)
2767 .await
2768 .expect("cached distribution lock");
2769 let lock_refs = Arc::strong_count(&lock);
2770 let mut task = tokio::spawn({
2771 let client = client.clone();
2772 async move {
2773 let recipient: Jid = "100000000000002@lid".parse().unwrap();
2774 client
2775 .send_status_message(
2776 wa::Message {
2777 conversation: Some("serialized status".into()),
2778 ..Default::default()
2779 },
2780 std::slice::from_ref(&recipient),
2781 crate::features::status::StatusSendOptions::default(),
2782 )
2783 .await
2784 }
2785 });
2786
2787 wait_for_lock_waiter(&lock, lock_refs).await;
2788 assert!(!task.is_finished(), "status send must wait for the lane");
2789 drop(held);
2790
2791 let _ = tokio::time::timeout(std::time::Duration::from_secs(5), &mut task)
2792 .await
2793 .expect("status send must resume")
2794 .expect("status task");
2795 }
2796
2797 #[tokio::test]
2802 async fn send_message_logged_out_dm_returns_not_logged_in() {
2803 let client = crate::test_utils::create_test_client().await;
2804 let to: Jid = "111111111111@s.whatsapp.net".parse().unwrap();
2805 let err = client
2806 .send_message(
2807 to,
2808 wa::Message {
2809 conversation: Some("hi".into()),
2810 ..Default::default()
2811 },
2812 )
2813 .await
2814 .expect_err("logged-out DM send must error");
2815 assert!(
2816 matches!(err, SendError::NotLoggedIn),
2817 "expected SendError::NotLoggedIn, got: {err:?}"
2818 );
2819 }
2820
2821 #[tokio::test]
2824 async fn edit_message_logged_out_dm_returns_not_logged_in() {
2825 let client = crate::test_utils::create_test_client().await;
2826 let to: Jid = "111111111111@s.whatsapp.net".parse().unwrap();
2827 let err = client
2828 .edit_message(
2829 to,
2830 "ORIG_ID",
2831 wa::Message {
2832 conversation: Some("x".into()),
2833 ..Default::default()
2834 },
2835 )
2836 .await
2837 .expect_err("logged-out DM edit must error");
2838 assert!(
2839 matches!(err, SendError::NotLoggedIn),
2840 "expected SendError::NotLoggedIn, got: {err:?}"
2841 );
2842 }
2843
2844 #[tokio::test]
2847 async fn edit_message_with_empty_stanza_id_returns_invalid_request() {
2848 let client = crate::test_utils::create_test_client().await;
2849 seed_pn(&client, "222222222222@s.whatsapp.net").await;
2850 let to: Jid = "111111111111@s.whatsapp.net".parse().unwrap();
2851 let err = client
2852 .edit_message_with_options(
2853 to,
2854 "ORIG_ID",
2855 wa::Message {
2856 conversation: Some("x".into()),
2857 ..Default::default()
2858 },
2859 EditOptions {
2860 stanza_id: Some(String::new()),
2861 },
2862 )
2863 .await
2864 .expect_err("empty stanza_id must error");
2865 assert!(
2866 matches!(err, SendError::InvalidRequest(_)),
2867 "expected SendError::InvalidRequest, got: {err:?}"
2868 );
2869 }
2870
2871 #[tokio::test]
2872 async fn send_message_to_status_reaction_rejects_non_user_participant() {
2873 let client = crate::test_utils::create_test_client().await;
2874 let to = Jid::status_broadcast();
2875 let err = client
2876 .send_message(
2877 to,
2878 wa::Message {
2879 reaction_message: buffa::MessageField::some(wa::message::ReactionMessage {
2880 key: buffa::MessageField::some(wa::MessageKey {
2881 remote_jid: Some("status@broadcast".into()),
2882 from_me: Some(false),
2883 id: Some("ORIGID".into()),
2884 participant: Some("120363040237990503@g.us".into()),
2885 }),
2886 text: Some("❤️".into()),
2887 sender_timestamp_ms: Some(1),
2888 ..Default::default()
2889 }),
2890 ..Default::default()
2891 },
2892 )
2893 .await
2894 .expect_err("group JID as participant must error");
2895 assert!(
2896 format!("{err}").contains("user JID"),
2897 "expected user-JID error, got: {err}"
2898 );
2899 }
2900
2901 #[tokio::test]
2902 async fn send_message_to_status_reaction_without_participant_errors() {
2903 let client = crate::test_utils::create_test_client().await;
2904 let to = Jid::status_broadcast();
2905 let err = client
2906 .send_message(
2907 to,
2908 wa::Message {
2909 reaction_message: buffa::MessageField::some(wa::message::ReactionMessage {
2910 key: buffa::MessageField::some(wa::MessageKey {
2911 remote_jid: Some("status@broadcast".into()),
2912 from_me: Some(false),
2913 id: Some("ORIGID".into()),
2914 participant: None,
2915 }),
2916 text: Some("❤️".into()),
2917 sender_timestamp_ms: Some(1),
2918 ..Default::default()
2919 }),
2920 ..Default::default()
2921 },
2922 )
2923 .await
2924 .expect_err("reaction without key.participant must error");
2925 assert!(
2926 format!("{err}").contains("participant"),
2927 "expected participant error, got: {err}"
2928 );
2929 }
2930
2931 #[test]
2932 fn test_revoke_type_default_is_sender() {
2933 let revoke_type = RevokeType::default();
2935 assert_eq!(revoke_type, RevokeType::Sender);
2936 }
2937
2938 #[test]
2939 fn test_force_skdm_only_for_admin_revoke() {
2940 let sender_jid = Jid::from_str("123456@s.whatsapp.net").unwrap();
2944
2945 let sender_revoke = RevokeType::Sender;
2946 let admin_revoke = RevokeType::Admin {
2947 original_sender: sender_jid,
2948 };
2949
2950 let force_skdm_sender = matches!(sender_revoke, RevokeType::Admin { .. });
2952 let force_skdm_admin = matches!(admin_revoke, RevokeType::Admin { .. });
2953
2954 assert!(!force_skdm_sender, "Sender revoke should NOT force SKDM");
2955 assert!(force_skdm_admin, "Admin revoke MUST force SKDM");
2956 }
2957
2958 #[test]
2959 fn test_sender_revoke_message_key_structure() {
2960 let to = Jid::from_str("120363040237990503@g.us").unwrap();
2963 let message_id = "3EB0ABC123".to_string();
2964
2965 let (from_me, participant, edit_attr) = match RevokeType::Sender {
2966 RevokeType::Sender => (true, None, EditAttribute::SenderRevoke),
2967 RevokeType::Admin { original_sender } => (
2968 false,
2969 Some(original_sender.to_non_ad_string()),
2970 EditAttribute::AdminRevoke,
2971 ),
2972 };
2973
2974 assert!(from_me, "Sender revoke must have from_me=true");
2975 assert!(
2976 participant.is_none(),
2977 "Sender revoke must NOT set participant"
2978 );
2979 assert_eq!(edit_attr.to_string_val(), "7");
2980
2981 let revoke_message = build_revoke_message(&to, from_me, message_id.clone(), participant);
2982
2983 let proto_msg = revoke_message.protocol_message.into_option().unwrap();
2984 let key = proto_msg.key.into_option().unwrap();
2985 assert_eq!(key.from_me, Some(true));
2986 assert_eq!(key.participant, None);
2987 assert_eq!(key.id, Some(message_id));
2988 }
2989
2990 #[test]
2991 fn test_admin_revoke_message_key_structure() {
2992 let to = Jid::from_str("120363040237990503@g.us").unwrap();
2995 let message_id = "3EB0ABC123".to_string();
2996 let original_sender = Jid::from_str("236395184570386:22@lid").unwrap();
2997
2998 let revoke_type = RevokeType::Admin {
2999 original_sender: original_sender.clone(),
3000 };
3001 let (from_me, participant, edit_attr) = match revoke_type {
3002 RevokeType::Sender => (true, None, EditAttribute::SenderRevoke),
3003 RevokeType::Admin { original_sender } => (
3004 false,
3005 Some(original_sender.to_non_ad_string()),
3006 EditAttribute::AdminRevoke,
3007 ),
3008 };
3009
3010 assert!(!from_me, "Admin revoke must have from_me=false");
3011 assert!(
3012 participant.is_some(),
3013 "Admin revoke MUST set participant to original sender"
3014 );
3015 assert_eq!(edit_attr.to_string_val(), "8");
3016
3017 let revoke_message =
3018 build_revoke_message(&to, from_me, message_id.clone(), participant.clone());
3019
3020 let proto_msg = revoke_message.protocol_message.into_option().unwrap();
3021 let key = proto_msg.key.into_option().unwrap();
3022 assert_eq!(key.from_me, Some(false));
3023 assert_eq!(key.participant, Some("236395184570386@lid".to_string()));
3025 assert_eq!(key.id, Some(message_id));
3026 }
3027
3028 const SELF_PN: &str = "5500000000000";
3033 const SELF_LID: &str = "111111111111111";
3034 const SELF_DEVICE: u16 = 7;
3035 const OTHER_LID: &str = "222222222222222";
3036
3037 #[test]
3038 fn self_dm_lid_recipient_matches_own_lid() {
3039 let own_pn = Jid::pn_device(SELF_PN, SELF_DEVICE);
3040 let own_lid = Jid::lid_device(SELF_LID, SELF_DEVICE);
3041 let recipient = Jid::lid(SELF_LID);
3042
3043 assert!(is_self_dm_recipient(&recipient, &own_pn, Some(&own_lid)));
3044 }
3045
3046 #[test]
3047 fn self_dm_pn_recipient_matches_own_pn() {
3048 let own_pn = Jid::pn_device(SELF_PN, SELF_DEVICE);
3050 let own_lid = Jid::lid_device(SELF_LID, SELF_DEVICE);
3051 let recipient = Jid::pn(SELF_PN);
3052
3053 assert!(is_self_dm_recipient(&recipient, &own_pn, Some(&own_lid)));
3054 }
3055
3056 #[test]
3057 fn self_dm_pn_recipient_self_dm_even_without_own_lid() {
3058 let own_pn = Jid::pn_device(SELF_PN, SELF_DEVICE);
3060 let recipient = Jid::pn(SELF_PN);
3061
3062 assert!(is_self_dm_recipient(&recipient, &own_pn, None));
3063 }
3064
3065 #[test]
3066 fn non_self_lid_recipient_is_not_self_dm() {
3067 let own_pn = Jid::pn_device(SELF_PN, SELF_DEVICE);
3068 let own_lid = Jid::lid_device(SELF_LID, SELF_DEVICE);
3069 let recipient = Jid::lid(OTHER_LID);
3070
3071 assert!(!is_self_dm_recipient(&recipient, &own_pn, Some(&own_lid)));
3072 }
3073
3074 #[test]
3075 fn lid_recipient_without_own_lid_is_not_self_dm() {
3076 let own_pn = Jid::pn_device(SELF_PN, SELF_DEVICE);
3079 let recipient = Jid::lid(SELF_PN);
3080
3081 assert!(!is_self_dm_recipient(&recipient, &own_pn, None));
3082 }
3083
3084 #[test]
3085 fn group_or_broadcast_recipient_is_not_self_dm() {
3086 let own_pn = Jid::pn_device(SELF_PN, SELF_DEVICE);
3088 let own_lid = Jid::lid_device(SELF_LID, SELF_DEVICE);
3089
3090 assert!(!is_self_dm_recipient(
3091 &Jid::group("120363000000000000"),
3092 &own_pn,
3093 Some(&own_lid),
3094 ));
3095 assert!(!is_self_dm_recipient(
3096 &Jid::status_broadcast(),
3097 &own_pn,
3098 Some(&own_lid),
3099 ));
3100 }
3101
3102 #[test]
3103 fn self_dm_with_no_recipient_cache_still_appends_own_devices() {
3104 let own_pn = Jid::pn_device(SELF_PN, SELF_DEVICE);
3109 let own_lid = Jid::lid_device(SELF_LID, SELF_DEVICE);
3110 let recipient_bare = Jid::lid(SELF_LID);
3111 assert!(is_self_dm_recipient(
3112 &recipient_bare,
3113 &own_pn,
3114 Some(&own_lid)
3115 ));
3116
3117 let recipient_cached: Option<Vec<Jid>> = None;
3118 let own_cached_pn: Vec<Jid> = [0u16, 3, SELF_DEVICE]
3119 .into_iter()
3120 .map(|d| Jid::pn_device(SELF_PN, d))
3121 .collect();
3122
3123 let keep_own = recipient_cached.is_none();
3126 assert!(keep_own);
3127
3128 let mut all_dm_jids = match recipient_cached {
3129 Some(devices) => devices,
3130 None => vec![recipient_bare],
3131 };
3132 if keep_own {
3133 all_dm_jids.extend(own_cached_pn.iter().cloned());
3134 }
3135 all_dm_jids.retain(|j| {
3136 let is_sender = (j.is_same_user_as(&own_pn) && j.device == own_pn.device)
3137 || (j.is_same_user_as(&own_lid) && j.device == own_lid.device);
3138 !is_sender
3139 });
3140 wacore::types::jid::sort_dedup_by_device(&mut all_dm_jids);
3141
3142 assert!(
3144 all_dm_jids.iter().any(|j| j.is_lid()),
3145 "bare recipient LID must remain"
3146 );
3147 assert_eq!(
3148 all_dm_jids.iter().filter(|j| j.is_pn()).count(),
3149 2,
3150 "companion PN devices must survive when recipient_cached is None"
3151 );
3152 }
3153
3154 #[test]
3155 fn old_merge_produced_lid_pn_duplicates_for_self_dm() {
3156 let own_pn = Jid::pn_device(SELF_PN, SELF_DEVICE);
3160 let own_lid = Jid::lid_device(SELF_LID, SELF_DEVICE);
3161 let recipient_bare = Jid::lid(SELF_LID);
3162
3163 let devices = [0u16, 3, 5, SELF_DEVICE];
3164 let recipient_cached: Vec<Jid> = devices
3165 .iter()
3166 .map(|&d| Jid::lid_device(SELF_LID, d))
3167 .collect();
3168 let own_cached: Vec<Jid> = devices
3169 .iter()
3170 .map(|&d| Jid::pn_device(SELF_PN, d))
3171 .collect();
3172
3173 let retain_non_sender = |j: &Jid| {
3174 let is_sender = (j.is_same_user_as(&own_pn) && j.device == own_pn.device)
3175 || (j.is_same_user_as(&own_lid) && j.device == own_lid.device);
3176 !is_sender
3177 };
3178
3179 let mut buggy = recipient_cached.clone();
3180 buggy.extend(own_cached.clone());
3181 buggy.retain(retain_non_sender);
3182 wacore::types::jid::sort_dedup_by_device(&mut buggy);
3183 assert_eq!(buggy.len(), (devices.len() - 1) * 2);
3184
3185 assert!(is_self_dm_recipient(
3186 &recipient_bare,
3187 &own_pn,
3188 Some(&own_lid)
3189 ));
3190
3191 let mut fixed = recipient_cached;
3192 fixed.retain(retain_non_sender);
3193 wacore::types::jid::sort_dedup_by_device(&mut fixed);
3194 assert_eq!(fixed.len(), devices.len() - 1);
3195 for j in &fixed {
3196 assert!(j.is_lid());
3197 }
3198 }
3199
3200 #[test]
3201 fn test_admin_revoke_preserves_lid_format() {
3202 let lid_sender = Jid::from_str("236395184570386:22@lid").unwrap();
3206 let participant_str = lid_sender.to_non_ad_string();
3207
3208 assert_eq!(participant_str, "236395184570386@lid");
3210 assert!(
3211 participant_str.ends_with("@lid"),
3212 "LID participant must preserve @lid suffix"
3213 );
3214 }
3215
3216 #[test]
3219 fn test_skdm_recipient_filtering_basic() {
3220 use std::collections::HashSet;
3221
3222 let known_recipients: Vec<Jid> = [
3223 "1234567890:0@s.whatsapp.net",
3224 "1234567890:5@s.whatsapp.net",
3225 "9876543210:0@s.whatsapp.net",
3226 ]
3227 .into_iter()
3228 .map(|s| Jid::from_str(s).unwrap())
3229 .collect();
3230
3231 let all_devices: Vec<Jid> = [
3232 "1234567890:0@s.whatsapp.net",
3233 "1234567890:5@s.whatsapp.net",
3234 "9876543210:0@s.whatsapp.net",
3235 "5555555555:0@s.whatsapp.net", ]
3237 .into_iter()
3238 .map(|s| Jid::from_str(s).unwrap())
3239 .collect();
3240
3241 let known_set: HashSet<DeviceKey<'_>> =
3242 known_recipients.iter().map(|j| j.device_key()).collect();
3243
3244 let new_devices: Vec<Jid> = all_devices
3245 .into_iter()
3246 .filter(|device| !known_set.contains(&device.device_key()))
3247 .collect();
3248
3249 assert_eq!(new_devices.len(), 1);
3250 assert_eq!(new_devices[0].user, "5555555555");
3251 }
3252
3253 #[test]
3254 fn test_skdm_recipient_filtering_lid_jids() {
3255 use std::collections::HashSet;
3256
3257 let known_recipients: Vec<Jid> = [
3258 "236395184570386:91@lid",
3259 "129171292463295:0@lid",
3260 "45857667830004:14@lid",
3261 ]
3262 .into_iter()
3263 .map(|s| Jid::from_str(s).unwrap())
3264 .collect();
3265
3266 let all_devices: Vec<Jid> = [
3267 "236395184570386:91@lid",
3268 "129171292463295:0@lid",
3269 "45857667830004:14@lid",
3270 "45857667830004:15@lid", ]
3272 .into_iter()
3273 .map(|s| Jid::from_str(s).unwrap())
3274 .collect();
3275
3276 let known_set: HashSet<DeviceKey<'_>> =
3277 known_recipients.iter().map(|j| j.device_key()).collect();
3278
3279 let new_devices: Vec<Jid> = all_devices
3280 .into_iter()
3281 .filter(|device| !known_set.contains(&device.device_key()))
3282 .collect();
3283
3284 assert_eq!(new_devices.len(), 1);
3285 assert_eq!(new_devices[0].user, "45857667830004");
3286 assert_eq!(new_devices[0].device, 15);
3287 }
3288
3289 #[test]
3290 fn test_skdm_recipient_filtering_all_known() {
3291 use std::collections::HashSet;
3292
3293 let known_recipients: Vec<Jid> =
3294 ["1234567890:0@s.whatsapp.net", "1234567890:5@s.whatsapp.net"]
3295 .into_iter()
3296 .map(|s| Jid::from_str(s).unwrap())
3297 .collect();
3298
3299 let all_devices: Vec<Jid> = ["1234567890:0@s.whatsapp.net", "1234567890:5@s.whatsapp.net"]
3300 .into_iter()
3301 .map(|s| Jid::from_str(s).unwrap())
3302 .collect();
3303
3304 let known_set: HashSet<DeviceKey<'_>> =
3305 known_recipients.iter().map(|j| j.device_key()).collect();
3306
3307 let new_devices: Vec<Jid> = all_devices
3308 .into_iter()
3309 .filter(|device| !known_set.contains(&device.device_key()))
3310 .collect();
3311
3312 assert!(new_devices.is_empty());
3313 }
3314
3315 #[test]
3316 fn test_skdm_recipient_filtering_all_new() {
3317 use std::collections::HashSet;
3318
3319 let known_recipients: Vec<Jid> = vec![];
3320
3321 let all_devices: Vec<Jid> = ["1234567890:0@s.whatsapp.net", "9876543210:0@s.whatsapp.net"]
3322 .into_iter()
3323 .map(|s| Jid::from_str(s).unwrap())
3324 .collect();
3325
3326 let known_set: HashSet<DeviceKey<'_>> =
3327 known_recipients.iter().map(|j| j.device_key()).collect();
3328
3329 let new_devices: Vec<Jid> = all_devices
3330 .clone()
3331 .into_iter()
3332 .filter(|device| !known_set.contains(&device.device_key()))
3333 .collect();
3334
3335 assert_eq!(new_devices.len(), all_devices.len());
3336 }
3337
3338 #[test]
3339 fn test_device_key_comparison() {
3340 let test_cases = [
3343 (
3344 "1234567890:0@s.whatsapp.net",
3345 "1234567890@s.whatsapp.net",
3346 true,
3347 ),
3348 (
3349 "1234567890:5@s.whatsapp.net",
3350 "1234567890:5@s.whatsapp.net",
3351 true,
3352 ),
3353 (
3354 "1234567890:5@s.whatsapp.net",
3355 "1234567890:6@s.whatsapp.net",
3356 false,
3357 ),
3358 ("236395184570386:91@lid", "236395184570386:91@lid", true),
3359 ("236395184570386:0@lid", "236395184570386@lid", true),
3360 ("user1@s.whatsapp.net", "user2@s.whatsapp.net", false),
3361 ];
3362
3363 for (jid1_str, jid2_str, should_match) in test_cases {
3364 let jid1: Jid = jid1_str.parse().expect("should parse jid1");
3365 let jid2: Jid = jid2_str.parse().expect("should parse jid2");
3366
3367 let key1 = jid1.device_key();
3368 let key2 = jid2.device_key();
3369
3370 assert_eq!(
3371 key1 == key2,
3372 should_match,
3373 "DeviceKey comparison failed for '{}' vs '{}': expected match={}, got match={}",
3374 jid1_str,
3375 jid2_str,
3376 should_match,
3377 key1 == key2
3378 );
3379
3380 assert_eq!(
3381 jid1.device_eq(&jid2),
3382 should_match,
3383 "device_eq failed for '{}' vs '{}'",
3384 jid1_str,
3385 jid2_str
3386 );
3387 }
3388 }
3389
3390 #[test]
3391 fn empty_sender_key_device_map_marks_all_devices_for_skdm() {
3392 use crate::sender_key_device_cache::SenderKeyDeviceMap;
3393
3394 let map = SenderKeyDeviceMap::from_db_rows(&[]);
3395 assert_eq!(map.device_has_key("271060335329480", 0), None);
3396
3397 let all_resolved_devices: Vec<Jid> = [
3398 "271060335329480@lid",
3399 "77610646245392@lid",
3400 "276661023027320:5@lid",
3401 ]
3402 .into_iter()
3403 .map(|s| Jid::from_str(s).unwrap())
3404 .collect();
3405
3406 let needs_skdm: Vec<&Jid> = all_resolved_devices
3407 .iter()
3408 .filter(|device| {
3409 !map.device_has_key(&device.user, device.device)
3410 .unwrap_or(false)
3411 || !map.device_has_key(&device.user, 0).unwrap_or(false)
3412 })
3413 .collect();
3414
3415 assert_eq!(needs_skdm.len(), all_resolved_devices.len());
3416 }
3417
3418 #[tokio::test]
3420 async fn resolve_skdm_targets_distributes_when_cache_empty_but_devices_known() {
3421 use wacore::client::context::GroupInfo;
3422 use wacore::store::traits::{DeviceInfo, DeviceListRecord};
3423 use wacore::types::message::AddressingMode;
3424
3425 let client = crate::test_utils::create_test_client().await;
3426 let group_jid = "120363161500776365@g.us";
3427 let own_lid = Jid::from_str("193832511623409:13@lid").unwrap();
3428
3429 let participant_users = ["271060335329480", "77610646245392", "276661023027320"];
3430
3431 for user in &participant_users {
3433 let record = DeviceListRecord {
3434 user: (*user).into(),
3435 devices: vec![DeviceInfo::new(0, None)],
3436 timestamp: wacore::time::now_secs(),
3437 phash: None,
3438 raw_id: None,
3439 };
3440 client
3441 .device_registry_cache
3442 .raw_insert_for_tests((*user).into(), Arc::new(record))
3443 .await;
3444 }
3445
3446 let participants: Vec<Jid> = participant_users
3447 .iter()
3448 .map(|u| Jid::from_str(&format!("{u}@lid")).unwrap())
3449 .collect();
3450
3451 let group_info = GroupInfo::new(participants.clone(), AddressingMode::Lid);
3452
3453 let needs_skdm = client
3454 .resolve_status_skdm_targets(
3455 group_jid,
3456 &group_info,
3457 &own_lid,
3458 crate::cache::Freshness::CachePreferred,
3459 false,
3460 )
3461 .await
3462 .expect("device resolution must succeed")
3463 .expect("missing targets means device resolution failed");
3464
3465 assert_eq!(needs_skdm.len(), participants.len());
3468 for user in &participant_users {
3469 assert!(needs_skdm.iter().any(|j| j.user == *user));
3470 }
3471 }
3472
3473 #[test]
3474 fn single_forgotten_row_keeps_full_distribution() {
3475 use crate::sender_key_device_cache::SenderKeyDeviceMap;
3476
3477 let map = SenderKeyDeviceMap::from_db_rows(&[("271060335329480@lid".to_string(), false)]);
3478 assert_eq!(map.device_has_key("271060335329480", 0), Some(false));
3479
3480 let all_resolved_devices: Vec<Jid> = [
3481 "271060335329480@lid",
3482 "77610646245392@lid",
3483 "276661023027320:5@lid",
3484 ]
3485 .into_iter()
3486 .map(|s| Jid::from_str(s).unwrap())
3487 .collect();
3488
3489 let needs_skdm: Vec<&Jid> = all_resolved_devices
3490 .iter()
3491 .filter(|device| {
3492 !map.device_has_key(&device.user, device.device)
3493 .unwrap_or(false)
3494 || !map.device_has_key(&device.user, 0).unwrap_or(false)
3495 })
3496 .collect();
3497
3498 assert_eq!(
3499 needs_skdm.len(),
3500 3,
3501 "after retry inserts one row, ALL devices correctly flagged for SKDM \
3502 (this is what unblocks redistribution on the SECOND message)"
3503 );
3504 }
3505
3506 #[tokio::test]
3511 async fn filter_skdm_targets_uses_primary_device_gate() {
3512 use crate::sender_key_device_cache::SenderKeyDeviceMap;
3513
3514 let client = crate::test_utils::create_test_client().await;
3515 let group = "120363161500776365@g.us";
3516 let own = Jid::from_str("999999999999999:1@lid").unwrap();
3517
3518 let map = SenderKeyDeviceMap::from_db_rows(&[
3520 ("100100100100100@lid".to_string(), true),
3521 ("100100100100100:5@lid".to_string(), false),
3522 ]);
3523 let devices = [
3524 Jid::from_str("100100100100100@lid").unwrap(),
3525 Jid::from_str("100100100100100:5@lid").unwrap(),
3526 ];
3527 let needs = client.filter_skdm_targets(group, &devices, &map, &own);
3528 assert_eq!(needs.len(), 1, "warm primary keeps the keyed companion out");
3529 assert_eq!(needs[0].device, 5);
3530
3531 let map = SenderKeyDeviceMap::from_db_rows(&[
3534 ("200200200200200@lid".to_string(), false),
3535 ("200200200200200:5@lid".to_string(), true),
3536 ]);
3537 let devices = [
3538 Jid::from_str("200200200200200@lid").unwrap(),
3539 Jid::from_str("200200200200200:5@lid").unwrap(),
3540 ];
3541 let needs = client.filter_skdm_targets(group, &devices, &map, &own);
3542 assert_eq!(needs.len(), 2, "cold primary redistributes the whole user");
3543
3544 let map = SenderKeyDeviceMap::from_db_rows(&[("300300300300300:5@lid".to_string(), true)]);
3547 let devices = [Jid::from_str("300300300300300:5@lid").unwrap()];
3548 let needs = client.filter_skdm_targets(group, &devices, &map, &own);
3549 assert_eq!(
3550 needs.len(),
3551 1,
3552 "absent primary is cold, companion redistributes"
3553 );
3554 }
3555
3556 #[tokio::test]
3562 async fn own_companion_is_never_memoized_so_it_redistributes_every_send() {
3563 use crate::sender_key_device_cache::SenderKeyDeviceMap;
3564
3565 let client = crate::test_utils::create_test_client().await;
3566 let own_lid = Jid::from_str("888000888000888:1@lid").unwrap();
3567 client
3568 .persistence_manager
3569 .process_command(DeviceCommand::SetLid(Some(own_lid.clone())))
3570 .await;
3571
3572 let group = "120363000000000009@g.us";
3573 let own_companion = Jid::from_str("888000888000888:5@lid").unwrap();
3574 let member = Jid::from_str("111000111000111@lid").unwrap();
3575
3576 client
3578 .update_sender_key_devices(group, &[own_companion.clone(), member.clone()])
3579 .await;
3580
3581 let rows = client
3583 .persistence_manager
3584 .get_sender_key_devices(group)
3585 .await
3586 .unwrap();
3587 let map = SenderKeyDeviceMap::from_db_rows(&rows);
3588 assert_eq!(
3589 map.device_has_key("111000111000111", 0),
3590 Some(true),
3591 "external member is memoized warm"
3592 );
3593 assert_eq!(
3594 map.device_has_key("888000888000888", 5),
3595 None,
3596 "own companion is never memoized (re-distributed every send)"
3597 );
3598
3599 let devices = [own_companion.clone(), member];
3601 let needs = client.filter_skdm_targets(group, &devices, &map, &own_lid);
3602 assert_eq!(
3603 needs,
3604 vec![own_companion],
3605 "own companion redistributes; external member stays warm"
3606 );
3607 }
3608
3609 #[tokio::test]
3614 async fn own_only_skdm_mark_keeps_device_map_cached() {
3615 use crate::sender_key_device_cache::SenderKeyDeviceMap;
3616 use std::sync::Arc;
3617
3618 let client = crate::test_utils::create_test_client().await;
3619 let own_lid = Jid::from_str("888000888000888:1@lid").unwrap();
3620 client
3621 .persistence_manager
3622 .process_command(DeviceCommand::SetLid(Some(own_lid.clone())))
3623 .await;
3624
3625 let group = "120363000000000010@g.us";
3626 let own_primary = Jid::from_str("888000888000888:0@lid").unwrap();
3627 let member = Jid::from_str("111000111000111:0@lid").unwrap();
3628
3629 client
3630 .sender_key_device_cache
3631 .get_or_init(group, async {
3632 Arc::new(SenderKeyDeviceMap::from_db_rows(&[]))
3633 })
3634 .await;
3635
3636 client
3638 .update_sender_key_devices(group, std::slice::from_ref(&own_primary))
3639 .await;
3640 client
3641 .sender_key_device_cache
3642 .get_or_init(group, async {
3643 panic!("own-only SKDM mark must not invalidate the device map")
3644 })
3645 .await;
3646
3647 client
3650 .update_sender_key_devices(group, &[own_primary, member])
3651 .await;
3652 let rebuilt = std::sync::atomic::AtomicBool::new(false);
3653 client
3654 .sender_key_device_cache
3655 .get_or_init(group, async {
3656 rebuilt.store(true, std::sync::atomic::Ordering::Relaxed);
3657 Arc::new(SenderKeyDeviceMap::from_db_rows(&[]))
3658 })
3659 .await;
3660 assert!(
3661 rebuilt.load(std::sync::atomic::Ordering::Relaxed),
3662 "a new external warm mark must invalidate the device map"
3663 );
3664 }
3665
3666 #[test]
3670 fn skdm_needs_only_own_devices_classification() {
3671 let own_pn = Jid::from_str("5511999990000:2@s.whatsapp.net").unwrap();
3672 let own_lid = Jid::from_str("888000888000888:2@lid").unwrap();
3673 let own_primary_lid = Jid::from_str("888000888000888:0@lid").unwrap();
3674 let own_primary_pn = Jid::from_str("5511999990000:0@s.whatsapp.net").unwrap();
3675 let member = Jid::from_str("111000111000111:0@lid").unwrap();
3676
3677 assert!(skdm_needs_only_own_devices(
3678 &[own_primary_lid.clone(), own_primary_pn],
3679 Some(&own_pn),
3680 Some(&own_lid)
3681 ));
3682 assert!(
3683 !skdm_needs_only_own_devices(
3684 &[own_primary_lid, member.clone()],
3685 Some(&own_pn),
3686 Some(&own_lid)
3687 ),
3688 "an external member must take the cold path"
3689 );
3690 assert!(
3691 !skdm_needs_only_own_devices(&[], Some(&own_pn), Some(&own_lid)),
3692 "the empty set is handled by its own (fully warm) arm"
3693 );
3694 assert!(
3695 !skdm_needs_only_own_devices(&[member], Some(&own_pn), Some(&own_lid)),
3696 "external-only must take the cold path"
3697 );
3698 }
3699
3700 #[test]
3701 fn test_skdm_filtering_large_group() {
3702 use std::collections::HashSet;
3703
3704 let mut known_recipients: Vec<Jid> = Vec::with_capacity(1000);
3705 let mut all_devices: Vec<Jid> = Vec::with_capacity(1010);
3706
3707 for i in 0..1000i64 {
3708 let jid_str = format!("{}:1@lid", 100000000000000i64 + i);
3709 let jid = Jid::from_str(&jid_str).unwrap();
3710 known_recipients.push(jid.clone());
3711 all_devices.push(jid);
3712 }
3713
3714 for i in 1000i64..1010i64 {
3715 let jid_str = format!("{}:1@lid", 100000000000000i64 + i);
3716 all_devices.push(Jid::from_str(&jid_str).unwrap());
3717 }
3718
3719 let known_set: HashSet<DeviceKey<'_>> =
3720 known_recipients.iter().map(|j| j.device_key()).collect();
3721
3722 let new_devices: Vec<Jid> = all_devices
3723 .into_iter()
3724 .filter(|device| !known_set.contains(&device.device_key()))
3725 .collect();
3726
3727 assert_eq!(new_devices.len(), 10);
3728 }
3729
3730 mod infer_stanza {
3731 use super::*;
3732
3733 #[test]
3734 fn regular_message_returns_none() {
3735 let msg = wa::Message {
3736 conversation: Some("hello".into()),
3737 ..Default::default()
3738 };
3739 let (edit, node) = infer_stanza_metadata(&msg);
3740 assert!(edit.is_none());
3741 assert!(node.is_none());
3742 }
3743
3744 #[test]
3745 fn pin_returns_edit_attribute() {
3746 let msg = wa::Message {
3747 pin_in_chat_message: buffa::MessageField::some(Default::default()),
3748 ..Default::default()
3749 };
3750 let (edit, node) = infer_stanza_metadata(&msg);
3751 assert_eq!(edit, Some(EditAttribute::PinInChat));
3752 assert!(node.is_none());
3753 }
3754
3755 #[test]
3756 fn poll_creation_v3_returns_meta_node() {
3757 let msg = wa::Message {
3758 poll_creation_message_v3: buffa::MessageField::some(Default::default()),
3759 ..Default::default()
3760 };
3761 let (edit, node) = infer_stanza_metadata(&msg);
3762 assert!(edit.is_none());
3763 let node = node.expect("should have meta node");
3764 assert_eq!(node.tag, "meta");
3765 let mut attrs = node.attrs();
3766 assert_eq!(
3767 attrs.optional_string("polltype").unwrap().as_ref(),
3768 "creation"
3769 );
3770 }
3771
3772 #[test]
3773 fn event_returns_meta_node() {
3774 let msg = wa::Message {
3775 event_message: buffa::MessageField::some(Default::default()),
3776 ..Default::default()
3777 };
3778 let (edit, node) = infer_stanza_metadata(&msg);
3779 assert!(edit.is_none());
3780 let node = node.expect("should have meta node");
3781 assert_eq!(node.tag, "meta");
3782 let mut attrs = node.attrs();
3783 assert_eq!(
3784 attrs.optional_string("event_type").unwrap().as_ref(),
3785 "creation"
3786 );
3787 }
3788
3789 #[test]
3790 fn empty_message_returns_none() {
3791 let (edit, node) = infer_stanza_metadata(&wa::Message::default());
3792 assert!(edit.is_none());
3793 assert!(node.is_none());
3794 }
3795
3796 #[test]
3797 fn member_label_set_returns_member_tag_user_update() {
3798 let msg = wacore::send::build_member_label_message("VIP".to_string(), 1_700_000_000);
3799 let (_, node) = infer_stanza_metadata(&msg);
3800 let node = node.expect("member_label should have meta node");
3801 let mut attrs = node.attrs();
3802 assert_eq!(
3803 attrs.optional_string("appdata").unwrap().as_ref(),
3804 "member_tag"
3805 );
3806 assert_eq!(
3807 attrs.optional_string("tag_reason").unwrap().as_ref(),
3808 "user_update"
3809 );
3810 }
3811
3812 #[test]
3813 fn member_label_clear_returns_user_delete() {
3814 let msg = wacore::send::build_member_label_message(String::new(), 1_700_000_000);
3816 let (_, node) = infer_stanza_metadata(&msg);
3817 let node = node.expect("member_label should have meta node");
3818 let mut attrs = node.attrs();
3819 assert_eq!(
3820 attrs.optional_string("appdata").unwrap().as_ref(),
3821 "member_tag"
3822 );
3823 assert_eq!(
3824 attrs.optional_string("tag_reason").unwrap().as_ref(),
3825 "user_delete"
3826 );
3827 }
3828
3829 #[test]
3830 fn poll_creation_v1_returns_meta_node() {
3831 let msg = wa::Message {
3832 poll_creation_message: buffa::MessageField::some(Default::default()),
3833 ..Default::default()
3834 };
3835 let (edit, node) = infer_stanza_metadata(&msg);
3836 assert!(edit.is_none());
3837 let node = node.expect("should have meta node");
3838 assert_eq!(node.tag, "meta");
3839 let mut attrs = node.attrs();
3840 assert_eq!(
3841 attrs.optional_string("polltype").unwrap().as_ref(),
3842 "creation"
3843 );
3844 }
3845
3846 #[test]
3847 fn poll_creation_v2_returns_meta_node() {
3848 let msg = wa::Message {
3849 poll_creation_message_v2: buffa::MessageField::some(Default::default()),
3850 ..Default::default()
3851 };
3852 let (edit, node) = infer_stanza_metadata(&msg);
3853 assert!(edit.is_none());
3854 let node = node.expect("should have meta node");
3855 assert_eq!(node.tag, "meta");
3856 let mut attrs = node.attrs();
3857 assert_eq!(
3858 attrs.optional_string("polltype").unwrap().as_ref(),
3859 "creation"
3860 );
3861 }
3862
3863 #[test]
3864 fn poll_vote_returns_meta_node() {
3865 let msg = wa::Message {
3866 poll_update_message: buffa::MessageField::some(wa::message::PollUpdateMessage {
3867 vote: buffa::MessageField::some(wa::message::PollEncValue::default()),
3868 ..Default::default()
3869 }),
3870 ..Default::default()
3871 };
3872 let (edit, node) = infer_stanza_metadata(&msg);
3873 assert!(edit.is_none());
3874 let node = node.expect("should have meta node");
3875 assert_eq!(node.tag, "meta");
3876 let mut attrs = node.attrs();
3877 assert_eq!(attrs.optional_string("polltype").unwrap().as_ref(), "vote");
3878 }
3879
3880 #[test]
3881 fn view_once_image_emits_view_once_meta() {
3882 let msg = wa::Message {
3883 image_message: buffa::MessageField::some(wa::message::ImageMessage {
3884 view_once: Some(true),
3885 ..Default::default()
3886 }),
3887 ..Default::default()
3888 };
3889 let (_, node) = infer_stanza_metadata(&msg);
3890 let node = node.expect("view-once image should emit meta");
3891 assert_eq!(node.tag, "meta");
3892 assert_eq!(
3893 node.attrs().optional_string("view_once").unwrap().as_ref(),
3894 "true"
3895 );
3896 }
3897
3898 #[test]
3899 fn plain_image_emits_no_meta() {
3900 let msg = wa::Message {
3901 image_message: buffa::MessageField::some(wa::message::ImageMessage::default()),
3902 ..Default::default()
3903 };
3904 assert!(infer_stanza_metadata(&msg).1.is_none());
3905 }
3906
3907 #[test]
3908 fn event_response_returns_meta_node() {
3909 let msg = wa::Message {
3910 enc_event_response_message: buffa::MessageField::some(Default::default()),
3911 ..Default::default()
3912 };
3913 let (edit, node) = infer_stanza_metadata(&msg);
3914 assert!(edit.is_none());
3915 let node = node.expect("should have meta node");
3916 assert_eq!(node.tag, "meta");
3917 let mut attrs = node.attrs();
3918 assert_eq!(
3919 attrs.optional_string("event_type").unwrap().as_ref(),
3920 "response"
3921 );
3922 }
3923
3924 #[test]
3925 fn poll_update_without_vote_returns_none() {
3926 let msg = wa::Message {
3927 poll_update_message: buffa::MessageField::some(wa::message::PollUpdateMessage {
3928 ..Default::default()
3929 }),
3930 ..Default::default()
3931 };
3932 let (edit, node) = infer_stanza_metadata(&msg);
3933 assert!(edit.is_none());
3934 assert!(node.is_none());
3935 }
3936
3937 #[test]
3938 fn revoked_reaction_returns_sender_revoke() {
3939 let msg = wa::Message {
3940 reaction_message: buffa::MessageField::some(wa::message::ReactionMessage {
3941 text: Some(String::new()),
3942 ..Default::default()
3943 }),
3944 ..Default::default()
3945 };
3946 let (edit, _) = infer_stanza_metadata(&msg);
3947 assert_eq!(edit, Some(EditAttribute::SenderRevoke));
3948 }
3949
3950 #[test]
3951 fn keep_in_chat_undo_returns_sender_revoke() {
3952 let msg = wa::Message {
3953 keep_in_chat_message: buffa::MessageField::some(wa::message::KeepInChatMessage {
3954 key: buffa::MessageField::some(wa::MessageKey {
3955 from_me: Some(true),
3956 ..Default::default()
3957 }),
3958 keep_type: Some(wa::KeepType::UndoKeepForAll),
3959 ..Default::default()
3960 }),
3961 ..Default::default()
3962 };
3963 let (edit, _) = infer_stanza_metadata(&msg);
3964 assert_eq!(edit, Some(EditAttribute::SenderRevoke));
3965 }
3966
3967 #[test]
3968 fn secret_encrypted_message_edit_returns_message_edit() {
3969 let msg = wa::Message {
3970 secret_encrypted_message: buffa::MessageField::some(
3971 wa::message::SecretEncryptedMessage {
3972 secret_enc_type: Some(
3973 wa::message::secret_encrypted_message::SecretEncType::MessageEdit,
3974 ),
3975 ..Default::default()
3976 },
3977 ),
3978 ..Default::default()
3979 };
3980 let (edit, _) = infer_stanza_metadata(&msg);
3981 assert_eq!(edit, Some(EditAttribute::MessageEdit));
3982 }
3983
3984 #[test]
3985 fn secret_encrypted_event_edit_emits_both_edit_attr_and_meta_node() {
3986 let msg = wa::Message {
3989 secret_encrypted_message: buffa::MessageField::some(
3990 wa::message::SecretEncryptedMessage {
3991 secret_enc_type: Some(
3992 wa::message::secret_encrypted_message::SecretEncType::EventEdit,
3993 ),
3994 ..Default::default()
3995 },
3996 ),
3997 ..Default::default()
3998 };
3999 let (edit, node) = infer_stanza_metadata(&msg);
4000 assert_eq!(edit, Some(EditAttribute::MessageEdit));
4001 let node = node.expect("should have meta node");
4002 assert_eq!(
4003 node.attrs().optional_string("event_type").unwrap().as_ref(),
4004 "edit"
4005 );
4006 }
4007
4008 #[test]
4009 fn top_level_edited_message_returns_message_edit() {
4010 let msg = wa::Message {
4011 edited_message: buffa::MessageField::some(wa::message::FutureProofMessage {
4012 message: buffa::MessageField::some(wa::Message::default()),
4013 }),
4014 ..Default::default()
4015 };
4016 let (edit, _) = infer_stanza_metadata(&msg);
4017 assert_eq!(edit, Some(EditAttribute::MessageEdit));
4018 }
4019
4020 #[test]
4021 fn build_edit_message_uses_top_level_protocol_message() {
4022 use std::str::FromStr;
4023 let to = Jid::from_str("5511999999999@s.whatsapp.net").unwrap();
4024 let new_content = wa::Message {
4025 conversation: Some("edited".to_string()),
4026 ..Default::default()
4027 };
4028 let msg = build_edit_message(
4029 &to,
4030 "ORIG_ID".to_string(),
4031 None,
4032 new_content,
4033 1_700_000_000_000,
4034 );
4035
4036 assert!(
4039 msg.edited_message.is_unset(),
4040 "edit must not use the FutureProofMessage wrapper"
4041 );
4042 let pm = msg
4043 .protocol_message
4044 .as_option()
4045 .expect("top-level protocol_message");
4046 assert_eq!(
4047 pm.r#type,
4048 Some(wa::message::protocol_message::Type::MessageEdit)
4049 );
4050 assert_eq!(
4051 pm.key.as_option().and_then(|k| k.id.as_deref()),
4052 Some("ORIG_ID")
4053 );
4054 assert_eq!(pm.key.as_option().and_then(|k| k.from_me), Some(true));
4055 assert_eq!(
4056 pm.edited_message
4057 .as_option()
4058 .and_then(|m| m.conversation.as_deref()),
4059 Some("edited")
4060 );
4061 assert_eq!(
4063 infer_stanza_metadata(&msg).0,
4064 Some(EditAttribute::MessageEdit)
4065 );
4066 }
4067 }
4068
4069 mod biz_node_tests {
4070 use super::*;
4071 use std::str::FromStr;
4072 use wa::message::interactive_message::{
4073 self, NativeFlowMessage, native_flow_message::NativeFlowButton,
4074 };
4075
4076 const FIXED_NOW: u64 = 1_700_000_000;
4078 const EXPECTED_PRIVACY_TS: &str = "1622019543";
4080
4081 fn msg_with_native_flow_button(button_name: &str) -> wa::Message {
4082 wa::Message {
4083 interactive_message: buffa::MessageField::some(wa::message::InteractiveMessage {
4084 interactive_message: Some(
4085 interactive_message::InteractiveMessage::NativeFlowMessage(Box::new(
4086 NativeFlowMessage {
4087 buttons: vec![NativeFlowButton {
4088 name: Some(button_name.to_string()),
4089 button_params_json: None,
4090 }],
4091 message_version: Some(1),
4092 message_params_json: None,
4093 },
4094 )),
4095 ),
4096 ..Default::default()
4097 }),
4098 ..Default::default()
4099 }
4100 }
4101
4102 fn assert_biz_common_attrs(node: &Node, ctx: &str) {
4103 assert_eq!(node.tag, "biz", "{ctx}");
4104 let mut a = node.attrs();
4105 assert_eq!(
4106 a.optional_string("actual_actors").unwrap().as_ref(),
4107 "2",
4108 "{ctx}"
4109 );
4110 assert_eq!(
4111 a.optional_string("host_storage").unwrap().as_ref(),
4112 "2",
4113 "{ctx}"
4114 );
4115 assert_eq!(
4116 a.optional_string("privacy_mode_ts").unwrap().as_ref(),
4117 EXPECTED_PRIVACY_TS,
4118 "{ctx}"
4119 );
4120 }
4121
4122 fn assert_nested_biz(node: &Node, expected_flow_name: &str, ctx: &str) {
4123 assert_biz_common_attrs(node, ctx);
4124 assert!(
4125 node.attrs().optional_string("native_flow_name").is_none(),
4126 "{ctx}: nested form has no native_flow_name attr"
4127 );
4128 let interactive = node
4129 .get_optional_child("interactive")
4130 .unwrap_or_else(|| panic!("{ctx}: missing <interactive>"));
4131 let mut ia = interactive.attrs();
4132 assert_eq!(
4133 ia.optional_string("type").unwrap().as_ref(),
4134 "native_flow",
4135 "{ctx}"
4136 );
4137 assert_eq!(ia.optional_string("v").unwrap().as_ref(), "1", "{ctx}");
4138
4139 let nf = interactive
4140 .get_optional_child("native_flow")
4141 .unwrap_or_else(|| panic!("{ctx}: missing <native_flow>"));
4142 let mut nfa = nf.attrs();
4143 assert_eq!(nfa.optional_string("v").unwrap().as_ref(), "9", "{ctx}");
4144 assert_eq!(
4145 nfa.optional_string("name").unwrap().as_ref(),
4146 expected_flow_name,
4147 "{ctx}"
4148 );
4149
4150 let qc = node
4151 .get_optional_child("quality_control")
4152 .unwrap_or_else(|| panic!("{ctx}: missing <quality_control>"));
4153 assert_eq!(
4154 qc.attrs().optional_string("source_type").unwrap().as_ref(),
4155 "third_party",
4156 "{ctx}"
4157 );
4158 }
4159
4160 #[test]
4163 fn payment_simple_form() {
4164 let cases: &[(&str, &str)] = &[
4165 ("payment_info", "payment_info"),
4166 ("review_and_pay", "order_details"),
4167 ("review_order", "order_status"),
4168 ("order_status", "order_status"),
4169 ("payment_status", "payment_status"),
4170 ("payment_method", "payment_method"),
4171 ("payment_reminder", "payment_reminder"),
4172 ];
4173 for (button, expected_flow) in cases {
4174 let biz = infer_biz_node(&msg_with_native_flow_button(button), FIXED_NOW)
4175 .unwrap_or_else(|| panic!("{button}: should produce biz"));
4176 assert_biz_common_attrs(&biz, button);
4177 assert_eq!(
4178 biz.attrs()
4179 .optional_string("native_flow_name")
4180 .unwrap()
4181 .as_ref(),
4182 *expected_flow,
4183 "{button}: native_flow_name attr"
4184 );
4185 assert!(
4186 biz.children().unwrap_or(&[]).is_empty(),
4187 "{button}: PaymentSimple has no children"
4188 );
4189 }
4190 }
4191
4192 #[test]
4197 fn formerly_named_buttons_now_route_through_mixed() {
4198 let cases: &[&str] = &[
4199 "cta_url",
4200 "cta_catalog",
4201 "catalog_message",
4202 "galaxy_message",
4203 "booking_confirmation",
4204 "call_permission_request",
4205 "open_webview",
4206 "message_with_link_status",
4207 ];
4208 for button in cases {
4209 let biz = infer_biz_node(&msg_with_native_flow_button(button), FIXED_NOW)
4210 .unwrap_or_else(|| panic!("{button}: should produce biz"));
4211 assert_nested_biz(&biz, "mixed", button);
4212 }
4213 }
4214
4215 #[test]
4218 fn mixed_form_for_dropped_buttons() {
4219 let cases: &[&str] = &[
4220 "quick_reply",
4221 "cta_copy",
4222 "cta_call",
4223 "single_select",
4224 "send_location",
4225 "future_button_xyz",
4226 ];
4227 for button in cases {
4228 let biz = infer_biz_node(&msg_with_native_flow_button(button), FIXED_NOW)
4229 .unwrap_or_else(|| panic!("{button}: should produce biz"));
4230 assert_nested_biz(&biz, "mixed", button);
4231 }
4232 }
4233
4234 #[test]
4237 fn no_interactive_returns_none() {
4238 let msg = wa::Message {
4239 conversation: Some("hello".into()),
4240 ..Default::default()
4241 };
4242 assert!(infer_biz_node(&msg, FIXED_NOW).is_none());
4243 }
4244
4245 fn carousel_msg(im: wa::message::InteractiveMessage) -> wa::Message {
4246 wa::Message {
4247 interactive_message: buffa::MessageField::some(wa::message::InteractiveMessage {
4248 interactive_message: Some(
4249 interactive_message::InteractiveMessage::CarouselMessage(Default::default()),
4250 ),
4251 ..im
4252 }),
4253 ..Default::default()
4254 }
4255 }
4256
4257 fn body(text: &str) -> buffa::MessageField<interactive_message::Body> {
4258 buffa::MessageField::some(interactive_message::Body {
4259 text: Some(text.to_string()),
4260 })
4261 }
4262
4263 #[test]
4267 fn carousel_with_body_emits_mixed_biz() {
4268 let msg = carousel_msg(wa::message::InteractiveMessage {
4269 body: body("pick a plan"),
4270 ..Default::default()
4271 });
4272 let biz = infer_biz_node(&msg, FIXED_NOW).expect("carousel should produce biz");
4273 assert_nested_biz(&biz, "mixed", "carousel");
4274 }
4275
4276 #[test]
4279 fn carousel_envelope_variants_emit_mixed_biz() {
4280 let title = carousel_msg(wa::message::InteractiveMessage {
4281 header: buffa::MessageField::some(interactive_message::Header {
4282 title: Some("Our menu".into()),
4283 ..Default::default()
4284 }),
4285 ..Default::default()
4286 });
4287 assert_nested_biz(
4288 &infer_biz_node(&title, FIXED_NOW).expect("title should produce biz"),
4289 "mixed",
4290 "header title",
4291 );
4292
4293 let footer = carousel_msg(wa::message::InteractiveMessage {
4294 footer: buffa::MessageField::some(interactive_message::Footer {
4295 text: Some("tap to order".into()),
4296 ..Default::default()
4297 }),
4298 ..Default::default()
4299 });
4300 assert_nested_biz(
4301 &infer_biz_node(&footer, FIXED_NOW).expect("footer should produce biz"),
4302 "mixed",
4303 "footer",
4304 );
4305
4306 let image = carousel_msg(wa::message::InteractiveMessage {
4307 header: buffa::MessageField::some(interactive_message::Header {
4308 media: Some(interactive_message::header::Media::ImageMessage(
4309 Default::default(),
4310 )),
4311 ..Default::default()
4312 }),
4313 ..Default::default()
4314 });
4315 assert_nested_biz(
4316 &infer_biz_node(&image, FIXED_NOW).expect("header image should produce biz"),
4317 "mixed",
4318 "header image",
4319 );
4320 }
4321
4322 #[test]
4324 fn carousel_without_envelope_returns_none() {
4325 assert!(infer_biz_node(&carousel_msg(Default::default()), FIXED_NOW).is_none());
4326 }
4327
4328 #[test]
4331 fn empty_body_text_is_not_an_envelope() {
4332 let msg = carousel_msg(wa::message::InteractiveMessage {
4333 body: body(""),
4334 ..Default::default()
4335 });
4336 assert!(infer_biz_node(&msg, FIXED_NOW).is_none());
4337 }
4338
4339 #[test]
4341 fn shop_storefront_returns_none() {
4342 let msg = wa::Message {
4343 interactive_message: buffa::MessageField::some(wa::message::InteractiveMessage {
4344 body: body("visit our shop"),
4345 interactive_message: Some(
4346 interactive_message::InteractiveMessage::ShopStorefrontMessage(
4347 Default::default(),
4348 ),
4349 ),
4350 ..Default::default()
4351 }),
4352 ..Default::default()
4353 };
4354 assert!(infer_biz_node(&msg, FIXED_NOW).is_none());
4355 }
4356
4357 #[test]
4359 fn interactive_without_native_flow_returns_none() {
4360 let msg = wa::Message {
4361 interactive_message: buffa::MessageField::some(wa::message::InteractiveMessage {
4362 interactive_message: Some(
4363 interactive_message::InteractiveMessage::CollectionMessage(
4364 Default::default(),
4365 ),
4366 ),
4367 ..Default::default()
4368 }),
4369 ..Default::default()
4370 };
4371 assert!(infer_biz_node(&msg, FIXED_NOW).is_none());
4372 }
4373
4374 #[test]
4376 fn native_flow_without_buttons_returns_none() {
4377 let msg = wa::Message {
4378 interactive_message: buffa::MessageField::some(wa::message::InteractiveMessage {
4379 interactive_message: Some(
4380 interactive_message::InteractiveMessage::NativeFlowMessage(Box::new(
4381 NativeFlowMessage {
4382 buttons: vec![],
4383 message_version: Some(1),
4384 message_params_json: None,
4385 },
4386 )),
4387 ),
4388 ..Default::default()
4389 }),
4390 ..Default::default()
4391 };
4392 assert!(infer_biz_node(&msg, FIXED_NOW).is_none());
4393 }
4394
4395 #[test]
4397 fn button_without_name_returns_none() {
4398 let msg = wa::Message {
4399 interactive_message: buffa::MessageField::some(wa::message::InteractiveMessage {
4400 interactive_message: Some(
4401 interactive_message::InteractiveMessage::NativeFlowMessage(Box::new(
4402 NativeFlowMessage {
4403 buttons: vec![NativeFlowButton {
4404 name: None,
4405 button_params_json: None,
4406 }],
4407 message_version: Some(1),
4408 message_params_json: None,
4409 },
4410 )),
4411 ),
4412 ..Default::default()
4413 }),
4414 ..Default::default()
4415 };
4416 assert!(infer_biz_node(&msg, FIXED_NOW).is_none());
4417 }
4418
4419 #[test]
4422 fn document_with_caption_wrapper() {
4423 let inner = wa::Message {
4424 interactive_message: buffa::MessageField::some(wa::message::InteractiveMessage {
4425 interactive_message: Some(
4426 interactive_message::InteractiveMessage::NativeFlowMessage(Box::new(
4427 NativeFlowMessage {
4428 buttons: vec![NativeFlowButton {
4429 name: Some("quick_reply".into()),
4430 button_params_json: None,
4431 }],
4432 message_version: Some(1),
4433 message_params_json: None,
4434 },
4435 )),
4436 ),
4437 ..Default::default()
4438 }),
4439 ..Default::default()
4440 };
4441 let msg = wa::Message {
4442 document_with_caption_message: buffa::MessageField::some(
4443 wa::message::FutureProofMessage {
4444 message: buffa::MessageField::some(inner),
4445 },
4446 ),
4447 ..Default::default()
4448 };
4449 let biz = infer_biz_node(&msg, FIXED_NOW)
4450 .expect("doc-with-caption wrapper should propagate the inner native_flow");
4451 assert_nested_biz(&biz, "mixed", "doc-with-caption/quick_reply");
4452 }
4453
4454 fn quick_reply_biz() -> Node {
4457 infer_biz_node(&msg_with_native_flow_button("quick_reply"), FIXED_NOW)
4458 .expect("quick_reply produces biz")
4459 }
4460
4461 fn payment_biz() -> Node {
4462 infer_biz_node(&msg_with_native_flow_button("payment_info"), FIXED_NOW)
4463 .expect("payment_info produces biz")
4464 }
4465
4466 fn jid(s: &str) -> Jid {
4467 Jid::from_str(s).expect("valid jid in test")
4468 }
4469
4470 #[test]
4473 fn dm_emits_bot_before_biz() {
4474 let nodes = build_extra_stanza_nodes(
4475 &jid("5511999999999@s.whatsapp.net"),
4476 None,
4477 Some(quick_reply_biz()),
4478 vec![],
4479 );
4480 assert_eq!(nodes.len(), 2, "expected [<bot>, <biz>]");
4481 assert_eq!(nodes[0].tag, "bot");
4482 assert_eq!(
4483 nodes[0]
4484 .attrs()
4485 .optional_string("biz_bot")
4486 .unwrap()
4487 .as_ref(),
4488 "1"
4489 );
4490 assert_eq!(nodes[1].tag, "biz");
4491 }
4492
4493 #[test]
4495 fn group_omits_bot() {
4496 let nodes = build_extra_stanza_nodes(
4497 &jid("120363000000000001@g.us"),
4498 None,
4499 Some(quick_reply_biz()),
4500 vec![],
4501 );
4502 assert_eq!(nodes.len(), 1);
4503 assert_eq!(nodes[0].tag, "biz");
4504 }
4505
4506 #[test]
4508 fn lid_dm_emits_bot() {
4509 let nodes = build_extra_stanza_nodes(
4510 &jid("100000000000001@lid"),
4511 None,
4512 Some(payment_biz()),
4513 vec![],
4514 );
4515 assert_eq!(nodes.len(), 2);
4516 assert_eq!(nodes[0].tag, "bot");
4517 }
4518
4519 #[test]
4521 fn no_biz_no_meta_passthrough() {
4522 let user_nodes = vec![NodeBuilder::new("custom").build()];
4523 let nodes =
4524 build_extra_stanza_nodes(&jid("X@s.whatsapp.net"), None, None, user_nodes.clone());
4525 assert_eq!(nodes.len(), 1);
4526 assert_eq!(nodes[0].tag, "custom");
4527 }
4528
4529 #[test]
4531 fn full_ordering_meta_bot_biz_user() {
4532 let meta = NodeBuilder::new("meta").attr("appdata", "default").build();
4533 let user_a = NodeBuilder::new("user_a").build();
4534 let user_b = NodeBuilder::new("user_b").build();
4535 let nodes = build_extra_stanza_nodes(
4536 &jid("X@s.whatsapp.net"),
4537 Some(meta),
4538 Some(quick_reply_biz()),
4539 vec![user_a, user_b],
4540 );
4541 assert_eq!(nodes.len(), 5);
4542 assert_eq!(nodes[0].tag, "meta");
4543 assert_eq!(nodes[1].tag, "bot");
4544 assert_eq!(nodes[2].tag, "biz");
4545 assert_eq!(nodes[3].tag, "user_a");
4546 assert_eq!(nodes[4].tag, "user_b");
4547 }
4548
4549 #[test]
4551 fn meta_only_preserves_order() {
4552 let meta = NodeBuilder::new("meta").build();
4553 let user = NodeBuilder::new("u").build();
4554 let nodes =
4555 build_extra_stanza_nodes(&jid("X@s.whatsapp.net"), Some(meta), None, vec![user]);
4556 assert_eq!(nodes.len(), 2);
4557 assert_eq!(nodes[0].tag, "meta");
4558 assert_eq!(nodes[1].tag, "u");
4559 }
4560 }
4561
4562 #[test]
4563 fn structural_extra_children_are_rejected_before_send_work() {
4564 for tag in RESERVED_EXTRA_STANZA_CHILDREN {
4565 let error = validate_extra_stanza_nodes(&[NodeBuilder::new(tag).build()])
4566 .expect_err("send-owned child must be rejected");
4567 assert!(error.to_string().contains(tag));
4568 }
4569
4570 validate_extra_stanza_nodes(&[
4571 NodeBuilder::new("meta").build(),
4572 NodeBuilder::new("biz").build(),
4573 NodeBuilder::new("custom-extension").build(),
4574 ])
4575 .expect("non-structural protocol extensions remain available");
4576 }
4577
4578 mod session_lock_regression {
4580 use super::*;
4581
4582 #[tokio::test]
4583 async fn per_device_lock_keys_cover_all_devices() {
4584 let client = crate::test_utils::create_test_client().await;
4585
4586 let devices: Vec<Jid> = [
4587 "100000012345678@lid",
4588 "100000012345678:5@lid",
4589 "100000012345678:33@lid",
4590 ]
4591 .iter()
4592 .map(|s| Jid::from_str(s).unwrap())
4593 .collect();
4594
4595 let send_lock_keys = client.build_session_lock_keys(&devices).await;
4597
4598 assert_eq!(send_lock_keys.len(), 3);
4599 assert_eq!(send_lock_keys[0].device, 0);
4601 assert_eq!(send_lock_keys[1].device, 5);
4602 assert_eq!(send_lock_keys[2].device, 33);
4603
4604 for device_jid in &devices {
4606 assert!(
4607 send_lock_keys.contains(device_jid),
4608 "device {device_jid} not in send keys: {send_lock_keys:?}"
4609 );
4610 }
4611
4612 let bare_key = devices[0].to_protocol_address_string();
4614 let device5_key = devices[1].to_protocol_address_string();
4615 assert_ne!(bare_key, device5_key);
4616 }
4617
4618 #[tokio::test]
4619 async fn per_device_lock_serializes_concurrent_session_access() {
4620 use std::sync::Arc;
4621 use std::sync::atomic::{AtomicU32, Ordering};
4622
4623 let session_locks: crate::cache::Cache<String, Arc<async_lock::Mutex<()>>> =
4624 crate::cache::Cache::builder().max_capacity(100).build();
4625
4626 let lock_key = "100000012345678:5@lid.0".to_string();
4627 let access_counter = Arc::new(AtomicU32::new(0));
4628 let max_concurrent = Arc::new(AtomicU32::new(0));
4629
4630 let mut handles = Vec::new();
4631 for _ in 0..10 {
4632 let locks = session_locks.clone();
4633 let key = lock_key.clone();
4634 let counter = access_counter.clone();
4635 let max = max_concurrent.clone();
4636
4637 handles.push(tokio::spawn(async move {
4638 let mutex: Arc<async_lock::Mutex<()>> = locks
4639 .get_with_by_ref(&key, async { Arc::new(async_lock::Mutex::new(())) })
4640 .await;
4641 let _guard = mutex.lock_arc().await;
4644
4645 let active = counter.fetch_add(1, Ordering::SeqCst) + 1;
4646 max.fetch_max(active, Ordering::SeqCst);
4647 tokio::task::yield_now().await;
4648 counter.fetch_sub(1, Ordering::SeqCst);
4649 }));
4650 }
4651
4652 for handle in handles {
4653 handle.await.unwrap();
4654 }
4655
4656 assert_eq!(max_concurrent.load(Ordering::SeqCst), 1);
4657 }
4658
4659 #[tokio::test]
4660 async fn different_device_locks_are_independent() {
4661 use std::sync::Arc;
4662 use std::sync::atomic::{AtomicU32, Ordering};
4663
4664 let session_locks: crate::cache::Cache<String, Arc<async_lock::Mutex<()>>> =
4665 crate::cache::Cache::builder().max_capacity(100).build();
4666
4667 let max_concurrent = Arc::new(AtomicU32::new(0));
4668 let counter = Arc::new(AtomicU32::new(0));
4669 let barrier = Arc::new(tokio::sync::Barrier::new(2));
4670
4671 let keys = ["100000012345678@lid.0", "100000012345678:5@lid.0"];
4672
4673 let mut handles = Vec::new();
4674 for key in keys {
4675 let locks = session_locks.clone();
4676 let key = key.to_string();
4677 let c = counter.clone();
4678 let m = max_concurrent.clone();
4679 let b = barrier.clone();
4680
4681 handles.push(tokio::spawn(async move {
4682 let mutex: Arc<async_lock::Mutex<()>> = locks
4683 .get_with_by_ref(&key, async { Arc::new(async_lock::Mutex::new(())) })
4684 .await;
4685 let _guard = mutex.lock_arc().await;
4687
4688 let active = c.fetch_add(1, Ordering::SeqCst) + 1;
4689 m.fetch_max(active, Ordering::SeqCst);
4690 b.wait().await;
4691 c.fetch_sub(1, Ordering::SeqCst);
4692 }));
4693 }
4694
4695 for handle in handles {
4696 handle.await.unwrap();
4697 }
4698
4699 assert_eq!(max_concurrent.load(Ordering::SeqCst), 2);
4700 }
4701
4702 #[tokio::test]
4706 async fn dm_recipient_uses_bare_address() {
4707 let client = crate::test_utils::create_test_client().await;
4708
4709 let recipient_device33 = Jid::from_str("100000012345678:33@lid").unwrap();
4711 let own_device_5 = Jid::from_str("999999999999:5@s.whatsapp.net").unwrap();
4712
4713 let recipient_bare = client
4715 .resolve_encryption_jid(&recipient_device33)
4716 .await
4717 .to_non_ad();
4718
4719 let all_dm_jids = vec![recipient_bare.clone(), own_device_5.clone()];
4720 let lock_jids = client.build_session_lock_keys(&all_dm_jids).await;
4721
4722 assert_eq!(
4724 recipient_bare.to_protocol_address_string(),
4725 "100000012345678@lid.0"
4726 );
4727 assert!(lock_jids.contains(&recipient_bare));
4728
4729 assert!(lock_jids.contains(&own_device_5));
4731
4732 assert!(
4734 !lock_jids.contains(&recipient_device33),
4735 "recipient must NOT use device-specific address"
4736 );
4737 }
4738
4739 #[test]
4741 fn bare_normalization_deduplicates_recipient_devices() {
4742 let devices: Vec<Jid> = [
4743 "100000012345678@lid",
4744 "100000012345678:5@lid",
4745 "100000012345678:33@lid",
4746 ]
4747 .iter()
4748 .map(|s| Jid::from_str(s).unwrap())
4749 .collect();
4750
4751 let bare: Vec<Jid> = devices.iter().map(|j| j.to_non_ad()).collect();
4753 assert!(bare.windows(2).all(|w| w[0] == w[1]));
4754 assert_eq!(
4755 bare[0].to_protocol_address_string(),
4756 "100000012345678@lid.0"
4757 );
4758 }
4759
4760 #[tokio::test]
4765 async fn taking_guards_locks_every_key_and_releasing_them_frees_every_key() {
4766 let client = crate::test_utils::create_test_client_with_name("guards_cover").await;
4767
4768 for count in [0usize, 1, 3] {
4769 let devices: Vec<Jid> = (0..count)
4770 .map(|i| Jid::from_str(&format!("10000001234567{i}:5@lid")).unwrap())
4771 .collect();
4772 let keys = client.build_session_lock_keys(&devices).await;
4773 assert_eq!(keys.len(), count, "one key per device at count {count}");
4774 let mutexes = client.session_mutexes_for(&keys).await;
4775
4776 let guards = client.session_guards_for(&keys).await;
4777 assert_eq!(guards.len(), count, "one guard per key at count {count}");
4778 for (i, mutex) in mutexes.iter().enumerate() {
4779 assert!(
4780 mutex.try_lock().is_none(),
4781 "key {i} of {count} must be held while the guards live"
4782 );
4783 }
4784
4785 drop(guards);
4786 for (i, mutex) in mutexes.iter().enumerate() {
4787 assert!(
4788 mutex.try_lock().is_some(),
4789 "key {i} of {count} must be free once the guards are dropped"
4790 );
4791 }
4792 }
4793 }
4794
4795 #[tokio::test]
4804 async fn keys_are_locked_in_the_order_they_are_given() {
4805 let client = crate::test_utils::create_test_client_with_name("guards_order").await;
4806
4807 let devices: Vec<Jid> = ["100000012345670:5@lid", "100000012345671:5@lid"]
4808 .iter()
4809 .map(|s| Jid::from_str(s).unwrap())
4810 .collect();
4811 let keys = client.build_session_lock_keys(&devices).await;
4812 assert_eq!(keys.len(), 2);
4813 let mutexes = client.session_mutexes_for(&keys).await;
4814
4815 let blocker = mutexes[1].lock_arc().await;
4816
4817 let mut taker = tokio::spawn({
4818 let client = client.clone();
4819 let keys = keys.clone();
4820 async move { client.session_guards_for(&keys).await.len() }
4821 });
4822
4823 let mut polls = 0;
4825 while mutexes[0].try_lock().is_some() {
4826 polls += 1;
4827 assert!(
4828 polls < 10_000,
4829 "the first key was never taken, so acquisition did not start there"
4830 );
4831 tokio::task::yield_now().await;
4832 }
4833 assert!(
4834 futures::poll!(&mut taker).is_pending(),
4835 "the taker must still be parked on the second key"
4836 );
4837
4838 drop(blocker);
4839 assert_eq!(taker.await.expect("taker finishes"), 2);
4840 }
4841 }
4842
4843 use crate::store::commands::DeviceCommand;
4846 use std::sync::Arc;
4847
4848 async fn seed_pn(client: &Arc<Client>, pn: &str) {
4849 client
4850 .persistence_manager
4851 .process_command(DeviceCommand::SetId(Some(pn.parse().expect("pn"))))
4852 .await;
4853 }
4854
4855 async fn seed_pn_and_lid(client: &Arc<Client>, pn: &str, lid: &str) {
4856 client
4857 .persistence_manager
4858 .process_command(DeviceCommand::SetId(Some(pn.parse().expect("pn"))))
4859 .await;
4860 client
4861 .persistence_manager
4862 .process_command(DeviceCommand::SetLid(Some(lid.parse().expect("lid"))))
4863 .await;
4864 }
4865
4866 fn peer_test_account_proto() -> wa::ADVSignedDeviceIdentity {
4867 wa::ADVSignedDeviceIdentity {
4868 details: Some(vec![0u8; 32]),
4869 account_signature_key: Some(vec![0u8; 32]),
4870 account_signature: Some(vec![0u8; 64]),
4871 device_signature: Some(vec![0u8; 64]),
4872 }
4873 }
4874
4875 async fn seed_peer_send_state(client: &Arc<Client>, peer: &Jid) {
4876 use wacore::libsignal::protocol::{
4877 IdentityKeyPair, KeyPair, PreKeyBundle, SignalProtocolError, UsePQRatchet,
4878 process_prekey_bundle,
4879 };
4880
4881 client
4882 .persistence_manager
4883 .process_command(DeviceCommand::SetAccount(Some(peer_test_account_proto())))
4884 .await;
4885
4886 let bundle =
4887 tokio::task::spawn_blocking(|| -> Result<PreKeyBundle, SignalProtocolError> {
4888 let mut rng = rand::make_rng::<rand::rngs::StdRng>();
4889 let receiver = IdentityKeyPair::generate(&mut rng);
4890 let spk = KeyPair::generate(&mut rng);
4891 let opk = KeyPair::generate(&mut rng);
4892 let sig = receiver
4893 .private_key()
4894 .calculate_signature(&spk.public_key.serialize(), &mut rng)?;
4895
4896 PreKeyBundle::new(
4897 1,
4898 1u32.into(),
4899 Some((1u32.into(), opk.public_key)),
4900 1u32.into(),
4901 spk.public_key,
4902 sig.to_vec(),
4903 *receiver.identity_key(),
4904 )
4905 })
4906 .await
4907 .expect("prekey bundle task")
4908 .expect("prekey bundle");
4909
4910 let mut adapter = client.signal_adapter().await;
4911 let mut rng = rand::make_rng::<rand::rngs::StdRng>();
4912 process_prekey_bundle(
4913 &peer.to_protocol_address(),
4914 &mut adapter.session_store,
4915 &mut adapter.identity_store,
4916 &bundle,
4917 &mut rng,
4918 UsePQRatchet::No,
4919 )
4920 .await
4921 .expect("peer session");
4922 }
4923
4924 fn pdo_request_message(request_type: wa::message::PeerDataOperationRequestType) -> wa::Message {
4925 wa::Message {
4926 protocol_message: buffa::MessageField::some(wa::message::ProtocolMessage {
4927 r#type: Some(wa::message::protocol_message::Type::PeerDataOperationRequestMessage),
4928 peer_data_operation_request_message: buffa::MessageField::some(
4929 wa::message::PeerDataOperationRequestMessage {
4930 peer_data_operation_request_type: Some(request_type),
4931 ..Default::default()
4932 },
4933 ),
4934 ..Default::default()
4935 }),
4936 ..Default::default()
4937 }
4938 }
4939
4940 #[tokio::test]
4941 async fn peer_pdo_send_path_stamps_history_sync_options() {
4942 let client = crate::test_utils::create_test_client_with_name("peer_pdo_attrs").await;
4943 let peer: Jid = "100000000000001@s.whatsapp.net".parse().unwrap();
4944 seed_peer_send_state(&client, &peer).await;
4945
4946 let request_id = "PDO_PEER_ATTRS_1";
4947 let waiter = client
4948 .wait_for_sent_node(crate::client::NodeFilter::tag("message").attr("id", request_id));
4949 let msg =
4950 pdo_request_message(wa::message::PeerDataOperationRequestType::HistorySyncOnDemand);
4951
4952 let result = client
4953 .send_message_impl(
4954 peer,
4955 &msg,
4956 SendPipelineOptions {
4957 request_id: Some(request_id),
4958 peer: true,
4959 ..Default::default()
4960 },
4961 )
4962 .await;
4963 assert!(
4964 result.is_err(),
4965 "test client has no socket; send should fail after stanza capture"
4966 );
4967
4968 let node = tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
4969 .await
4970 .expect("sent node should be captured")
4971 .expect("sent node waiter should resolve");
4972 assert_eq!(
4973 node.attrs().optional_string("category").unwrap().as_ref(),
4974 "peer"
4975 );
4976 assert_eq!(
4977 node.attrs()
4978 .optional_string("push_priority")
4979 .unwrap()
4980 .as_ref(),
4981 "high_force"
4982 );
4983 assert_eq!(
4984 node.attrs()
4985 .optional_string("privacy_sensitive")
4986 .unwrap()
4987 .as_ref(),
4988 "1"
4989 );
4990 }
4991
4992 #[tokio::test]
4993 async fn stanza_type_override_sets_wire_type_attr() {
4994 let client = crate::test_utils::create_test_client_with_name("stanza_type_override").await;
4995 let peer: Jid = "100000000000003@s.whatsapp.net".parse().unwrap();
4996 seed_peer_send_state(&client, &peer).await;
4997
4998 let request_id = "STANZA_TYPE_OVERRIDE_1";
4999 let waiter = client
5000 .wait_for_sent_node(crate::client::NodeFilter::tag("message").attr("id", request_id));
5001 let msg =
5002 pdo_request_message(wa::message::PeerDataOperationRequestType::HistorySyncOnDemand);
5003
5004 let result = client
5006 .send_message_impl(
5007 peer,
5008 &msg,
5009 SendPipelineOptions {
5010 request_id: Some(request_id),
5011 peer: true,
5012 stanza_type: Some(StanzaType::Poll),
5013 ..Default::default()
5014 },
5015 )
5016 .await;
5017 assert!(
5018 result.is_err(),
5019 "test client has no socket; send should fail after stanza capture"
5020 );
5021
5022 let node = tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
5023 .await
5024 .expect("sent node should be captured")
5025 .expect("sent node waiter should resolve");
5026 assert_eq!(
5027 node.attrs().optional_string("type").unwrap().as_ref(),
5028 StanzaType::Poll.as_wire()
5029 );
5030 }
5031
5032 async fn seed_dm_wire_namespace_state(client: &Arc<Client>) -> (Jid, Jid) {
5038 use wacore::libsignal::protocol::{
5039 IdentityKeyPair, KeyPair, PreKeyBundle, SignalProtocolError, UsePQRatchet,
5040 process_prekey_bundle,
5041 };
5042
5043 let own_pn: Jid = "111111111111@s.whatsapp.net".parse().unwrap();
5045 let own_lid: Jid = "222222222222@lid".parse().unwrap();
5046 client
5047 .persistence_manager
5048 .process_command(DeviceCommand::SetId(Some(own_pn.clone())))
5049 .await;
5050 client
5051 .persistence_manager
5052 .process_command(DeviceCommand::SetLid(Some(own_lid)))
5053 .await;
5054 client
5055 .persistence_manager
5056 .process_command(DeviceCommand::SetAccount(Some(peer_test_account_proto())))
5057 .await;
5058
5059 let peer_pn: Jid = "100000000000777@s.whatsapp.net".parse().unwrap();
5062 let peer_lid: Jid = "555000000000777@lid".parse().unwrap();
5063 client
5064 .add_lid_pn_mapping(
5065 peer_lid.user.as_str(),
5066 peer_pn.user.as_str(),
5067 crate::lid_pn_cache::LearningSource::Usync,
5068 )
5069 .await
5070 .expect("seed lid mapping");
5071
5072 for user in [
5077 peer_lid.user.to_string(),
5078 peer_pn.user.to_string(),
5079 own_pn.user.to_string(),
5080 ] {
5081 client
5082 .update_device_list(wacore::store::traits::DeviceListRecord {
5083 user,
5084 devices: vec![wacore::store::traits::DeviceInfo::new(0, None)],
5085 timestamp: wacore::time::now_secs(),
5086 phash: None,
5087 raw_id: None,
5088 })
5089 .await
5090 .expect("seed device registry");
5091 }
5092
5093 client.enter_live_mode_for_tests();
5098
5099 let lid_addr = peer_lid.to_non_ad();
5104 let bundle =
5105 tokio::task::spawn_blocking(|| -> Result<PreKeyBundle, SignalProtocolError> {
5106 let mut rng = rand::make_rng::<rand::rngs::StdRng>();
5107 let receiver = IdentityKeyPair::generate(&mut rng);
5108 let spk = KeyPair::generate(&mut rng);
5109 let opk = KeyPair::generate(&mut rng);
5110 let sig = receiver
5111 .private_key()
5112 .calculate_signature(&spk.public_key.serialize(), &mut rng)?;
5113 PreKeyBundle::new(
5114 1,
5115 1u32.into(),
5116 Some((1u32.into(), opk.public_key)),
5117 1u32.into(),
5118 spk.public_key,
5119 sig.to_vec(),
5120 *receiver.identity_key(),
5121 )
5122 })
5123 .await
5124 .expect("prekey bundle task")
5125 .expect("prekey bundle");
5126 {
5127 let mut adapter = client.signal_adapter().await;
5128 let mut rng = rand::make_rng::<rand::rngs::StdRng>();
5129 process_prekey_bundle(
5130 &lid_addr.to_protocol_address(),
5131 &mut adapter.session_store,
5132 &mut adapter.identity_store,
5133 &bundle,
5134 &mut rng,
5135 UsePQRatchet::No,
5136 )
5137 .await
5138 .expect("peer lid session");
5139 }
5140
5141 (peer_pn, peer_lid)
5142 }
5143
5144 #[tokio::test]
5152 async fn dm_to_lid_mapped_peer_addresses_outer_to_by_lid() {
5153 let client = crate::test_utils::create_test_client_with_name("lid_dm_to").await;
5154 let (peer_pn, peer_lid) = seed_dm_wire_namespace_state(&client).await;
5155
5156 client
5158 .persistence_manager
5159 .process_command(DeviceCommand::SetLidMigrated(true))
5160 .await;
5161
5162 let request_id = "LID_DM_TO_1";
5163 let waiter = client
5164 .wait_for_sent_node(crate::client::NodeFilter::tag("message").attr("id", request_id));
5165 let msg = wa::Message {
5166 conversation: Some("hi".into()),
5167 ..Default::default()
5168 };
5169 let result = client
5171 .send_message_impl(
5172 peer_pn,
5173 &msg,
5174 SendPipelineOptions {
5175 request_id: Some(request_id),
5176 ..Default::default()
5177 },
5178 )
5179 .await;
5180 assert!(
5181 result.is_err(),
5182 "test client has no socket; send captures the stanza then errors"
5183 );
5184
5185 let node = tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
5186 .await
5187 .expect("sent node should be captured")
5188 .expect("sent node waiter should resolve");
5189
5190 let to_str = node
5192 .attrs()
5193 .optional_string("to")
5194 .expect("message has a to")
5195 .into_owned();
5196 let to_jid: Jid = to_str.parse().expect("to parses");
5197 assert!(
5198 to_jid.is_lid(),
5199 "outer <message to> must be LID to match the LID participants, got {to_str}"
5200 );
5201 assert_eq!(
5202 to_jid.user.as_str(),
5203 peer_lid.user.as_str(),
5204 "outer to user must be the peer LID"
5205 );
5206
5207 let participants = node
5209 .get_optional_child("participants")
5210 .expect("stanza has participants");
5211 let entries = participants.children().expect("participants has children");
5212 assert!(
5213 !entries.is_empty(),
5214 "fanout must target at least the recipient"
5215 );
5216 for entry in entries {
5217 let pj: Jid = entry
5218 .attrs()
5219 .optional_string("jid")
5220 .expect("participant jid")
5221 .parse()
5222 .expect("participant jid parses");
5223 assert!(
5224 pj.is_lid(),
5225 "participant {pj} must be LID (uniform namespace)"
5226 );
5227 }
5228 }
5229
5230 #[tokio::test]
5235 async fn dm_from_unmigrated_account_addresses_outer_to_by_pn() {
5236 let client = crate::test_utils::create_test_client_with_name("pn_dm_to").await;
5237 let (peer_pn, _peer_lid) = seed_dm_wire_namespace_state(&client).await;
5238
5239 let request_id = "PN_DM_TO_1";
5240 let waiter = client
5241 .wait_for_sent_node(crate::client::NodeFilter::tag("message").attr("id", request_id));
5242 let msg = wa::Message {
5243 conversation: Some("hi".into()),
5244 ..Default::default()
5245 };
5246 let result = client
5247 .send_message_impl(
5248 peer_pn.clone(),
5249 &msg,
5250 SendPipelineOptions {
5251 request_id: Some(request_id),
5252 ..Default::default()
5253 },
5254 )
5255 .await;
5256 assert!(
5257 result.is_err(),
5258 "test client has no socket; send captures the stanza then errors"
5259 );
5260
5261 let node = tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
5262 .await
5263 .expect("sent node should be captured")
5264 .expect("sent node waiter should resolve");
5265
5266 let to_str = node
5267 .attrs()
5268 .optional_string("to")
5269 .expect("message has a to")
5270 .into_owned();
5271 let to_jid: Jid = to_str.parse().expect("to parses");
5272 assert!(
5273 to_jid.is_pn(),
5274 "outer <message to> must stay PN on an unmigrated account, got {to_str}"
5275 );
5276 assert_eq!(
5277 to_jid.user.as_str(),
5278 peer_pn.user.as_str(),
5279 "outer to user must be the peer PN"
5280 );
5281
5282 let participants = node
5284 .get_optional_child("participants")
5285 .expect("stanza has participants");
5286 let entries = participants.children().expect("participants has children");
5287 assert!(
5288 !entries.is_empty(),
5289 "fanout must target at least the recipient"
5290 );
5291 for entry in entries {
5292 let pj: Jid = entry
5293 .attrs()
5294 .optional_string("jid")
5295 .expect("participant jid")
5296 .parse()
5297 .expect("participant jid parses");
5298 assert!(
5299 pj.is_pn(),
5300 "participant {pj} must be PN (uniform namespace)"
5301 );
5302 }
5303 }
5304
5305 #[tokio::test]
5308 async fn newsletter_jid_rejected_on_e2e_send_path() {
5309 let client = crate::test_utils::create_test_client_with_name("newsletter_e2e_guard").await;
5310 let channel: Jid = "120363000000000001@newsletter".parse().unwrap();
5311 let msg = wa::Message {
5312 conversation: Some("x".to_string()),
5313 ..Default::default()
5314 };
5315 let err = client
5316 .send_message_impl(channel, &msg, SendPipelineOptions::default())
5317 .await
5318 .expect_err("newsletter JID must be rejected on the E2E send path");
5319 assert!(
5320 err.to_string().to_lowercase().contains("newsletter"),
5321 "error should name the newsletter mis-route, got: {err}"
5322 );
5323 }
5324
5325 #[tokio::test]
5328 async fn pin_message_rejects_newsletter() {
5329 let client = crate::test_utils::create_test_client_with_name("newsletter_pin_guard").await;
5330 let channel: Jid = "120363000000000002@newsletter".parse().unwrap();
5331 let key = wa::MessageKey {
5332 remote_jid: Some(channel.to_string()),
5333 from_me: Some(true),
5334 id: Some("MID".to_string()),
5335 participant: None,
5336 };
5337 let err = client
5338 .pin_message(channel, key, PinDuration::Days7)
5339 .await
5340 .expect_err("pinning a newsletter message must be rejected");
5341 assert!(
5342 err.to_string().to_lowercase().contains("newsletter"),
5343 "error should name the newsletter mis-route, got: {err}"
5344 );
5345 }
5346
5347 #[test]
5351 fn build_newsletter_edit_node_emits_plaintext_edit() {
5352 use buffa::Message as _;
5353 let to: Jid = "120363000000000001@newsletter".parse().unwrap();
5354 let content = wa::Message {
5355 conversation: Some("edited text".to_string()),
5356 ..Default::default()
5357 };
5358 let node =
5359 build_newsletter_edit_node(&to, "3EB0EDITTARGET", NewsletterEdit::Edit(&content));
5360
5361 let mut a = node.attrs();
5362 assert_eq!(a.optional_string("id").unwrap().as_ref(), "3EB0EDITTARGET");
5363 assert_eq!(a.optional_string("type").unwrap().as_ref(), "text");
5364 assert_eq!(a.optional_string("edit").unwrap().as_ref(), "3");
5365
5366 let pt = node
5367 .get_optional_child("plaintext")
5368 .expect("plaintext child");
5369 assert!(
5370 pt.attrs().optional_string("mediatype").is_none(),
5371 "a text edit must not carry a mediatype attr"
5372 );
5373 let bytes = match pt.content.as_ref() {
5374 Some(wacore_binary::NodeContent::Bytes(b)) => b.clone(),
5375 other => panic!("expected plaintext bytes, got {other:?}"),
5376 };
5377 let decoded = wa::Message::decode_from_slice(bytes.as_slice()).expect("decode plaintext");
5378 assert_eq!(decoded.conversation.as_deref(), Some("edited text"));
5379 }
5380
5381 #[test]
5383 fn build_newsletter_edit_node_media_edit() {
5384 let to: Jid = "120363000000000001@newsletter".parse().unwrap();
5385 let content = wa::Message {
5386 image_message: buffa::MessageField::some(wa::message::ImageMessage {
5387 caption: Some("new caption".to_string()),
5388 ..Default::default()
5389 }),
5390 ..Default::default()
5391 };
5392 let node = build_newsletter_edit_node(&to, "3EB0MEDIA", NewsletterEdit::Edit(&content));
5393
5394 let mut a = node.attrs();
5395 assert_eq!(a.optional_string("id").unwrap().as_ref(), "3EB0MEDIA");
5396 assert_eq!(a.optional_string("type").unwrap().as_ref(), "media");
5397 assert_eq!(a.optional_string("edit").unwrap().as_ref(), "3");
5398 let pt = node
5399 .get_optional_child("plaintext")
5400 .expect("plaintext child");
5401 assert_eq!(
5402 pt.attrs().optional_string("mediatype").unwrap().as_ref(),
5403 "image"
5404 );
5405 }
5406
5407 #[test]
5410 fn build_newsletter_edit_node_revoke_is_empty_plaintext() {
5411 let to: Jid = "120363000000000002@newsletter".parse().unwrap();
5412 let node = build_newsletter_edit_node(&to, "3EB0REVOKETARGET", NewsletterEdit::Revoke);
5413
5414 let mut a = node.attrs();
5415 assert_eq!(
5416 a.optional_string("id").unwrap().as_ref(),
5417 "3EB0REVOKETARGET"
5418 );
5419 assert_eq!(a.optional_string("type").unwrap().as_ref(), "text");
5420 assert_eq!(a.optional_string("edit").unwrap().as_ref(), "8");
5421
5422 let pt = node
5423 .get_optional_child("plaintext")
5424 .expect("plaintext child");
5425 let empty = match pt.content.as_ref() {
5426 None => true,
5427 Some(wacore_binary::NodeContent::Bytes(b)) => b.is_empty(),
5428 _ => false,
5429 };
5430 assert!(empty, "revoke must carry an empty plaintext");
5431 }
5432
5433 #[tokio::test]
5436 async fn newsletter_edit_message_wrapper_sends_plaintext_edit() {
5437 let client = crate::test_utils::create_test_client_with_name("nl_edit_wrap").await;
5438 let channel: Jid = "120363000000000001@newsletter".parse().unwrap();
5439 let waiter =
5440 client.wait_for_sent_node(crate::client::NodeFilter::tag("message").attr("edit", "3"));
5441 let content = wa::Message {
5442 conversation: Some("edited".to_string()),
5443 ..Default::default()
5444 };
5445 let _ = client
5447 .newsletter()
5448 .edit_message(&channel, "TARGETMID", content)
5449 .await;
5450
5451 let node = tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
5452 .await
5453 .expect("sent node captured")
5454 .expect("waiter resolves");
5455 let mut a = node.attrs();
5456 assert_eq!(a.optional_string("id").unwrap().as_ref(), "TARGETMID");
5457 assert_eq!(a.optional_string("edit").unwrap().as_ref(), "3");
5458 }
5459
5460 #[tokio::test]
5463 async fn newsletter_edit_revoke_reject_non_newsletter_jid() {
5464 let client = crate::test_utils::create_test_client_with_name("nl_reject_nonchannel").await;
5465 let dm: Jid = "5511999999999@s.whatsapp.net".parse().unwrap();
5466 let group: Jid = "120363000000000009@g.us".parse().unwrap();
5467
5468 let e1 = client
5469 .newsletter()
5470 .edit_message(
5471 &dm,
5472 "MID",
5473 wa::Message {
5474 conversation: Some("x".to_string()),
5475 ..Default::default()
5476 },
5477 )
5478 .await
5479 .expect_err("edit_message must reject a DM JID");
5480 assert!(e1.to_string().to_lowercase().contains("newsletter"));
5481
5482 let e2 = client
5483 .newsletter()
5484 .revoke_message(&group, "MID")
5485 .await
5486 .expect_err("revoke_message must reject a group JID");
5487 assert!(e2.to_string().to_lowercase().contains("newsletter"));
5488 }
5489
5490 #[tokio::test]
5493 async fn newsletter_edit_revoke_reject_empty_message_id() {
5494 let client = crate::test_utils::create_test_client_with_name("nl_reject_empty_id").await;
5495 let channel: Jid = "120363000000000001@newsletter".parse().unwrap();
5496
5497 let e1 = client
5498 .newsletter()
5499 .edit_message(
5500 &channel,
5501 "",
5502 wa::Message {
5503 conversation: Some("x".to_string()),
5504 ..Default::default()
5505 },
5506 )
5507 .await
5508 .expect_err("edit_message must reject an empty message_id");
5509 assert!(e1.to_string().to_lowercase().contains("message_id"));
5510
5511 let e2 = client
5512 .newsletter()
5513 .revoke_message(&channel, "")
5514 .await
5515 .expect_err("revoke_message must reject an empty message_id");
5516 assert!(e2.to_string().to_lowercase().contains("message_id"));
5517 }
5518
5519 #[tokio::test]
5520 async fn persist_outbound_msg_secret_writes_under_chat_sender_id() {
5521 let client = crate::test_utils::create_test_client_with_name("secret_chat_id").await;
5522 seed_pn(&client, "5511000000001:0@s.whatsapp.net").await;
5523 let chat: Jid = "5511777776666@s.whatsapp.net".parse().unwrap();
5524 let sender: Jid = "5511000000001:0@s.whatsapp.net".parse().unwrap();
5525 let secret = [0x55u8; 32];
5526 client
5527 .persist_outbound_msg_secret(
5528 &chat,
5529 &sender,
5530 "MID_1",
5531 &secret,
5532 wacore::msg_secret::RetentionClass::Text,
5533 SendInstant::now(),
5534 )
5535 .await;
5536 client.msg_secret_buffer.wait_flushed().await;
5537 let got = client
5538 .persistence_manager
5539 .backend()
5540 .get_msg_secret(
5541 "5511777776666@s.whatsapp.net",
5542 "5511000000001@s.whatsapp.net",
5543 "MID_1",
5544 )
5545 .await
5546 .expect("get");
5547 assert_eq!(got.as_deref(), Some(&secret[..]));
5548 }
5549
5550 #[tokio::test]
5551 async fn persist_outbound_msg_secret_strips_devices_in_key() {
5552 let client = crate::test_utils::create_test_client_with_name("secret_strip").await;
5553 let chat_with_dev: Jid = "5511777776666:7@s.whatsapp.net".parse().unwrap();
5554 let sender_with_dev: Jid = "5511000000001:3@s.whatsapp.net".parse().unwrap();
5555 client
5556 .persist_outbound_msg_secret(
5557 &chat_with_dev,
5558 &sender_with_dev,
5559 "MID_4",
5560 &[2u8; 32],
5561 wacore::msg_secret::RetentionClass::Text,
5562 SendInstant::now(),
5563 )
5564 .await;
5565 client.msg_secret_buffer.wait_flushed().await;
5566 let got = client
5567 .persistence_manager
5568 .backend()
5569 .get_msg_secret(
5570 "5511777776666@s.whatsapp.net",
5571 "5511000000001@s.whatsapp.net",
5572 "MID_4",
5573 )
5574 .await
5575 .unwrap();
5576 assert_eq!(
5577 got.as_deref(),
5578 Some(&[2u8; 32][..]),
5579 "chat and sender must be stored non-AD"
5580 );
5581 }
5582
5583 #[tokio::test]
5584 async fn dm_sender_identity_picks_lid_for_bot_else_pn() {
5585 let client = crate::test_utils::create_test_client_with_name("dm_id_pick").await;
5586 seed_pn_and_lid(
5587 &client,
5588 "5511000000001:0@s.whatsapp.net",
5589 "999888777666555:0@lid",
5590 )
5591 .await;
5592 let bot_chat: Jid = "867051314767696@bot".parse().unwrap();
5593 let pn_chat: Jid = "5511777776666@s.whatsapp.net".parse().unwrap();
5594 let lid_chat: Jid = "111222333444555@lid".parse().unwrap();
5595 assert_eq!(
5596 client
5597 .dm_sender_identity_for(&bot_chat)
5598 .await
5599 .map(|j| j.to_non_ad_string()),
5600 Some("999888777666555@lid".to_string()),
5601 "bot chats must resolve to our LID"
5602 );
5603 assert_eq!(
5604 client
5605 .dm_sender_identity_for(&pn_chat)
5606 .await
5607 .map(|j| j.to_non_ad_string()),
5608 Some("5511000000001@s.whatsapp.net".to_string()),
5609 "PN chats must resolve to our PN"
5610 );
5611 assert_eq!(
5614 client
5615 .dm_sender_identity_for(&lid_chat)
5616 .await
5617 .map(|j| j.to_non_ad_string()),
5618 Some("5511000000001@s.whatsapp.net".to_string()),
5619 );
5620 }
5621
5622 #[tokio::test]
5626 async fn persist_uses_group_sender_identity_for_lid_mode_groups() {
5627 let client = crate::test_utils::create_test_client_with_name("secret_lid_group").await;
5628 seed_pn_and_lid(
5629 &client,
5630 "5511000000001:0@s.whatsapp.net",
5631 "999888777666555:0@lid",
5632 )
5633 .await;
5634 let group_chat: Jid = "120363021033254949@g.us".parse().unwrap();
5636 let lid_sender: Jid = "999888777666555:0@lid".parse().unwrap();
5637 let secret = [0x4Du8; 32];
5638 client
5639 .persist_outbound_msg_secret(
5640 &group_chat,
5641 &lid_sender,
5642 "GROUP_MID",
5643 &secret,
5644 wacore::msg_secret::RetentionClass::Text,
5645 SendInstant::now(),
5646 )
5647 .await;
5648 client.msg_secret_buffer.wait_flushed().await;
5649 let got = client
5650 .persistence_manager
5651 .backend()
5652 .get_msg_secret(
5653 "120363021033254949@g.us",
5654 "999888777666555@lid",
5655 "GROUP_MID",
5656 )
5657 .await
5658 .unwrap();
5659 assert_eq!(
5660 got.as_deref(),
5661 Some(&secret[..]),
5662 "LID-mode group secrets must key under our LID, not PN"
5663 );
5664 let under_pn = client
5665 .persistence_manager
5666 .backend()
5667 .get_msg_secret(
5668 "120363021033254949@g.us",
5669 "5511000000001@s.whatsapp.net",
5670 "GROUP_MID",
5671 )
5672 .await
5673 .unwrap();
5674 assert!(
5675 under_pn.is_none(),
5676 "LID-mode group must NOT key under our PN"
5677 );
5678 }
5679
5680 #[test]
5686 fn prepared_dm_stanza_exposes_generated_message_secret() {
5687 use wacore::reporting_token::generate_reporting_token;
5688
5689 let msg = wa::Message {
5690 conversation: Some("hi bot".into()),
5691 ..Default::default()
5692 };
5693 let to: Jid = "867051314767696@bot".parse().unwrap();
5694 let result = generate_reporting_token(&msg, "MID_X", &to, &to, None);
5695 assert!(
5696 result.is_some(),
5697 "ordinary text messages must produce a reporting token + secret"
5698 );
5699 let result = result.unwrap();
5700 assert_eq!(result.message_secret.len(), 32);
5701 let prepared = wacore::send::PreparedDmStanza {
5704 node: NodeBuilder::new("message").build(),
5705 phash: None,
5706 message_secret: Some(result.message_secret),
5707 };
5708 assert_eq!(prepared.message_secret.as_ref().unwrap().len(), 32);
5709 }
5710
5711 #[tokio::test]
5717 async fn one_id_names_the_stanza_the_waiter_the_secret_and_the_result() {
5718 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
5719 let (peer_pn, _peer_lid) = seed_dm_wire_namespace_state(&client).await;
5720
5721 let message_id = "ID_ünïcødé_✅_ONE";
5722 let result = client
5723 .send_message_with_options(
5724 peer_pn.clone(),
5725 wa::Message {
5726 conversation: Some("hi".into()),
5727 ..Default::default()
5728 },
5729 SendOptions::default().with_message_id(message_id),
5730 )
5731 .await
5732 .expect("connected test client should complete the send");
5733
5734 assert_eq!(
5735 result.message_id, message_id,
5736 "result carries the caller id"
5737 );
5738 assert_eq!(result.to, peer_pn, "result carries the caller target");
5739
5740 let waiters = client.response_waiters_guard();
5741 assert!(
5742 waiters.contains_key(message_id),
5743 "the phash ack-waiter must be keyed by the send's own id"
5744 );
5745 assert_eq!(waiters.len(), 1, "no second entry under another spelling");
5746 drop(waiters);
5747
5748 let secret = client.msg_secret_buffer.lookup(
5749 &peer_pn.to_non_ad_string(),
5750 &client.pn().expect("own pn").to_non_ad_string(),
5751 message_id,
5752 );
5753 assert!(
5754 secret.is_some(),
5755 "the outbound messageSecret must be bound to the same id"
5756 );
5757 }
5758
5759 #[tokio::test]
5764 async fn a_failed_send_takes_its_phash_waiter_back_out() {
5765 let client = crate::test_utils::create_test_client_with_name("phash_waiter_rollback").await;
5766 let (peer_pn, _peer_lid) = seed_dm_wire_namespace_state(&client).await;
5767
5768 let message_id = "ID_ünïcødé_✅_ROLLBACK";
5769 let result = client
5770 .send_message_impl(
5771 peer_pn,
5772 &wa::Message {
5773 conversation: Some("hi".into()),
5774 ..Default::default()
5775 },
5776 SendPipelineOptions {
5777 request_id: Some(message_id),
5778 ..Default::default()
5779 },
5780 )
5781 .await;
5782 assert!(result.is_err(), "no socket: the send must fail on the wire");
5783 assert_eq!(
5784 client.response_waiters_guard().len(),
5785 0,
5786 "a failed send must leave no waiter behind, under any key"
5787 );
5788 }
5789
5790 #[tokio::test]
5794 async fn a_borrowed_id_registers_no_waiter_and_binds_no_secret() {
5795 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
5796 let (peer_pn, _peer_lid) = seed_dm_wire_namespace_state(&client).await;
5797
5798 let message_id = "ID_BORROWED_1";
5799 client
5800 .send_message_impl(
5801 peer_pn.clone(),
5802 &wa::Message {
5803 conversation: Some("hi".into()),
5804 ..Default::default()
5805 },
5806 SendPipelineOptions {
5807 request_id: Some(message_id),
5808 borrowed_message_id: true,
5809 ..Default::default()
5810 },
5811 )
5812 .await
5813 .expect("connected test client should complete the send");
5814
5815 assert_eq!(
5816 client.response_waiters_guard().len(),
5817 0,
5818 "a borrowed id must not claim the waiter slot"
5819 );
5820 assert!(
5821 client
5822 .msg_secret_buffer
5823 .lookup(
5824 &peer_pn.to_non_ad_string(),
5825 &client.pn().expect("own pn").to_non_ad_string(),
5826 message_id,
5827 )
5828 .is_none(),
5829 "a borrowed id must not claim the secret slot"
5830 );
5831 }
5832
5833 #[tokio::test]
5836 async fn an_empty_id_is_refused_at_both_entry_points() {
5837 let client = crate::test_utils::create_test_client_with_name("empty_send_id").await;
5838 let peer: Jid = "100000000000777@s.whatsapp.net".parse().unwrap();
5839 let msg = wa::Message {
5840 conversation: Some("hi".into()),
5841 ..Default::default()
5842 };
5843
5844 let public = client
5845 .send_message_with_options(
5846 peer.clone(),
5847 msg.clone(),
5848 SendOptions::default().with_message_id(""),
5849 )
5850 .await;
5851 assert!(
5852 matches!(public, Err(SendError::InvalidRequest(_))),
5853 "public send must reject an empty id, got {public:?}"
5854 );
5855
5856 let internal = client
5857 .send_message_impl(
5858 peer,
5859 &msg,
5860 SendPipelineOptions {
5861 request_id: Some(""),
5862 ..Default::default()
5863 },
5864 )
5865 .await;
5866 let internal = internal.expect_err("internal send must reject an empty id");
5867 assert!(
5868 internal
5869 .to_string()
5870 .contains("message ID must not be empty"),
5871 "unexpected error: {internal}"
5872 );
5873 }
5874
5875 #[tokio::test]
5879 async fn the_newsletter_branch_returns_the_id_and_target_it_stamped() {
5880 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
5881 let channel: Jid = "123456789@newsletter".parse().unwrap();
5882
5883 let message_id = "ID_ünïcødé_✅_NEWS";
5884 let waiter = client
5885 .wait_for_sent_node(crate::client::NodeFilter::tag("message").attr("id", message_id));
5886 let result = client
5887 .send_message_with_options(
5888 channel.clone(),
5889 wa::Message {
5890 conversation: Some("hi".into()),
5891 ..Default::default()
5892 },
5893 SendOptions::default().with_message_id(message_id),
5894 )
5895 .await
5896 .expect("newsletter send is plaintext and needs no session");
5897
5898 assert_eq!(result.message_id, message_id);
5899 assert_eq!(result.to, channel);
5900
5901 let node = waiter.await.expect("the stanza should be captured");
5902 assert_eq!(
5903 node.attrs().optional_string("id").as_deref(),
5904 Some(message_id),
5905 "the wire id must be the same one the result reports"
5906 );
5907 }
5908}
5909
5910#[cfg(test)]
5911mod jid_into_convention {
5912 use super::*;
5913
5914 #[allow(dead_code)]
5918 async fn both_call_styles_compile(client: &Client, jid: Jid) {
5919 let msg = wa::Message::default();
5920 let _ = client.send_message(&jid, msg.clone()).await;
5921 let _ = client
5922 .send_message_with_options(&jid, msg.clone(), SendOptions::default())
5923 .await;
5924 let _ = client.forward_message(&jid, &msg).await;
5925 let _ = client
5926 .edit_message(&jid, "ID", wa::Message::default())
5927 .await;
5928 let _ = client.revoke_message(&jid, "ID", RevokeType::Sender).await;
5929 let _ = client
5930 .pin_message(&jid, wa::MessageKey::default(), PinDuration::default())
5931 .await;
5932 let _ = client.unpin_message(&jid, wa::MessageKey::default()).await;
5933 let _ = client
5934 .send_reaction(&jid, wa::MessageKey::default(), "x")
5935 .await;
5936 let _ = client
5937 .keep_message(&jid, wa::MessageKey::default(), true)
5938 .await;
5939 let _ = client.send_message(jid.clone(), msg.clone()).await;
5942 let _ = client
5943 .send_message_with_options(jid.clone(), msg.clone(), SendOptions::default())
5944 .await;
5945 let _ = client.forward_message(jid.clone(), &msg).await;
5946 let _ = client
5947 .edit_message(jid.clone(), "ID", wa::Message::default())
5948 .await;
5949 let _ = client
5950 .revoke_message(jid.clone(), "ID", RevokeType::Sender)
5951 .await;
5952 let _ = client
5953 .pin_message(
5954 jid.clone(),
5955 wa::MessageKey::default(),
5956 PinDuration::default(),
5957 )
5958 .await;
5959 let _ = client
5960 .unpin_message(jid.clone(), wa::MessageKey::default())
5961 .await;
5962 let _ = client
5963 .send_reaction(jid.clone(), wa::MessageKey::default(), "x")
5964 .await;
5965 let _ = client
5966 .keep_message(jid, wa::MessageKey::default(), true)
5967 .await;
5968 }
5969}
5970
5971#[cfg(test)]
5972mod future_size_tests {
5973 #[tokio::test]
5977 async fn send_futures_stay_small() {
5978 let client = crate::test_utils::create_test_client().await;
5979 let jid: wacore_binary::jid::Jid = "5511999990000@s.whatsapp.net".parse().unwrap();
5980 let msg = waproto::whatsapp::Message::default();
5981
5982 let f = client.send_message(jid.clone(), msg.clone());
5983 assert!(size_of_val(&f) <= 192, "send_message future grew");
5984 drop(f);
5985 let f = client.send_text(jid.clone(), "x");
5986 assert!(size_of_val(&f) <= 192, "send_text future grew");
5987 drop(f);
5988 let f = client.forward_message(jid.clone(), &msg);
5989 assert!(size_of_val(&f) <= 192, "forward_message future grew");
5990 drop(f);
5991 let f = client.send_message_with_options(jid, msg, Default::default());
5992 assert!(
5993 size_of_val(&f) <= 192,
5994 "send_message_with_options future grew"
5995 );
5996 drop(f);
5997 }
5998}
5999
6000#[cfg(test)]
6001mod clock_budget_tests {
6002 use super::*;
6003 use crate::store::commands::DeviceCommand;
6004 use std::sync::Arc;
6005 use wacore::time::clock_reads;
6006
6007 const OWN_PN: &str = "15551234001";
6008 const PEER_PN: &str = "5511900000001";
6009 const PEER_LID: &str = "100000000000079";
6010
6011 const SEND_WALL_READS: u64 = 1;
6015 const SEND_MONOTONIC_READS: u64 = 2;
6016
6017 async fn seed_devices(client: &Arc<Client>, user: &str) {
6018 client
6019 .update_device_list(wacore::store::traits::DeviceListRecord {
6020 user: user.into(),
6021 devices: vec![wacore::store::traits::DeviceInfo::new(0, None)],
6022 timestamp: wacore::time::now_secs(),
6023 phash: None,
6024 raw_id: None,
6025 })
6026 .await
6027 .expect("seed device list");
6028 }
6029
6030 async fn cold_send_client() -> (
6033 Arc<Client>,
6034 Arc<crate::transport::mock::CapturingMockTransport>,
6035 Jid,
6036 ) {
6037 let (client, transport) = crate::test_utils::create_iq_test_client().await;
6038 client
6039 .persistence_manager
6040 .process_command(DeviceCommand::SetId(Some(
6041 format!("{OWN_PN}@s.whatsapp.net").parse().expect("own pn"),
6042 )))
6043 .await;
6044 client
6045 .persistence_manager
6046 .process_command(DeviceCommand::SetLid(Some(
6047 "100000000000001@lid".parse().expect("own lid"),
6048 )))
6049 .await;
6050
6051 let peer = Jid::pn(PEER_PN);
6052 seed_devices(&client, PEER_PN).await;
6053 seed_devices(&client, OWN_PN).await;
6054 seed_devices(&client, PEER_LID).await;
6055 client
6056 .add_lid_pn_mapping(
6057 PEER_LID,
6058 PEER_PN,
6059 crate::lid_pn_cache::LearningSource::Usync,
6060 )
6061 .await
6062 .expect("lid mapping");
6063 crate::test_utils::seed_peer_session(&client, &peer).await;
6064 crate::test_utils::seed_peer_session(
6065 &client,
6066 &format!("{PEER_LID}@lid").parse().expect("lid jid"),
6067 )
6068 .await;
6069
6070 (client, transport, peer)
6071 }
6072
6073 async fn warm_send_client() -> (
6076 Arc<Client>,
6077 Arc<crate::transport::mock::CapturingMockTransport>,
6078 Jid,
6079 ) {
6080 let (client, transport, peer) = cold_send_client().await;
6081 client.send_text(peer.clone(), "warm").await.expect("warm");
6082 crate::test_utils::poll_until("the privacy token to be issued", || {
6085 transport.sent_count() >= 2
6086 })
6087 .await;
6088 crate::test_utils::wait_for_outbound_tasks(&client).await;
6089 (client, transport, peer)
6090 }
6091
6092 #[tokio::test(start_paused = true)]
6095 async fn dm_send_stays_within_its_clock_budget() {
6096 let (client, transport, peer) = warm_send_client().await;
6097 let frames_before = transport.sent_count();
6098
6099 let base = clock_reads::snapshot();
6100 client.send_text(peer, "hello").await.expect("send");
6101 let reads = clock_reads::since(base);
6102
6103 assert_eq!(
6104 transport.sent_count() - frames_before,
6105 1,
6106 "the budget only describes a send that writes exactly one frame"
6107 );
6108 assert!(
6109 reads.total() > 0,
6110 "a zero count means the flow did not run, not that it got free"
6111 );
6112 assert!(
6113 reads.wall <= SEND_WALL_READS,
6114 "wall-clock reads per DM send rose to {} (budget {SEND_WALL_READS})",
6115 reads.wall
6116 );
6117 assert!(
6118 reads.monotonic <= SEND_MONOTONIC_READS,
6119 "monotonic reads per DM send rose to {} (budget {SEND_MONOTONIC_READS})",
6120 reads.monotonic
6121 );
6122
6123 client.disconnect().await;
6124 }
6125
6126 #[tokio::test(start_paused = true)]
6130 async fn a_send_reads_the_clock_once() {
6131 let (client, _transport, peer) = warm_send_client().await;
6132
6133 let base = clock_reads::snapshot();
6134 let sent = client.send_text(peer.clone(), "hello").await.expect("send");
6135 assert_eq!(
6136 clock_reads::since(base).wall,
6137 1,
6138 "every stamp on the send path must come from the same read"
6139 );
6140
6141 client.msg_secret_buffer.wait_flushed().await;
6144 let stored = client
6145 .persistence_manager
6146 .backend()
6147 .get_msg_secret(
6148 &peer.to_non_ad_string(),
6149 &format!("{OWN_PN}@s.whatsapp.net"),
6150 &sent.message_id,
6151 )
6152 .await
6153 .expect("msg secret lookup");
6154 assert!(
6155 stored.is_some(),
6156 "the send persisted an outbound secret under the measured instant"
6157 );
6158
6159 client.disconnect().await;
6160 }
6161
6162 #[tokio::test(start_paused = true)]
6165 async fn wire_timestamp_keeps_real_time() {
6166 let (client, transport, peer) = cold_send_client().await;
6167
6168 let before = wacore::time::now_secs();
6169 client.send_text(peer, "hello").await.expect("send");
6170 crate::test_utils::poll_until("the privacy token to be issued", || {
6171 transport.sent_count() >= 2
6172 })
6173 .await;
6174 let after = wacore::time::now_secs();
6175
6176 let mut seen = None;
6177 for index in 0..transport.sent_count() {
6178 let node = crate::test_utils::decode_sent_iq(&transport, index).await;
6179 let node = node.get();
6180 if node.attrs().optional_string("xmlns").as_deref() != Some("privacy") {
6181 continue;
6182 }
6183 let token = node
6184 .get_optional_child("tokens")
6185 .and_then(|t| t.get_optional_child("token"))
6186 .expect("the privacy IQ carries a <token>");
6187 seen = token
6188 .attrs()
6189 .optional_string("t")
6190 .and_then(|t| t.parse::<i64>().ok());
6191 break;
6192 }
6193
6194 let stamped = seen.expect("a first send issues a privacy token");
6195 assert!(
6196 (before..=after).contains(&stamped),
6197 "wire timestamp {stamped} outside [{before}, {after}]"
6198 );
6199
6200 client.disconnect().await;
6201 }
6202}