Skip to content

feat(groups): backfill participant phone_number from LID-PN mapping - #909

Merged
jlucaso1 merged 4 commits into
oxidezap:mainfrom
Salientekill:feat/backfill-participant-pn-from-mapping
Jun 19, 2026
Merged

feat(groups): backfill participant phone_number from LID-PN mapping#909
jlucaso1 merged 4 commits into
oxidezap:mainfrom
Salientekill:feat/backfill-participant-pn-from-mapping

Conversation

@Salientekill

@Salientekill Salientekill commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Problem

In LID-addressed groups the server frequently omits the phone_number attribute on <participant> nodes, so the GroupMetadata returned by get_participating/get_metadata ends up LID-only (phone_number: None for every participant).

Any consumer that cross-references data keyed by PN (legacy rows, anything that canonicalizes to phone number) then treats current members as absent — even though the client already knows their PN via the lid_pn_mapping it learned from messages/usync. Downstream this shows up as e.g. orphan-cleanup deleting data for members who are still in the group, or mentions failing to resolve.

Change

get_participating and get_metadata now backfill each LID participant's phone_number from the persisted lid_pn_mapping:

  • One backend load (get_all_lid_mappings) + in-memory join — not N per-participant lookups.
  • Gated by needs_pn_fill: only runs when a LID-addressed group actually has a participant missing its PN, so PN-addressed groups and already-complete metadata pay nothing.
  • Degrades gracefully: if the mapping load fails, metadata is served PN-less as before (no behavior regression).

The fill happens only on the read paths that hand GroupMetadata to callers; the persisted blob / phash used for not-modified detection is untouched.

Tests

  • fill_participant_pns_backfills_lid_from_mapping — LID participant gets its PN from the map.
  • fill_participant_pns_noop_in_pn_group — PN-addressed groups are left untouched.

Fictitious JIDs/numbers only.

Review in cubic

The server often omits the phone_number attribute on <participant> nodes
of LID-addressed groups, leaving GroupMetadata LID-only. Consumers that
cross-reference data keyed by PN then treat current members as absent.

get_participating/get_metadata now backfill each LID participant's
phone_number from the persisted lid_pn_mapping the client already learned
(single backend load + in-memory join, only when a LID-addressed group has
a participant missing its PN).
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0dfd9b56-2ee3-46ca-9ab2-c9c59e7bd81c

📥 Commits

Reviewing files that changed from the base of the PR and between a58fb39 and 2c68cc0.

📒 Files selected for processing (1)
  • src/features/groups.rs

📝 Walkthrough

Summary by CodeRabbit

Bug Fixes

  • Group metadata now automatically backfills missing participant phone numbers for LID-addressed groups when an LID→phone mapping is available, improving contact completeness.
  • Phone numbers for PN-addressed groups remain unchanged.

Tests

  • Added unit tests covering phone-number backfill for LID-addressed groups and verifying that PN-addressed groups are not modified.

Walkthrough

Adds phone number backfill for LID-addressed group participants. A new fill_participant_pns helper mutates GroupMetadata in-place by resolving participant LIDs through the client's LID→PN cache and converting matches to PN Jid values. get_participating and get_metadata both apply this helper to returned metadata before returning. Two unit tests cover LID and PN addressing modes to ensure correctness.

Changes

LID→PN Phone Number Backfill for Group Participants

Layer / File(s) Summary
fill_participant_pns helper implementation
src/features/groups.rs
Adds async fill_participant_pns that checks GroupMetadata.addressing_mode == Lid, iterates participants, and fills missing phone_number by resolving participant LIDs via get_lid_pn_entry, converting returned user parts to PN Jid values, and leaving unresolved entries unchanged.
Backfill wired into get_participating and get_metadata
src/features/groups.rs
get_participating post-processes each GroupMetadata in the returned HashMap with fill_participant_pns before returning; get_metadata applies the same backfill on GroupInfoOutcome::Full before returning, leaving NotModified behavior unchanged.
Unit tests for fill_participant_pns
src/features/groups.rs
Two tokio tests assert that LID-addressed participants receive phone_number from the warm cache when mapping exists, and that PN-addressed groups remain unmodified with phone_number staying None.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#487: The new fill_participant_pns logic depends directly on the retrieved PR's client.get_lid_pn_entry(&Jid) API for LID→PN resolution.
  • oxidezap/whatsapp-rust#680: This PR backfills participant phone numbers by calling client.get_lid_pn_entry, which depends on the retrieved PR's LID→PN cache population via learn_lid_pn_mappings_batch.
  • oxidezap/whatsapp-rust#565: This PR's fill_participant_pns calls Client::get_lid_pn_entry, which is refactored in the retrieved PR to support cache-aside backend fallback for LID resolution.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(groups): backfill participant phone_number from LID-PN mapping' is concise, specific, and directly describes the main feature addition in the changeset.
Description check ✅ Passed The description thoroughly explains the problem, solution, implementation details, and test cases, all directly related to the phone_number backfilling functionality in LID-addressed groups.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Re-trigger cubic

@codspeed-hq

codspeed-hq Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 176 untouched benchmarks
⏩ 6 skipped benchmarks1


Comparing Salientekill:feat/backfill-participant-pn-from-mapping (2c68cc0) with main (c3e44df)

Open in CodSpeed

Footnotes

  1. 6 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@jlucaso1 jlucaso1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch on the problem. In LID groups the server really does drop phone_number a lot, and anything keyed by PN ends up thinking present members are gone. The needs_pn_fill gate and the graceful fallback are good.

One thing on the approach though. Instead of reloading the whole lid_pn_mapping table with get_all_lid_mappings on every call, you can use the warm in-memory cache the client already keeps (lid_pn_cache, warmed from that same table at startup). create_group right above already does exactly this kind of lookup for the same "LID with no phone_number" case.

Two reasons it matters:

  1. Cost. get_metadata on one small group will spawn_blocking, scan the full table and build a HashMap of every mapping in the account just to fill a couple of fields. The cache lookup is in-memory and only touches the participants that actually need it.
  2. Freshness. Mappings learned offline only live in the cache and aren't persisted (learn_lid_pn_mapping_fast/_batch with is_offline), and the detached persist can lag or fail. So a DB-only load can miss something the cache already has, and the participant stays PN-less even though we could resolve it.

What I'd suggest, dropping load_lid_pn_map entirely:

for p in meta.participants.iter_mut() {
    if p.phone_number.is_none()
        && p.jid.is_lid()
        && let Some(pn) = self.client.lid_pn_cache.get_phone_number(&p.jid.user).await
    {
        p.phone_number = Some(Jid::pn(pn));
    }
}

get_phone_number degrades the same way you already do (unknown returns None). If you want it to survive a bounded/evicted cache, use get_lid_pn_entry instead, same as create_group. Keep needs_pn_fill if you like, it just doesn't need to gate any I/O anymore.

Couple of small things:

  • Keep the addressing_mode != Lid check inside fill_participant_pns. In get_participating the fill runs over every group including PN ones, so it's actually doing work there, not redundant.
  • The tests only cover the pure helper. A quick one through the client (warmed mapping in, filled PN out) would cover the part that changed.
  • The red CodSpeed integration check isn't your fault, it's a workflow template error (codspeed.yml line 82), happens on fork PRs.

Happy to merge once the lookup goes through the cache.

…able load

Per review: instead of reloading the whole lid_pn_mapping table on every
call, look each LID participant up through the client's warm in-memory cache
via get_lid_pn_entry (same path create_group uses). Cheaper (only touches
participants that need it) and fresher (picks up offline-learned mappings the
detached DB persist may lag). Drops load_lid_pn_map/needs_pn_fill; the
addressing_mode guard stays inside fill_participant_pns. Tests now go through
a client with a warmed mapping.
@Salientekill

Copy link
Copy Markdown
Contributor Author

Thanks for the review — all applied in 6a70fad.

fill_participant_pns now resolves each LID participant through get_lid_pn_entry (warm in-memory cache + backend fallback, same path create_group uses), so it only touches participants that need it and picks up offline-learned mappings the detached DB persist may lag. Dropped load_lid_pn_map/needs_pn_fill entirely; kept the addressing_mode != Lid guard inside the helper. Tests now go through a client with a warmed mapping (LID in → PN filled, PN-group no-op).

Good call on both the cost and freshness points.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/groups.rs`:
- Around line 361-365: The sequential await calls in the loop over
meta.participants for get_lid_pn_entry are causing performance bottlenecks on
large groups. Instead of awaiting each lookup one at a time in the for loop,
collect all the concurrent lookup futures for participants that need LID to PN
resolution (those with no phone number and is_lid), execute them concurrently
with a bounded fan-out using a stream buffer to limit parallelism, and then
apply the resolved results back to the participants array by their original
indices. This will allow multiple lookups to proceed in parallel while still
maintaining control over resource usage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e81e4d59-e841-4d3a-8b30-92009e027e7e

📥 Commits

Reviewing files that changed from the base of the PR and between 05b72cb and 6a70fad.

📒 Files selected for processing (1)
  • src/features/groups.rs

Comment thread src/features/groups.rs Outdated
Per CodeRabbit nitpick: the per-participant get_lid_pn_entry awaits ran
serially, which on a large group with a cold cache serializes the DB
fallbacks. Collect the PN-less LID participants by index and resolve them
with buffer_unordered(16), then apply results back. Cache hits stay cheap;
cold-cache large groups no longer serialize.
@jlucaso1

Copy link
Copy Markdown
Collaborator

@Salientekill e esse lint ai (clippy)

@Salientekill

Copy link
Copy Markdown
Contributor Author

@Salientekill e esse lint ai (clippy)

Vagabunda kskskskkskskkskskksks

clippy::filter_map_bool_then under -D warnings. Same result, no behavior change.
@jlucaso1
jlucaso1 merged commit d8e5f73 into oxidezap:main Jun 19, 2026
12 of 13 checks passed
@Salientekill
Salientekill deleted the feat/backfill-participant-pn-from-mapping branch June 19, 2026 23:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants