feat: community support — full CRUD, subgroup management, queries - #392
Conversation
… deactivate Full community (parent group) feature implementation. Communities are WhatsApp's hierarchical group structure: a parent group containing linked subgroups. ### API ```rust // Creation & deletion client.community().create(options).await? client.community().deactivate(&jid).await? // Subgroup management client.community().link_subgroups(&parent, &[subgroup]).await? client.community().unlink_subgroups(&parent, &[subgroup], remove_orphans).await? // Queries (MEX GraphQL) client.community().get_subgroups(&parent).await? client.community().get_subgroup_participant_counts(&parent).await? // Queries (IQ) client.community().query_linked_group(&parent, &subgroup).await? client.community().join_subgroup(&parent, &subgroup).await? client.community().get_linked_groups_participants(&parent).await? ``` ### New types - `GroupType` — Default, Community, LinkedSubgroup, LinkedAnnouncementGroup, LinkedGeneralGroup - `CreateCommunityOptions`, `CreateCommunityResult` - `CommunitySubgroup` — subgroup metadata with participant count - `LinkSubgroupsResult`, `UnlinkSubgroupsResult` - `group_type()` helper to classify GroupMetadata ### Extended types - `GroupMetadata` — added `is_parent_group`, `parent_group_jid`, `is_default_sub_group`, `is_general_chat` - `GroupCreateOptions` — added `is_parent`, `closed`, `allow_non_admin_sub_group_creation`, `create_general_chat` - `GroupInfoResponse` — community field parsing from w:g2 response nodes ### New IQ specs (wacore) - `LinkSubgroupsIq` — link groups as subgroups - `UnlinkSubgroupsIq` — unlink subgroups (with orphan member removal option) - `DeleteCommunityIq` — deactivate a community - `QueryLinkedGroupIq` — query subgroup metadata from parent - `JoinLinkedGroupIq` — join a subgroup via parent community - `GetLinkedGroupsParticipantsIq` — all participants across linked groups ### E2E tests (10/10 passing) | Test | Covers | |---|---| | `test_community_create` | Create → verify is_parent_group + GroupType::Community | | `test_community_create_with_general_chat` | Create with general chat → verify subgroup list | | `test_community_get_subgroups` | Verify auto-created default announcement subgroup | | `test_community_link_subgroup` | Create group → link → verify in subgroup list | | `test_community_unlink_subgroup` | Link → unlink → verify removed from list | | `test_community_deactivate` | Create → deactivate → verify deleted | | `test_community_query_linked_group` | Query linked subgroup metadata | | `test_community_join_subgroup` | Client B joins subgroup via community parent | | `test_community_get_linked_groups_participants` | Fetch participants across linked groups | | `test_community_subgroup_participant_counts` | MEX query for per-subgroup counts |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds a new Community feature: a Community API module, community-aware IQ specs and mex docs, extensions to GroupMetadata and group IQ flows for parent/subgroup data, end-to-end tests, and public re-exports to expose community types and helpers. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant Feature as Community Feature
participant IQ as IQ Layer
participant MEX as MEX/GraphQL
participant Server as WA Server
Client->>Feature: community().create(options)
activate Feature
Feature->>IQ: Build GroupCreateIq / Delete/Link/Unlink Iq
IQ->>Server: send IQ
Server-->>IQ: IQ response
IQ-->>Feature: parsed GroupInfoResponse / Link/Unlink result
Feature->>MEX: MexRequest (mex_docs) for subgroup lists/participant counts
MEX-->>Feature: MexResponse (nodes)
Feature-->>Client: CreateCommunityResult / LinkSubgroupsResult / CommunitySubgroup list
deactivate Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@claude review |
|
Claude finished @jlucaso1's task in 1m 30s —— View job Code Review: Community Support
Overall this is a clean, well-structured addition. The IQ specs follow existing patterns, the feature handle mirrors Issues
Minor / Nits
What looks good
|
1. Remove duplicated group_info_to_metadata(), use GroupMetadata::from_response() (now pub(crate)) 2. Add allow_non_admin_sub_group_creation to GroupMetadata 3. Add warn! logging for unparseable MEX entries 4. Assert creator presence in get_linked_groups_participants test 5. Remove unused is_general parameter from parse_subgroup_node 6. Add 12 unit tests for community IQ specs (build + parse round-trips) Also fixes: - GetLinkedGroupsParticipantsIq parser handles participants nested in <group> nodes - get_linked_groups_participants test links a subgroup first so there are participants to query
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
src/features/community.rs (1)
339-363: Deduplicate theGroupInfoResponse -> GroupMetadatamapping.This helper is the same field copy as
GroupMetadata::from_responseinsrc/features/groups.rs. A sharedFrom<GroupInfoResponse>impl would keep future metadata additions from drifting.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/community.rs` around lines 339 - 363, The mapping in group_info_to_metadata duplicates GroupMetadata::from_response; replace this with a single From<GroupInfoResponse> implementation used across the codebase: implement impl From<GroupInfoResponse> for GroupMetadata (moving the field-by-field copies from group_info_to_metadata / GroupMetadata::from_response into that impl), then remove or refactor the group_info_to_metadata function to construct GroupMetadata via GroupInfoResponse::into (or .into()) so all conversions use the shared From<GroupInfoResponse> implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/features/community.rs`:
- Around line 7-14: CreateCommunityOptions::description is accepted by the
public API but never applied in create(), so callers see a blank description;
update the create() implementation in community.rs to either include the
provided CreateCommunityOptions::description in the group creation payload
(e.g., map it into the GroupCreateIq / GroupCreateOptions sent to the backend)
or, if the protocol cannot set description during creation, perform a
post-create mutation to set the description (or return an explicit unsupported
error instead of silently ignoring it). Locate the create() function and the
types CreateCommunityOptions, GroupCreateIq, and GroupCreateOptions to add the
description mapping or the follow-up update/error path.
In `@tests/e2e/tests/community.rs`:
- Around line 477-481: The test currently only checks presence of the subgroup
in counts using counts.iter().any(|(jid, _)| *jid == group.gid); change it to
locate the tuple for group.gid (e.g., using counts.iter().find or filter) and
assert that the associated count value is >= 1; update the assertion to fail if
no matching tuple is found or if the found tuple's second element is less than 1
so the test verifies the returned participant count, not just presence.
- Around line 431-437: The test currently only logs whether the creator JID
(own_jid) is present in participants but doesn't assert it, so empty or
misparsed participant lists pass; update the test in
tests/e2e/tests/community.rs to assert the participant results are non-empty and
that participants.iter().any(|p| p.jid == own_jid || p.phone_number.as_ref() ==
Some(&own_jid)) is true (use assert! with a clear message), keeping the existing
info! line for logging.
- Around line 74-78: The test currently only asserts a default announcement
subgroup via subgroups.iter().any(|s| s.is_default_sub_group) but never verifies
the general-chat was created; update the test to also assert that the subgroups
collection contains the general-chat subgroup (e.g., check
subgroups.iter().any(|s| s.name == "general-chat" or another identifying field)
so create_general_chat is actually exercised), locating the assertion near the
existing use of subgroups and the create_general_chat call.
In `@wacore/src/iq/groups.rs`:
- Around line 1759-1762: The parse_response in parse_response(&self, response:
&Node) only calls collect_children(container, "participant") so it misses
participants nested inside <group> wrappers; update parse_response (and/or add a
helper) to first find the required_child("linked_groups_participants"), then
collect participant nodes both directly and within any <group> children (e.g.
iterate container children: if tag == "participant" parse with
GroupParticipantResponse, if tag == "group" descend into that node and collect
its "participant" children), or implement a small recursive collect that accepts
"participant" and handles "group" containers, then return collect_children
results accordingly so community().get_linked_groups_participants() receives
participants whether nested or direct.
---
Nitpick comments:
In `@src/features/community.rs`:
- Around line 339-363: The mapping in group_info_to_metadata duplicates
GroupMetadata::from_response; replace this with a single From<GroupInfoResponse>
implementation used across the codebase: implement impl From<GroupInfoResponse>
for GroupMetadata (moving the field-by-field copies from group_info_to_metadata
/ GroupMetadata::from_response into that impl), then remove or refactor the
group_info_to_metadata function to construct GroupMetadata via
GroupInfoResponse::into (or .into()) so all conversions use the shared
From<GroupInfoResponse> implementation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 395e20b7-9ed5-4dee-b0a0-287c9383ea97
📒 Files selected for processing (8)
src/features/community.rssrc/features/groups.rssrc/features/mod.rssrc/lib.rstests/e2e/tests/community.rswacore/src/iq/community.rswacore/src/iq/groups.rswacore/src/iq/mod.rs
There was a problem hiding this comment.
♻️ Duplicate comments (5)
tests/e2e/tests/community.rs (3)
479-483:⚠️ Potential issue | 🟡 MinorValidate the returned participant count, not just subgroup presence.
Line 481 only checks that the subgroup exists in the result, not that its count is meaningful.
Suggested patch
- assert!( - counts.iter().any(|(jid, _)| *jid == group.gid), - "linked subgroup should appear in participant counts" - ); + let linked_group_count = counts + .iter() + .find_map(|(jid, count)| (*jid == group.gid).then_some(*count)); + assert!( + linked_group_count.is_some_and(|count| count >= 1), + "linked subgroup should report at least the creator" + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/tests/community.rs` around lines 479 - 483, The test currently only asserts the subgroup id appears in counts; update the assertion to also validate the participant count is >= 1 by locating the tuple for group.gid in counts (use counts.iter().find or counts.iter().any with a predicate that checks both *jid == group.gid and count >= 1) and assert that the found count meets >= 1 (reference symbols: counts, group.gid). Ensure the failure message describes a missing or zero participant count for the linked subgroup.
74-78:⚠️ Potential issue | 🟡 MinorAssert
create_general_chatbehavior explicitly.Line 76 verifies only the default subgroup. This test can still pass even if general-chat creation regresses.
Suggested patch
assert!( subgroups.iter().any(|s| s.is_default_sub_group), "should have a default announcement subgroup" ); + assert!( + subgroups.iter().any(|s| s.is_general_chat), + "should have a general chat subgroup" + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/tests/community.rs` around lines 74 - 78, The test currently only asserts a default subgroup exists (checking subgroups.iter().any(|s| s.is_default_sub_group)); explicitly assert the expected "general-chat" was created by adding an assertion that a subgroup with the general chat identifier/name exists and/or that the function/operation create_general_chat succeeded (e.g., check for s.name == "general-chat" or s.is_general_chat) so regressions in create_general_chat will fail the test; locate this in tests/e2e/tests/community.rs near the existing subgroups assertions and add a precise assertion referencing the subgroup name or flag.
425-439:⚠️ Potential issue | 🟡 MinorStrengthen participant assertion to include creator identity.
Line 436 only checks non-empty results; it does not prove the expected participant set includes the test account.
Suggested patch
let participants = client .client .community() .get_linked_groups_participants(&community.gid) .await?; + + let own_jid = client + .client + .get_pn() + .await + .expect("client should have PN") + .to_non_ad(); + let own_lid = client + .client + .get_lid() + .await + .expect("client should have LID") + .to_non_ad(); assert!( !participants.is_empty(), "should return at least the creator as a participant across linked groups" ); + assert!( + participants.iter().any(|p| { + p.jid == own_jid || p.jid == own_lid || p.phone_number.as_ref() == Some(&own_jid) + }), + "linked_groups_participants should include the creator" + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/tests/community.rs` around lines 425 - 439, The test currently only asserts that participants is non-empty; update it to assert that the expected creator/test account appears in the returned participants from client.client.community().get_linked_groups_participants(&community.gid). Locate the test variables that hold the creator's identity (e.g., the test account id or community.creator) and add an assertion that participants contains that identifier (for example by checking any participant.uid == expected_uid or comparing actor ids), so the test verifies the creator is returned across linked groups.src/features/community.rs (1)
38-41:⚠️ Potential issue | 🟠 Major
CreateCommunityOptions::descriptionis currently a no-op.Line 40 exposes
description, butcreate()(Line 123-130) never applies it, so callers silently lose user input.Suggested patch
-use crate::features::groups::GroupMetadata; +use crate::features::groups::{GroupDescription, GroupMetadata}; @@ pub async fn create( &self, options: CreateCommunityOptions, ) -> Result<CreateCommunityResult, anyhow::Error> { + let CreateCommunityOptions { + name, + description, + closed, + allow_non_admin_sub_group_creation, + create_general_chat, + } = options; + + let description = description + .as_deref() + .map(GroupDescription::new) + .transpose()?; + let create_options = GroupCreateOptions { - subject: options.name, + subject: name, is_parent: true, - closed: options.closed, - allow_non_admin_sub_group_creation: options.allow_non_admin_sub_group_creation, - create_general_chat: options.create_general_chat, + closed, + allow_non_admin_sub_group_creation, + create_general_chat, ..Default::default() }; @@ let gid = self .client .execute(GroupCreateIq::new(create_options)) .await?; + + if let Some(description) = description { + self.client + .groups() + .set_description(&gid, Some(description), None) + .await?; + } Ok(CreateCommunityResult { gid }) }Also applies to: 119-138
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/community.rs` around lines 38 - 41, The CreateCommunityOptions struct exposes a description field but the create() function never applies it, so user-provided descriptions are dropped; update the create() implementation to read CreateCommunityOptions::description and set the corresponding field on the new Community (or include it in the DB insert/update) when constructing/saving the community object (refer to CreateCommunityOptions::description and the create() function where the Community is built/saved) so the description is persisted.wacore/src/iq/groups.rs (1)
1759-1775:⚠️ Potential issue | 🟡 MinorCollect both direct and nested participants in one pass.
Line 1764 returns early, so
<participant>entries nested under<group>(Line 1770+) are skipped if any direct participant exists.Suggested patch
fn parse_response(&self, response: &Node) -> Result<Self::Response> { let container = required_child(response, "linked_groups_participants")?; - // Participants may be direct children or nested inside <group> nodes. - let direct = collect_children::<GroupParticipantResponse>(container, "participant")?; - if !direct.is_empty() { - return Ok(direct); - } - - // Nested: <linked_groups_participants><group><participant/></group></linked_groups_participants> - let mut all = Vec::new(); + // Participants may be direct children and/or nested inside <group> nodes. + let mut all = collect_children::<GroupParticipantResponse>(container, "participant")?; for group_node in container.get_children_by_tag("group") { let participants = collect_children::<GroupParticipantResponse>(group_node, "participant")?; all.extend(participants); } Ok(all) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/groups.rs` around lines 1759 - 1775, The parse_response function currently returns early when direct participants are found, skipping nested <participant> entries; change it to always collect both direct and nested participants by initializing a combined Vec (e.g., let mut all = collect_children::<GroupParticipantResponse>(container, "participant")?;), then iterate container.get_children_by_tag("group") and extend all with collect_children::<GroupParticipantResponse>(group_node, "participant")?; finally return Ok(all) instead of returning early from the direct branch; update references to container, direct, all, group_node, and collect_children accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/features/community.rs`:
- Around line 38-41: The CreateCommunityOptions struct exposes a description
field but the create() function never applies it, so user-provided descriptions
are dropped; update the create() implementation to read
CreateCommunityOptions::description and set the corresponding field on the new
Community (or include it in the DB insert/update) when constructing/saving the
community object (refer to CreateCommunityOptions::description and the create()
function where the Community is built/saved) so the description is persisted.
In `@tests/e2e/tests/community.rs`:
- Around line 479-483: The test currently only asserts the subgroup id appears
in counts; update the assertion to also validate the participant count is >= 1
by locating the tuple for group.gid in counts (use counts.iter().find or
counts.iter().any with a predicate that checks both *jid == group.gid and count
>= 1) and assert that the found count meets >= 1 (reference symbols: counts,
group.gid). Ensure the failure message describes a missing or zero participant
count for the linked subgroup.
- Around line 74-78: The test currently only asserts a default subgroup exists
(checking subgroups.iter().any(|s| s.is_default_sub_group)); explicitly assert
the expected "general-chat" was created by adding an assertion that a subgroup
with the general chat identifier/name exists and/or that the function/operation
create_general_chat succeeded (e.g., check for s.name == "general-chat" or
s.is_general_chat) so regressions in create_general_chat will fail the test;
locate this in tests/e2e/tests/community.rs near the existing subgroups
assertions and add a precise assertion referencing the subgroup name or flag.
- Around line 425-439: The test currently only asserts that participants is
non-empty; update it to assert that the expected creator/test account appears in
the returned participants from
client.client.community().get_linked_groups_participants(&community.gid). Locate
the test variables that hold the creator's identity (e.g., the test account id
or community.creator) and add an assertion that participants contains that
identifier (for example by checking any participant.uid == expected_uid or
comparing actor ids), so the test verifies the creator is returned across linked
groups.
In `@wacore/src/iq/groups.rs`:
- Around line 1759-1775: The parse_response function currently returns early
when direct participants are found, skipping nested <participant> entries;
change it to always collect both direct and nested participants by initializing
a combined Vec (e.g., let mut all =
collect_children::<GroupParticipantResponse>(container, "participant")?;), then
iterate container.get_children_by_tag("group") and extend all with
collect_children::<GroupParticipantResponse>(group_node, "participant")?;
finally return Ok(all) instead of returning early from the direct branch; update
references to container, direct, all, group_node, and collect_children
accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c1615abe-c7b3-4ca0-9206-68727f7e1ab6
📒 Files selected for processing (4)
src/features/community.rssrc/features/groups.rstests/e2e/tests/community.rswacore/src/iq/groups.rs
1. CreateCommunityOptions::description now applied via post-create set_description IQ 2. Participant counts test asserts count >= 1, not just presence 3. Linked groups participants test asserts creator JID is present 4. General chat test verifies is_general_chat subgroup exists 5. parse_response nested <group> handling — already fixed (verified) 6. Converted GroupMetadata::from_response() to From<GroupInfoResponse> trait impl
Summary
w:g2IQ namespace) with community-specific fields and 6 new IQ specsGroupMetadataextended withis_parent_group,parent_group_jid,is_default_sub_group,is_general_chatGroupTypeenum andgroup_type()helper for classifying groups within the community hierarchyAPI
New files
wacore/src/iq/community.rs— MEX doc ID constantssrc/features/community.rs—Community<'a>feature with 9 methods + typestests/e2e/tests/community.rs— 10 E2E testsNot covered (can be added later)
GroupUpdatewithLink/Unlinkactions covers this)Test plan
cargo fmt— cleancargo clippy --all --tests— zero warningscargo test --all --exclude e2e-tests— all unit tests passcargo test -p e2e-tests --test community— 10/10 E2E tests passcargo test -p e2e-tests --test groups— 6/6 existing group tests pass (no regression)Summary by CodeRabbit
New Features
Improvements
Tests