Skip to main content

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}