whatsapp_rust/features/reaction.rs
1//! Sending reactions to DM/group/status messages.
2//!
3//! Newsletter reactions go through a different (plaintext) wire path; use
4//! [`Client::newsletter`]'s `send_reaction` for channels.
5//!
6//! Community Announcement Groups never accept plaintext reactions: WA Web
7//! (`WAWebReactionEncryptMsgData`) encrypts the reaction with the target
8//! message's `messageSecret` and emits an `enc_reaction_message` envelope.
9//! [`Client::send_reaction`] applies the same gate transparently.
10
11use buffa::MessageField;
12use wacore_binary::{Jid, JidExt};
13use waproto::whatsapp as wa;
14
15use crate::client::Client;
16use crate::send::{SendError, SendResult};
17
18impl Client {
19 /// React to a DM, group, or status@broadcast message.
20 ///
21 /// `target_key` references the message being reacted to. For groups and
22 /// status it must carry `participant` (the original sender) so the receipt
23 /// can be attributed; [`crate::bot::MessageContext::react`] fills this in
24 /// from the incoming message. An empty `emoji` removes a previous reaction
25 /// (WA Web's empty-text reaction == sender-revoke).
26 ///
27 /// For a Community Announcement Group the reaction is encrypted with the
28 /// target's `messageSecret` (captured when the message was received) and
29 /// sent as `enc_reaction_message`; reacting to a message whose secret was
30 /// never captured fails rather than emitting a plaintext reaction the
31 /// channel would reject.
32 ///
33 /// status@broadcast reactions fan out to the status author's devices; the
34 /// author is read from `target_key.participant` by the send path.
35 pub async fn send_reaction(
36 &self,
37 chat: impl Into<Jid>,
38 target_key: wa::MessageKey,
39 emoji: &str,
40 ) -> Result<SendResult, SendError> {
41 let chat = &chat.into();
42 if chat.is_group() && self.is_community_announce_group(chat).await? {
43 return self.send_enc_reaction(chat, target_key, emoji).await;
44 }
45 let reaction = wacore::proto_helpers::build_reaction_message(
46 target_key,
47 emoji,
48 wacore::time::now_millis(),
49 );
50 self.send_message(chat, reaction).await
51 }
52
53 /// Whether `chat` is a Community Announcement Group (WA Web `isCag`).
54 ///
55 /// Served from the cached/persisted group metadata; a blob persisted
56 /// before the flag existed answers `None` and falls back to one full
57 /// metadata query.
58 pub(crate) async fn is_community_announce_group(
59 &self,
60 chat: &Jid,
61 ) -> Result<bool, crate::features::GroupError> {
62 if let Some(flag) = self.groups().query_info(chat).await?.is_community_announce {
63 return Ok(flag);
64 }
65 Ok(self.groups().get_metadata(chat).await?.is_default_sub_group)
66 }
67
68 async fn send_enc_reaction(
69 &self,
70 chat: &Jid,
71 mut target_key: wa::MessageKey,
72 emoji: &str,
73 ) -> Result<SendResult, SendError> {
74 let (author, secret) = self
75 .resolve_outgoing_addon_parent(chat, &target_key)
76 .await?;
77 let target_id = target_key
78 .id
79 .clone()
80 .ok_or_else(|| SendError::InvalidRequest("target message key missing id".into()))?;
81 // Receivers derive the addon key with the STANZA sender, which in a
82 // CAG is our LID identity regardless of the parent author's namespace;
83 // mirror the comment path (WA Web authors CAG addons under LID).
84 let reactor = self
85 .lid()
86 .or_else(|| self.pn())
87 .map(|j| j.to_non_ad())
88 .ok_or(SendError::NotLoggedIn)?;
89
90 let (enc_payload, iv) = wacore::reaction::encrypt_reaction_with_secret(
91 emoji,
92 wacore::time::now_millis(),
93 &secret,
94 &target_id,
95 &author.to_non_ad_string(),
96 &reactor.to_non_ad_string(),
97 )?;
98
99 // Receivers resolve the parent author from the envelope key, so it
100 // must carry the same identity the HKDF was derived with.
101 if target_key.participant.is_none() {
102 target_key.participant = Some(author.to_non_ad_string());
103 }
104
105 let message = wa::Message {
106 enc_reaction_message: MessageField::some(wa::message::EncReactionMessage {
107 target_message_key: MessageField::some(target_key),
108 enc_payload: Some(enc_payload),
109 enc_iv: Some(iv.to_vec()),
110 }),
111 ..Default::default()
112 };
113 self.send_message(chat, message).await
114 }
115}