Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions changelog/2026-06-10-cag-enc-reactions-comments.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
title: "June 10, 2026 — Encrypted CAG reactions and channel comments"
description: "Reactions to Community Announcement Group posts are now encrypted automatically. A new comments() API lets you send and receive encrypted threaded replies on CAG channel posts."
---

## New features

**Encrypted reactions in Community Announcement Groups**

`Client::send_reaction` now transparently detects Community Announcement Groups (CAG — the default announcement subgroup of a community) and encrypts the reaction before sending. No API change is needed; the same call works for DMs, regular groups, and CAGs:

```rust
// Works identically for DMs, regular groups, and CAGs.
client.send_reaction(&cag_jid, target_key, "🔥").await?;
```

For a CAG chat the library checks `GroupInfo::is_community_announce` (cached from group metadata). When true, the reaction emoji and timestamp are encrypted with the target post's `messageSecret` under the `"Enc Reaction"` HKDF use-case and shipped as an `enc_reaction_message` envelope — matching WA Web's `WAWebReactionEncryptMsgData` flow. If the parent secret was not captured (message received before session started, or `msg_secret_policy` disabled without a resolver) the call fails with a descriptive error rather than silently emitting a plaintext reaction the channel would reject.

Incoming encrypted reactions are decrypted transparently by the receive path and dispatched in the same plaintext `reaction_message` shape as an ordinary group reaction. The `key` field is filled from the envelope's `target_message_key` so event handlers require no changes.

---

**Channel comments via `client.comments()`**

A new `Comments` feature handle lets you post encrypted threaded replies under a CAG post. Comments require the parent post's `messageSecret` (captured during receive) and are authored under the LID identity, matching WA Web's `getMeLidUserOrThrow`:

```rust
let parent_key = wa::MessageKey {
remote_jid: Some(cag_jid.to_string()),
from_me: Some(false),
id: Some("3EB0POSTID".to_string()),
// Must be the post author so receivers can derive the HKDF key.
participant: Some(post_author_jid.to_string()),
};

// Text comment (extended_text_message body)
let result = client.comments()
.send_text(&cag_jid, parent_key.clone(), "Great post!")
.await?;

// Arbitrary message body
let body = wa::Message {
extended_text_message: Some(Box::new(wa::message::ExtendedTextMessage {
text: Some("Great post!".to_string()),
..Default::default()
})),
..Default::default()
};
let result = client.comments()
.send_message(&cag_jid, parent_key, body)
.await?;
```

The comment carries a fresh `messageSecret` of its own so it can receive encrypted reactions. Both the comment body secret and the parent-post secret are persisted under the correct ids.

Incoming encrypted comments are decrypted transparently. The decrypted body is dispatched as a normal `Event::Message`. The parent post key surfaces on the new `MessageInfo::comment_target` field, since the inner `Message` proto has no slot for the threading link:

```rust
Event::Message(msg, info) => {
if let Some(parent_key) = &info.comment_target {
println!("Comment on post: {:?}", parent_key.id);
if let Some(text) = msg.text_content() {
println!("Comment text: {}", text);
}
}
}
```

The comment's own `messageSecret` (from the outer envelope) is persisted under the comment's id and sender so that future encrypted reactions to the comment can be decrypted.

## Breaking changes

Pre-1.0, additive surface. The following structs and enums gain new fields/variants. If you construct them with exhaustive struct-literal syntax (without `..Default::default()`), add the new fields:

**`MessageInfo`** gains `comment_target: Option<wa::MessageKey>`:

```rust
// Add the field or use ..Default::default():
let info = MessageInfo {
id: "...".to_string(),
// ...
peer_recipient_pn: None,
comment_target: None, // new
bcl_participants: vec![],
};
```

**`GroupInfo`** gains `is_community_announce: Option<bool>`. Blobs persisted before this field was added will deserialize with `None` ("unknown") and trigger one metadata re-query when the value is first needed. No migration is required — existing serialized blobs remain valid.

**`SecretEncKind`** gains two variants that must be covered in exhaustive `match` arms:

- `SecretEncKind::EncReaction` — an `enc_reaction_message` envelope
- `SecretEncKind::EncComment` — an `enc_comment_message` envelope
1 change: 1 addition & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
"group": "Changelog",
"pages": [
"changelog/overview",
"changelog/2026-06-10-cag-enc-reactions-comments",
"changelog/2026-06-10-hrtb-closures",
"changelog/2026-06-10-server-aware-lookup-probe",
"changelog/2026-06-10-phash-arena-sort",
Expand Down
104 changes: 102 additions & 2 deletions guides/communities.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
---
title: Community management
description: Learn how to create communities, manage subgroups, and query community metadata in whatsapp-rust
description: Learn how to create communities, manage subgroups, query community metadata, send encrypted CAG reactions, and post channel comments in whatsapp-rust
---

## Overview

Communities are parent groups that contain linked subgroups. They use the `w:g2` IQ namespace for mutations and MEX (GraphQL) for metadata queries.

This guide covers creating communities, linking and unlinking subgroups, querying subgroup metadata, and identifying group types within a community hierarchy.
This guide covers creating communities, linking and unlinking subgroups, querying subgroup metadata, identifying group types, sending encrypted reactions to Community Announcement Groups, and posting channel comments.

## Accessing the Community API

Expand Down Expand Up @@ -224,6 +224,104 @@ The classification is based on these `GroupMetadata` fields:

See [Groups API reference](/api/groups#get_metadata) for all `GroupMetadata` fields.

## Community Announcement Group (CAG) reactions

The default announcement subgroup of a community — the one where `is_default_sub_group` is `true` — is a Community Announcement Group (CAG). CAGs require encrypted reactions; plaintext reactions are silently dropped by the server.

`client.send_reaction()` handles this transparently. The same call works for DMs, regular groups, and CAGs with no change to your code:

```rust
let target_key = wa::MessageKey {
remote_jid: Some(cag_jid.to_string()),
from_me: Some(false),
id: Some("3EB0POSTID".to_string()),
// participant must be the post author.
participant: Some(post_author_jid.to_string()),
};

// send_reaction takes target_key by value; clone it to reuse for removal.
client.send_reaction(&cag_jid, target_key.clone(), "🔥").await?;

// Remove a reaction with an empty emoji:
client.send_reaction(&cag_jid, target_key, "").await?;
```

For CAG chats the library checks `GroupInfo::is_community_announce` (populated from metadata and cached). When `true`, the reaction is encrypted with the target post's `messageSecret` (captured when the post was received) and shipped as an `enc_reaction_message` envelope. If the parent secret is not available the call fails with a descriptive error rather than emitting a plaintext reaction the channel would drop.

Incoming encrypted reactions from CAG posts are decrypted transparently by the receive path and surfaced as a normal `reaction_message` event. The `key` field is filled from the envelope's `target_message_key`, so your event handler looks identical to a regular group reaction:

```rust
Event::Message(msg, info) => {
if let Some(reaction) = &msg.reaction_message {
println!("Reaction: {:?}", reaction.text);
println!("On post: {:?}", reaction.key.as_ref().and_then(|k| k.id.as_deref()));
}
}
```

## Channel comments

Post encrypted threaded replies under a CAG post using `client.comments()`:

```rust
use whatsapp_rust::features::Comments;

let parent_key = wa::MessageKey {
remote_jid: Some(cag_jid.to_string()),
from_me: Some(false),
id: Some("3EB0POSTID".to_string()),
// participant must point to the post author so receivers can derive the
// decryption key from the envelope. The library resolves the author
// automatically when from_me is true and participant is omitted.
participant: Some(post_author_jid.to_string()),
};

// Send a text comment:
let result = client.comments()
.send_text(&cag_jid, parent_key.clone(), "Great post!")
.await?;
println!("Comment sent: {}", result.message_id);
```

For arbitrary message bodies, use `send_message`:

```rust
let body = wa::Message {
extended_text_message: Some(Box::new(wa::message::ExtendedTextMessage {
text: Some("Great post!".to_string()),
..Default::default()
})),
..Default::default()
};

let result = client.comments()
.send_message(&cag_jid, parent_key, body)
.await?;
```

Comments require the parent post's `messageSecret` to have been captured when the post was received (via `msg_secret_policy`). If it was not captured the call returns an error explaining that the secret is missing.

Each comment carries a fresh `messageSecret` of its own so it can receive encrypted reactions. The comment's secret is persisted under the comment's own id and sender.

### Receiving comments

Incoming encrypted comments are decrypted transparently on the receive path. The decrypted body is dispatched as a normal `Event::Message`. Because the inner `Message` proto has no slot for the parent post key, the threading link surfaces on `MessageInfo::comment_target`:

```rust
Event::Message(msg, info) => {
if let Some(parent_key) = &info.comment_target {
// This is a channel comment.
println!("Comment on post: {:?}", parent_key.id);

if let Some(text) = msg.text_content() {
println!("Comment text: {}", text);
}
}
}
```

`comment_target` is `None` for all other message types.

## Error handling

Community mutations return `anyhow::Error`, while MEX-based queries (`get_subgroups`, `get_subgroup_participant_counts`) return `MexError`:
Expand All @@ -244,5 +342,7 @@ match client.community().get_subgroups(&community_jid).await {

- [Community API reference](/api/community) — Full API details and types
- [Group management](/guides/group-management) — Manage individual groups
- [Sending messages](/guides/sending-messages#reactions) — Reactions API overview
- [Receiving messages](/guides/receiving-messages) — Handle message events including comments
- [Events](/concepts/events) — Handle group update events
- [MEX API](/api/mex) — Understand the GraphQL layer used by community queries
40 changes: 36 additions & 4 deletions guides/receiving-messages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,19 @@ if let Some(expiration) = info.ephemeral_expiration {
if let Some(request_id) = &info.unavailable_request_id {
println!("Recovered via PDO request: {}", request_id);
}

// Check if this is a decrypted channel comment (CAG threaded reply)
if let Some(parent_key) = &info.comment_target {
println!("Comment on post: {:?}", parent_key.id);
}
```

The `ephemeral_expiration` field contains the disappearing messages timer in seconds, extracted from the message's `contextInfo.expiration`. This tells you how long the message will be visible before it auto-deletes. Use this value when sending replies to the same chat via [`SendOptions.ephemeral_expiration`](/api/send#sendoptions).

The `unavailable_request_id` field is set when a message was recovered via PDO rather than normal decryption. It contains the PDO request message ID, which you can use to correlate recovered messages with the original `UndecryptableMessage` event.

The `comment_target` field is set when the dispatched message is a decrypted CAG channel comment. It contains the `MessageKey` of the parent post. The inner `Message` proto has no slot for the threading link, so it surfaces here instead. See [Channel comments](#channel-comments) below.

### Message content extraction

Use the `MessageExt` trait to extract content:
Expand Down Expand Up @@ -217,12 +224,16 @@ See [Media Handling Guide](/guides/media-handling) for download details.

### Reactions

Incoming reactions — including encrypted CAG reactions — are dispatched in the same `reaction_message` shape. Encrypted reactions from Community Announcement Groups are decrypted transparently on the receive path; the `key` field is filled from the envelope's `target_message_key` before dispatch.

```rust
if let Some(reaction) = &message.reaction_message {
if let Some(emoji) = &reaction.text {
println!("👍 Reaction: {}", emoji);
} else {
println!("Reaction removed");
if emoji.is_empty() {
println!("Reaction removed");
} else {
println!("👍 Reaction: {}", emoji);
}
}

if let Some(key) = &reaction.key {
Expand All @@ -231,6 +242,26 @@ if let Some(reaction) = &message.reaction_message {
}
```

### Channel Comments

Encrypted channel comments from Community Announcement Groups are decrypted transparently and dispatched as `Event::Message` carrying the comment body. The parent post key surfaces on `MessageInfo::comment_target` (the inner `Message` proto has no slot for the threading link):

```rust
Event::Message(msg, info) => {
if let Some(parent_key) = &info.comment_target {
// This Event::Message is a decrypted CAG channel comment.
println!("Comment on post: {:?}", parent_key.id);
println!("Post author: {:?}", parent_key.participant);

if let Some(text) = msg.text_content() {
println!("Comment text: {}", text);
}
}
}
```

The comment's own `messageSecret` (carried in the outer envelope) is persisted under the comment's id and sender, so that future encrypted reactions targeting the comment can be decrypted. `comment_target` is `None` for all other message types.

### Quoted Messages

```rust
Expand Down Expand Up @@ -685,6 +716,7 @@ Spawn tasks for long-running operations:

## Next Steps

- [Sending Messages](/guides/sending-messages) - Send text, reactions, and replies
- [Sending Messages](/guides/sending-messages) - Send text, reactions, channel comments, and replies
- [Media Handling](/guides/media-handling) - Download and process media
- [Group Management](/guides/group-management) - Handle group events
- [Community management](/guides/communities) - CAG reactions and channel comments
Loading