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, identifying group types, sending encrypted reactions to Community Announcement Groups, and posting channel comments.
All community operations are accessed through the community() method:
let community = client.community();
See Community API reference for the full API.
use whatsapp_rust::features::community::CreateCommunityOptions;
let options = CreateCommunityOptions::new("My Community");
let result = client.community().create(options).await?;
println!("Community created: {}", result.gid);
By default, create_general_chat is true, so a general chat subgroup is created alongside the community.
See Community API reference for details.
Customize the community with CreateCommunityOptions:
use whatsapp_rust::features::community::CreateCommunityOptions;
let mut options = CreateCommunityOptions::new("My Community");
options.description = Some("Welcome to the community!".to_string());
options.closed = true;
options.allow_non_admin_sub_group_creation = false;
options.create_general_chat = true;
let result = client.community().create(options).await?;
If a description is provided, it is set via a follow-up IQ after creation — the group create stanza does not support inline descriptions for communities.
Deactivate (delete) a community. Subgroups are unlinked but not deleted:
client.community().deactivate(&community_jid).await?;
See Community API reference for details.
Managing subgroups
Link existing groups
Link existing groups as subgroups of a community:
let subgroup_jids = vec![
"120363001111111@g.us".parse()?,
"120363002222222@g.us".parse()?,
];
let result = client.community()
.link_subgroups(&community_jid, &subgroup_jids)
.await?;
println!("Linked: {:?}", result.linked_jids);
for (jid, error_code) in &result.failed_groups {
eprintln!("Failed to link {}: error code {}", jid, error_code);
}
See Community API reference for details.
Unlink subgroups
Unlink subgroups from a community:
let result = client.community()
.unlink_subgroups(
&community_jid,
&subgroup_jids,
true, // remove_orphan_members
)
.await?;
println!("Unlinked: {:?}", result.unlinked_jids);
When remove_orphan_members is true, members who are only in the community through the unlinked subgroups are removed from the community.
See Community API reference for details.
Join a subgroup
Join a linked subgroup via the parent community:
let metadata = client.community()
.join_subgroup(&community_jid, &subgroup_jid)
.await?;
println!("Joined: {} ({} participants)",
metadata.subject, metadata.participants.len());
See Community API reference for details.
List subgroups
Fetch all subgroups of a community via MEX (GraphQL):
let subgroups = client.community()
.get_subgroups(&community_jid)
.await?;
for sg in &subgroups {
println!("{}: {} (default: {}, general: {})",
sg.id, sg.subject, sg.is_default_sub_group, sg.is_general_chat);
if let Some(count) = sg.participant_count {
println!(" Participants: {}", count);
}
}
See Community API reference for details.
Get subgroup participant counts
Fetch participant counts per subgroup without fetching full subgroup details:
let counts = client.community()
.get_subgroup_participant_counts(&community_jid)
.await?;
for (jid, count) in &counts {
println!("{}: {} participants", jid, count);
}
See Community API reference for details.
Query a specific linked subgroup’s metadata from the parent community:
let metadata = client.community()
.query_linked_group(&community_jid, &subgroup_jid)
.await?;
println!("Subject: {}", metadata.subject);
println!("Participants: {}", metadata.participants.len());
See Community API reference for details.
Get all participants across subgroups
Fetch all participants across all linked groups of a community:
let participants = client.community()
.get_linked_groups_participants(&community_jid)
.await?;
for p in &participants {
println!("{} (admin: {})", p.jid, p.is_admin);
}
See Community API reference for details.
Identifying group types
Use the group_type function to classify a group within the community hierarchy:
use whatsapp_rust::features::community::{group_type, GroupType};
let metadata = client.groups().get_metadata(&group_jid).await?;
match group_type(&metadata) {
GroupType::Community => println!("This is a community parent group"),
GroupType::LinkedSubgroup => println!("This is a linked subgroup"),
GroupType::LinkedAnnouncementGroup => println!("This is the default announcement subgroup"),
GroupType::LinkedGeneralGroup => println!("This is the general chat subgroup"),
GroupType::Default => println!("This is a regular group"),
_ => println!("Unknown group type"),
}
The classification is based on these GroupMetadata fields:
| Field | Description |
|---|
is_parent_group | true for community parent groups |
parent_group_jid | JID of the parent community (for subgroups) |
is_default_sub_group | true for the default announcement subgroup |
is_general_chat | true for the general chat subgroup |
allow_non_admin_sub_group_creation | Whether non-admin members can create subgroups |
See Groups API reference for all GroupMetadata fields.
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:
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:
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()));
}
}
Post encrypted threaded replies under a CAG post using client.comments():
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:
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.
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:
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:
use whatsapp_rust::features::mex::MexError;
match client.community().get_subgroups(&community_jid).await {
Ok(subgroups) => println!("Found {} subgroups", subgroups.len()),
Err(MexError::PayloadParsing(msg)) => {
eprintln!("Parse error: {}", msg);
}
Err(e) => eprintln!("Error: {}", e),
}
Next steps