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