Skip to content

fix: nest profile picture tctoken and avoid usync on lookup#2614

Open
gusquadri wants to merge 3 commits into
WhiskeySockets:masterfrom
gusquadri:fix/profile-picture-tctoken-nesting
Open

fix: nest profile picture tctoken and avoid usync on lookup#2614
gusquadri wants to merge 3 commits into
WhiskeySockets:masterfrom
gusquadri:fix/profile-picture-tctoken-nesting

Conversation

@gusquadri

@gusquadri gusquadri commented May 31, 2026

Copy link
Copy Markdown
Contributor
  • nest profile-picture tctoken inside the <picture> node, matching WA Web and whatsmeow
  • avoid triggering PN→LID USync while only checking for a profile-picture tctoken (probably a ban vector)
  • keep profile-picture tctoken attachment limited to locally known token/mapping stat

Summary by cubic

Nest the profile-picture tctoken inside the <picture> node and avoid PN→LID USync when only fetching a profile photo URL. Matches WA Web/whatsmeow behavior and reduces potential ban risk.

  • Bug Fixes
    • Nest tctoken under <picture> in profile picture URL queries.
    • Use getKnownLIDForPN (cached PN→LID, no USync) to build the tctoken; accept only non-empty stored LIDs and attach the token only when the mapping is locally known.

Written for commit 2d91a47. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Improved identity resolution to return known device-aware IDs for privacy-number users, enhancing profile picture token generation.
  • Refactor

    • Reworked profile picture payload construction to integrate the new identity resolver and streamline privacy-token insertion for non-self users.

@whiskeysockets-bot

whiskeysockets-bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Thanks for opening this pull request and contributing to the project!

The next step is for the maintainers to review your changes. If everything looks good, it will be approved and merged into the main branch.

In the meantime, anyone in the community is encouraged to test this pull request and provide feedback.

✅ How to confirm it works

If you’ve tested this PR, please comment below with:

Tested and working ✅

This helps us speed up the review and merge process.

📦 To test this PR locally:

# NPM
npm install @whiskeysockets/baileys@gusquadri/Baileys#fix/profile-picture-tctoken-nesting

# Yarn (v2+)
yarn add @whiskeysockets/baileys@gusquadri/Baileys#fix/profile-picture-tctoken-nesting

# PNPM
pnpm add @whiskeysockets/baileys@gusquadri/Baileys#fix/profile-picture-tctoken-nesting

If you encounter any issues or have feedback, feel free to comment as well.

@coderabbitai

coderabbitai Bot commented May 31, 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: CHILL

Plan: Pro

Run ID: db0c04b4-9672-464a-a093-b10f8614beff

📥 Commits

Reviewing files that changed from the base of the PR and between a2aefa0 and 2d91a47.

📒 Files selected for processing (1)
  • src/Signal/lid-mapping.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Signal/lid-mapping.ts

📝 Walkthrough

Walkthrough

This PR adds LIDMappingStore.getKnownLIDForPN for single-PN LID resolution (validation, cache check, targeted DB fetch, and cache update) and wires it into makeChatsSocket; profilePictureUrl is refactored to build the picture node up front and use the resolver when creating WhatsApp tctoken payloads.

Changes

PN-to-LID Resolver Integration

Layer / File(s) Summary
LID mapping resolver method
src/Signal/lid-mapping.ts
New getKnownLIDForPN method validates PN format, decodes it, checks the pn: cache, performs a targeted lid-mapping database lookup if missing, updates both forward (pn:) and reverse (lid:) caches, and formats the resulting LID JID with optional device suffix and correct domain (hosted.lid vs lid).
Chat socket integration and profile picture refactoring
src/Socket/chats.ts
makeChatsSocket binds getKnownLIDForPN. profilePictureUrl now constructs a persistent pictureNode and content upfront, calls buildTcTokenFromJid with the resolver, and assigns token nodes into pictureNode.content only when non-empty.

Sequence Diagram

sequenceDiagram
  participant profilePictureUrl as profilePictureUrl
  participant LIDMappingStore
  participant Cache as pn:/lid: Cache
  participant DB as lid-mapping DB
  participant tctoken as buildTcTokenFromJid

  profilePictureUrl->>LIDMappingStore: getKnownLIDForPN(pn)
  LIDMappingStore->>LIDMappingStore: validate & decode PN
  LIDMappingStore->>Cache: check pn:<user> mapping
  alt cache hit
    Cache-->>LIDMappingStore: cached lid:<user>
  else cache miss
    LIDMappingStore->>DB: fetch lid-mapping for PN user
    DB-->>LIDMappingStore: mapping result
    LIDMappingStore->>Cache: update pn: & lid: caches
  end
  LIDMappingStore-->>profilePictureUrl: formatted LID JID
  profilePictureUrl->>tctoken: buildTcTokenFromJid(jid, getKnownLIDForPN)
  tctoken-->>profilePictureUrl: token nodes
  profilePictureUrl->>profilePictureUrl: assign to pictureNode.content
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 I hopped through code to find the link,
PN to LID in just a blink.
Tokens built with nimble cheer,
Profile pics now know who's near.
Hooray — a tidy lookup, clear and quick!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: nesting profile picture tctoken and avoiding USync on lookup by adding a cached LID lookup method.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.


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.

@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/Signal/lid-mapping.ts`:
- Around line 121-126: The code reads stored = await
this.keys.get('lid-mapping', [pnUser]) and unguardedly sets lidUser =
stored[pnUser] then caches and formats it; if stored[pnUser] is non-string we
may cache invalid JIDs. Update the block that handles the result from keys.get
(the stored variable and lidUser) to check typeof lidUser === 'string' before
calling this.mappingCache.set(`pn:${pnUser}`, lidUser) or
this.mappingCache.set(`lid:${lidUser}`, pnUser) and before using lidUser for any
JID formatting—mirror the same guard used in getLIDsForPNs to bail out or skip
caching when lidUser is not a string.
🪄 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: CHILL

Plan: Pro

Run ID: 517a657d-8490-4c0b-b8f6-d89f9d171f80

📥 Commits

Reviewing files that changed from the base of the PR and between 78e7e4e and a2aefa0.

📒 Files selected for processing (2)
  • src/Signal/lid-mapping.ts
  • src/Socket/chats.ts

Comment thread src/Signal/lid-mapping.ts
rsalcara added a commit to rsalcara/InfiniteAPI that referenced this pull request Jun 7, 2026
PR #510 cubic review (Thread 3, P3) flagged that `buildTcTokenFromJid` and
`buildTcTokenNode` carried byte-for-byte identical retrieval / expiry /
opportunistic-cleanup pipelines — only the return shape differed (sibling
array vs single node). Extracted that pipeline into a private
`resolveTcTokenForJid(...)` helper so the two public builders become thin
adapters around a single source of truth.

Behavioral contract preserved (verified against the existing 78 tests):

  - Both public builders keep their exact public signatures.
  - `buildTcTokenFromJid` still mutates `baseContent` in place when a
    token exists, and still returns `baseContent.length > 0 ? baseContent
    : undefined` on every "no token" branch — same legacy behavior the
    presenceSubscribe call site relies on.
  - `buildTcTokenNode` still returns a single `BinaryNode` or `undefined` —
    same shape profilePictureUrl uses for nesting under `<picture>`.
  - The opportunistic expired-token wipe (with senderTimestamp
    preservation) runs exactly once, in the helper, with the same write
    shape as before. Missing-token entries are still NOT wiped.
  - Key-store errors are still swallowed inside the helper; both public
    builders fall back to their no-token branch on throw.

Why the two builders stay as separate public surfaces (instead of one):

  - `presenceSubscribe` builds a `<presence>` whose content is the legacy
    sibling-array shape; it cares about preserving `baseContent` when no
    token exists. Forcing it to the single-node shape would require every
    call site to special-case `undefined`.
  - `profilePictureUrl` (port of PR WhiskeySockets#2614) needs the token nested as a
    CHILD of `<picture>`. It MUST get a single node, not an array.

So the API split is meaningful; only the internal implementation was
duplicated. The helper kills the duplication without changing the surface.

Test impact: 0 changes to the test suite, 78/78 still pass. Same numbers
for the LID-mapping suite (15) and identity-change suite (23) that
indirectly exercise the same signal-store paths — 116/116 in the
combined run.

Out of scope (intentionally NOT touched):
  - Carousel, lists, buttons, polls, view-once, biz quality_control,
    useLegacyLock, the broader TC token issuance flow (only retrieval
    helpers refactored), LID↔PN batched, Phase 9 multi-DB,
    lidDbMigrated:false, the cacheMetricsInterval memory-leak fix, schema
    migrations + statement cache + busy retry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
rsalcara added a commit to rsalcara/InfiniteAPI that referenced this pull request Jun 7, 2026
…dd id/invite/persona_id/common_gid (#510)

* fix(profile-picture): WA Web parity — nest tctoken + add id/invite/persona_id/common_gid

Port of upstream WhiskeySockets#2614 + adicional gaps identified by
comparing our `profilePictureUrl` against WA Web (live source via CDP) and
whatsmeow source code. Brings our `<iq xmlns="w:profile:picture">` stanza
to byte-for-byte parity with the official client behavior.

What changes:

1. New `LIDMappingStore.getKnownLIDForPN(pn)` (port of PR WhiskeySockets#2614, literal).
   Returns the LID for a PN **only if locally known** (cache or store) — never
   triggers USync. Opportunistic call sites (profile-picture, etc.) use this
   so the failure-to-resolve path does NOT fire a USync round-trip. USync-
   on-profile-picture is not a behavior WA Web or whatsmeow emit; doing it
   makes our traffic profile stand out and may serve as a ban signal.

2. New `buildTcTokenNode({ authState, jid, getLIDForPN })` helper in
   tc-token-utils.ts. Same expiry / opportunistic-cleanup semantics as the
   pre-existing `buildTcTokenFromJid`, but returns the single <tctoken>
   BinaryNode (or undefined) instead of pushing into a content array. Callers
   that need the token as a nested CHILD of another node use this; the older
   `buildTcTokenFromJid` is kept intact for `presenceSubscribe` which uses
   the sibling-array shape.

3. `profilePictureUrl` refactored to match WA Web's
   `WASmaxOutProfilePictureGetRequest` mixin composition:

   - tctoken is now nested as a CHILD of <picture> (was a sibling — verified
     wrong against WA Web JS source: mergeStanzas(<picture>, <tctoken>)
     places tctoken inside picture, NOT alongside it).
   - tctoken resolver swapped to getKnownLIDForPN (no USync, PR WhiskeySockets#2614).
   - New optional params accepted in `opts`:
       * existingId  → <picture id="..."> for 304-NotModified caching
                       (matches WA Web pictureId + whatsmeow ExistingID).
                       Saves a CDN re-fetch when the picture hasn't changed.
       * invite      → <picture invite="..."> for group-invite-link preview
                       (fetch a group's picture without joining). When set,
                       we skip the tctoken (the invite IS the authorization).
       * personaId   → <picture persona_id="..."> for Meta AI bot personas.
       * commonGid   → <picture common_gid="..."> required when the target's
                       privacy is "My contacts" and we share a group but are
                       not in their contacts — without it the server returns
                       401/403. Matches WA Web.

   - Same callers using the old signature work unchanged (all new params are
     in an optional `opts` object).

4. `presenceSubscribe` is intentionally NOT changed — it doesn't wrap the
   tctoken inside a `<picture>` (no nesting question), and a USync miss
   during presence-subscribe is far less common (we usually know the LID by
   the time we subscribe). Kept on `buildTcTokenFromJid` + `getLIDForPN`.

Empirical validation — triple-confirmed before applying:

  a) PR WhiskeySockets#2614 description (`fix: nest profile picture tctoken and avoid
     usync on lookup`) plus cubic + coderabbit summaries.
  b) whatsmeow's `Client.GetProfilePictureInfo` in user.go — pictureContent
     is the `Content` of the picture node (Go semantics: child).
  c) WA Web's live JS bundle, extracted via Chrome DevTools Protocol
     (`Debugger.getScriptSource`). The mixin chain in
     `WASmaxOutProfilePictureGetRequest` is:
       smax("picture", attrs)
         → optionalMerge(mergeAddRequestMixin, ...)
         → optionalMerge(mergeTCTokenMixin, ...)
         → optionalMerge(mergeAvatarMixin, ...)
     where `mergeStanzas(target, mixin)` places the mixin INSIDE target.
     The tctoken is undeniably a child of <picture>.

Tests:

  - Existing 72 tests in `tc-token.test.ts` continue to pass (no signature
    changes to `buildTcTokenFromJid`).
  - 6 new tests for `buildTcTokenNode` (valid token, missing, expired +
    cleanup, no-wipe-when-missing, error swallow, return shape).
  - Full TC token suite: 78/78 passing.

Out of scope (intentionally NOT touched):
  - Carousel, lists, buttons, polls, view-once, biz quality_control,
    useLegacyLock, the broader TC token issuance flow, LID↔PN batched, Phase
    9 multi-DB, lidDbMigrated:false, the cacheMetricsInterval memory-leak
    fix, schema migrations + statement cache + busy retry, presence-
    subscribe path (covered separately above).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(lid-mapping): wrap getKnownLIDForPN in checkDestroyed + trackOperation (PR #510 review)

PR #510 review caught a P2 contract violation: every other public method on
`LIDMappingStore` calls `this.checkDestroyed()` at entry and runs its body
inside `this.trackOperation(...)`. The new `getKnownLIDForPN` (added in this
PR as part of the WA Web profile-picture parity port) was missing both
guards.

Why this matters:

- Without `checkDestroyed()`: the method runs after `destroy()` has been
  called, when `this.keys` may already have been torn down. The first
  `await this.keys.get(...)` then races UAF on the underlying signal key
  store.
- Without `trackOperation()`: `destroy()` does not wait for this method's
  in-flight I/O before clearing the mapping cache. A concurrent call can
  observe a half-cleared cache (a `mappingCache.set(...)` against a Map
  that `destroy()` is about to `clear()`) and silently return stale data
  rather than failing fast.

Both threads (copilot + cubic) flagged the same issue, cubic with
confidence 9. The fix is identical to the suggestion both reviewers
provided: wrap the body in `this.trackOperation(async () => { … })`
preceded by `this.checkDestroyed()`.

Caller-side impact: none. `buildTcTokenNode` (the sole call site) has a
try/catch that swallows any throw — so the only effect a post-destroy
call has is "tctoken not attached" (same as the existing miss path), not
a crash.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(tc-token): extract resolveTcTokenForJid helper (PR #510 review)

PR #510 cubic review (Thread 3, P3) flagged that `buildTcTokenFromJid` and
`buildTcTokenNode` carried byte-for-byte identical retrieval / expiry /
opportunistic-cleanup pipelines — only the return shape differed (sibling
array vs single node). Extracted that pipeline into a private
`resolveTcTokenForJid(...)` helper so the two public builders become thin
adapters around a single source of truth.

Behavioral contract preserved (verified against the existing 78 tests):

  - Both public builders keep their exact public signatures.
  - `buildTcTokenFromJid` still mutates `baseContent` in place when a
    token exists, and still returns `baseContent.length > 0 ? baseContent
    : undefined` on every "no token" branch — same legacy behavior the
    presenceSubscribe call site relies on.
  - `buildTcTokenNode` still returns a single `BinaryNode` or `undefined` —
    same shape profilePictureUrl uses for nesting under `<picture>`.
  - The opportunistic expired-token wipe (with senderTimestamp
    preservation) runs exactly once, in the helper, with the same write
    shape as before. Missing-token entries are still NOT wiped.
  - Key-store errors are still swallowed inside the helper; both public
    builders fall back to their no-token branch on throw.

Why the two builders stay as separate public surfaces (instead of one):

  - `presenceSubscribe` builds a `<presence>` whose content is the legacy
    sibling-array shape; it cares about preserving `baseContent` when no
    token exists. Forcing it to the single-node shape would require every
    call site to special-case `undefined`.
  - `profilePictureUrl` (port of PR WhiskeySockets#2614) needs the token nested as a
    CHILD of `<picture>`. It MUST get a single node, not an array.

So the API split is meaningful; only the internal implementation was
duplicated. The helper kills the duplication without changing the surface.

Test impact: 0 changes to the test suite, 78/78 still pass. Same numbers
for the LID-mapping suite (15) and identity-change suite (23) that
indirectly exercise the same signal-store paths — 116/116 in the
combined run.

Out of scope (intentionally NOT touched):
  - Carousel, lists, buttons, polls, view-once, biz quality_control,
    useLegacyLock, the broader TC token issuance flow (only retrieval
    helpers refactored), LID↔PN batched, Phase 9 multi-DB,
    lidDbMigrated:false, the cacheMetricsInterval memory-leak fix, schema
    migrations + statement cache + busy retry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for 14 days with no activity. Remove the stale label or comment or this will be closed in 14 days

@github-actions github-actions Bot added the Stale label Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants