Skip to main content

whatsapp_rust/features/
comments.rs

1//! Encrypted channel comments (threaded replies under a Community
2//! Announcement Group post).
3//!
4//! Mirrors WA Web `WAWebSendCommentMessageAction`: the comment body is a
5//! regular `Message` (extended text), encrypted with the parent post's
6//! `messageSecret` under the `"Enc Comment"` use-case, and shipped as a
7//! top-level `enc_comment_message` envelope. The comment carries its own
8//! fresh `messageSecret` so it can itself receive reactions.
9//!
10//! Incoming comments are decrypted transparently on the receive path and
11//! dispatched as their inner body `Message`; the parent post key surfaces on
12//! `MessageInfo::comment_target`.
13
14use wacore_binary::Jid;
15use waproto::whatsapp as wa;
16
17use crate::client::Client;
18use crate::send::{SendError, SendResult};
19
20pub struct Comments<'a> {
21    client: &'a Client,
22}
23
24impl<'a> Comments<'a> {
25    pub(crate) fn new(client: &'a Client) -> Self {
26        Self { client }
27    }
28
29    /// Comment on a channel post with a text body.
30    ///
31    /// `parent_key` references the post being commented on and must carry
32    /// `participant` (the post author) so receivers can key the decryption.
33    /// Requires the parent's `messageSecret` (captured when the post was
34    /// received).
35    pub async fn send_text(
36        &self,
37        chat: impl Into<Jid>,
38        parent_key: wa::MessageKey,
39        text: &str,
40    ) -> Result<SendResult, SendError> {
41        let chat = &chat.into();
42        // WA Web encryptExtendedTextComment: the body is an extendedTextMessage.
43        let body = wa::Message {
44            extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
45                text: Some(text.to_string()),
46                ..Default::default()
47            }),
48            ..Default::default()
49        };
50        self.send_message(chat, parent_key, body).await
51    }
52
53    /// Comment on a channel post with an arbitrary body `Message`.
54    pub async fn send_message(
55        &self,
56        chat: impl Into<Jid>,
57        mut parent_key: wa::MessageKey,
58        body: wa::Message,
59    ) -> Result<SendResult, SendError> {
60        let chat = &chat.into();
61        let client = self.client;
62        let (author, secret) = client
63            .resolve_outgoing_addon_parent(chat, &parent_key)
64            .await?;
65        let parent_id = parent_key
66            .id
67            .clone()
68            .ok_or_else(|| SendError::InvalidRequest("parent message key missing id".into()))?;
69        // WA Web comments are authored under the LID identity
70        // (getMeLidUserOrThrow); fall back to PN only when no LID is known.
71        let commenter = client
72            .lid()
73            .or_else(|| client.pn())
74            .map(|j| j.to_non_ad())
75            .ok_or(SendError::NotLoggedIn)?;
76
77        let (enc_payload, iv) = wacore::comment::encrypt_comment_with_secret(
78            &body,
79            &secret,
80            &parent_id,
81            &author.to_non_ad_string(),
82            &commenter.to_non_ad_string(),
83        )?;
84
85        // Receivers resolve the parent author from the envelope key, so it
86        // must carry the same identity the HKDF was derived with.
87        if parent_key.participant.is_none() {
88            parent_key.participant = Some(author.to_non_ad_string());
89        }
90
91        // Fresh secret so the comment can itself receive encrypted add-ons.
92        let comment_secret: [u8; 32] = {
93            use rand::Rng;
94            let mut secret = [0u8; 32];
95            rand::rng().fill_bytes(&mut secret);
96            secret
97        };
98
99        let message = wa::Message {
100            enc_comment_message: buffa::MessageField::some(wa::message::EncCommentMessage {
101                target_message_key: buffa::MessageField::some(parent_key),
102                enc_payload: Some(enc_payload),
103                enc_iv: Some(iv.to_vec()),
104            }),
105            message_context_info: buffa::MessageField::some(wa::MessageContextInfo {
106                message_secret: Some(comment_secret.to_vec()),
107                ..Default::default()
108            }),
109            ..Default::default()
110        };
111        let result = client.send_message(chat, message).await?;
112
113        // The send path only persists reporting-token secrets, so store the
114        // comment's own secret here or we could never decrypt add-ons
115        // targeting our own comment.
116        client
117            .persist_outbound_msg_secret(
118                chat,
119                &commenter,
120                &result.message_id,
121                &comment_secret,
122                wacore::msg_secret::RetentionClass::Text,
123                crate::send::SendInstant::now(),
124            )
125            .await;
126        Ok(result)
127    }
128}
129
130impl Client {
131    pub fn comments(&self) -> Comments<'_> {
132        Comments::new(self)
133    }
134}