Skip to content

feat(tctoken): attach and issue tctoken on outgoing VoIP call offers - #970

Merged
jlucaso1 merged 4 commits into
mainfrom
claude/whatsapp-rust-token-review-7jsnxz
Jul 3, 2026
Merged

feat(tctoken): attach and issue tctoken on outgoing VoIP call offers#970
jlucaso1 merged 4 commits into
mainfrom
claude/whatsapp-rust-token-review-7jsnxz

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes the last documented gap in the trusted-contact token sweep: outgoing VoIP call offers. WhatsApp Web attaches the callee's trusted-contact token to the call offer and issues a fresh token to the callee when starting a call (sendTcToken in StartCall.js); the offer builder already had a <privacy> slot but place_call was passing None, and no issuance ran on the call path.

Changes

  • voip::facade::place_call — attaches the callee's stored (non-expired) tctoken to the offer's leading <privacy> node via lookup_tc_token_for_jid, the same token the 1:1 message and presence-subscribe paths already attach. Absent when we hold no valid token.
  • Post-send issuance — after a successful offer send, fire-and-forget issues our token to the callee (spawn_call_tc_token_issuance), rate-limited by the sender bucket so repeat calls to the same contact within a window don't re-issue. It's detached, so it never blocks call setup on the IQ round-trip. This prevents 463 nacks on later offers to privacy-restricted contacts.
  • should_issue_tc_token — new Client helper (voip-gated) exposing just the sender-bucket issuance decision the message path inlines in maybe_include_tc_token. issue_tc_token_after_send widened from pub(super) to pub(crate) so the call path can reuse it.
  • Doc — the stale TODO: VoIP call integration note in features/tctoken.rs is updated to reflect the wired 1:1 path; group-call initiation isn't implemented yet and should attach/issue per participant the same way when added.

Scope

place_call is the only outgoing offer path (1:1); group calls are inbound-only (offer_notice parsing) today, so there is no group initiation site to wire.

Testing

  • should_issue_tc_token returns true for an unknown contact and false within the current sender bucket.
  • place_call_attaches_stored_tctoken_as_privacy_node seeds a token and asserts it rides as the offer's leading <privacy> child; the existing no-token offer test still asserts no <privacy> node.
  • cargo fmt, cargo clippy --features voip, default and --features voip builds all clean; full voip::facade suite (30 tests) green.

Generated by Claude Code

Outgoing 1:1 call offers now carry the callee's stored trusted-contact
token as the offer's <privacy> node and issue a fresh token after send,
mirroring WhatsApp Web's sendTcToken in StartCall.js. Prevents 463 nacks
on call offers to privacy-restricted contacts. Issuance is fire-and-forget
and rate-limited by the sender bucket, so it never blocks call setup.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jlucaso1, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2c85ceeb-fde7-45e1-a987-e2d9cbd35ede

📥 Commits

Reviewing files that changed from the base of the PR and between 30b867a and 9d2407f.

📒 Files selected for processing (1)
  • src/send/tctoken_lifecycle.rs
📝 Walkthrough

Walkthrough

Adds trusted-contact token support to outgoing VOIP call offers: place_call now attaches a stored token to the offer privacy node and starts a detached, rate-limited post-send issuance path for the callee, with docs and tests updated accordingly.

Changes

VOIP trusted-contact token flow

Layer / File(s) Summary
Updated tctoken documentation
src/features/tctoken.rs
Module docs replace the prior TODO with current 1:1 call token attachment and post-send issuance behavior, and note group calls remain unimplemented.
Issuance decision helper
src/send/tctoken_lifecycle.rs
issue_tc_token_after_send is now crate-visible, and a VOIP-gated should_issue_tc_token helper skips self, bots, and status-broadcast recipients, reads stored sender_timestamp, applies rate limiting, and is covered by async tests.
Call offer token attachment and post-send issuance
src/voip/facade.rs
place_call looks up the callee’s stored trusted-contact token, passes it into build_offer, and starts detached post-send issuance; the outgoing offer test verifies the <privacy> node contents.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: api-design

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: attaching and issuing trusted-contact tokens for outgoing VoIP call offers.
Description check ✅ Passed The description is clearly aligned with the implemented VoIP tctoken attachment, issuance, and helper changes.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/whatsapp-rust-token-review-7jsnxz

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.

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown

Greptile Summary

This PR wires the trusted-contact token (tctoken) sweep into outgoing VoIP call offers, closing the last documented gap. On place_call, it attaches the callee's stored non-expired token to the offer's <privacy> node and fire-and-forgets a sender-bucket-rate-limited issuance after the offer send succeeds.

  • Token attachmentlookup_tc_token_for_jid(peer) is called before build_offer, so the callee receives the offer with the correct <privacy> node (mirrors how 1:1 message and presence-subscribe paths already attach tokens).
  • Post-send issuancespawn_call_tc_token_issuance upgrades the client's self-weak, then checks should_issue_tc_token (new, voip-gated helper) before calling the now-pub(crate) issue_tc_token_after_send; both helpers are covered by three new unit tests.
  • Refactor — the inline self-JID test in maybe_include_tc_token is extracted into is_own_jid, shared across the message and call paths to prevent future drift.

Confidence Score: 5/5

Safe to merge — the new token attachment and post-offer issuance follow established patterns, are guarded correctly, and the fire-and-forget issuance is detached so it cannot block or abort the call setup path.

The changes are tightly scoped: one read added before building the offer, one detached spawn after a successful send, and two helpers that consolidate already-proven logic. The issuance path can fail silently without affecting the call (explicitly designed as best-effort). Tests cover the three material cases for should_issue_tc_token and the offer-carries-token assertion.

No files require special attention.

Important Files Changed

Filename Overview
src/voip/facade.rs Attaches stored tctoken to outgoing call offer and spawns fire-and-forget post-send issuance; mirrors the relay-waiter spawn pattern correctly.
src/send/tctoken_lifecycle.rs Extracts is_own_jid helper, widens issue_tc_token_after_send to pub(crate), adds voip-gated should_issue_tc_token with three covering tests.
src/features/tctoken.rs Doc-comment-only change updating the TODO note to reflect the wired 1:1 VoIP path.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller as place_call
    participant DB as persistence_manager
    participant Net as send_node
    participant Spawn as spawn_call_tc_token_issuance
    participant IQ as issue_tc_token_after_send

    Caller->>DB: lookup_tc_token_for_jid(peer)
    DB-->>Caller: Option privacy_token
    Note over Caller: Build offer with privacy_token
    Caller->>Net: send_node(offer)
    Net-->>Caller: Ok
    Caller->>Spawn: spawn detached
    activate Spawn
    Spawn->>DB: should_issue_tc_token(peer)
    DB-->>Spawn: bool
    alt should issue
        Spawn->>IQ: issue_tc_token_after_send(peer)
        IQ->>DB: record_tc_token_sender_timestamp
    end
    deactivate Spawn
    Caller-->>Caller: return CallHandle
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller as place_call
    participant DB as persistence_manager
    participant Net as send_node
    participant Spawn as spawn_call_tc_token_issuance
    participant IQ as issue_tc_token_after_send

    Caller->>DB: lookup_tc_token_for_jid(peer)
    DB-->>Caller: Option privacy_token
    Note over Caller: Build offer with privacy_token
    Caller->>Net: send_node(offer)
    Net-->>Caller: Ok
    Caller->>Spawn: spawn detached
    activate Spawn
    Spawn->>DB: should_issue_tc_token(peer)
    DB-->>Spawn: bool
    alt should issue
        Spawn->>IQ: issue_tc_token_after_send(peer)
        IQ->>DB: record_tc_token_sender_timestamp
    end
    deactivate Spawn
    Caller-->>Caller: return CallHandle
Loading

Reviews (4): Last reviewed commit: "refactor(tctoken): extract shared is_own..." | Re-trigger Greptile

Comment thread src/voip/facade.rs Outdated
Follows AGENTS.md: comments explain why, not what. Removes narration of
lookup_tc_token_for_jid / spawn_call_tc_token_issuance at the call sites,
the codebase-trivia paragraph on should_issue_tc_token, and the verbose
test-block comment.

@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/send/tctoken_lifecycle.rs`:
- Around line 147-174: The VoIP tc-token issuance helper still allows self-calls
to proceed, so add the same self-JID guard used elsewhere before the bot/status
checks in should_issue_tc_token. Update should_issue_tc_token in
tctoken_lifecycle to resolve the peer and return false when it matches our own
PN/LID identity, so a call to ourselves never issues or records a tc token.
🪄 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 (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 59c006c7-c55a-4ff3-ad19-cd513c75a71c

📥 Commits

Reviewing files that changed from the base of the PR and between 0b1d349 and bf65283.

📒 Files selected for processing (3)
  • src/features/tctoken.rs
  • src/send/tctoken_lifecycle.rs
  • src/voip/facade.rs

Comment thread src/send/tctoken_lifecycle.rs

@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.

0 issues found across 2 files (changes from recent commits).

Requires human review: Changes add token attachment and issuance to call flow, affecting security and privacy. Requires human review to ensure correct token handling and no regression in call setup.

Re-trigger cubic

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 3, 2026

@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.

1 issue found and verified against the latest diff

Confidence score: 4/5

  • In src/send/tctoken_lifecycle.rs, should_issue_tc_token appears to miss a self-JID check on the outgoing-call path, so a self-resolved peer could trigger token issuance/recording for our own account; this could create incorrect token state or noisy background tasks if merged as-is — add an explicit self-JID guard before spawning the issuance task.

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/send/tctoken_lifecycle.rs
should_issue_tc_token now excludes our own PN/LID before the bot/status
check, matching maybe_include_tc_token, so a self-call never issues or
records a token for our own account.
@greptile-apps
greptile-apps Bot dismissed their stale review July 3, 2026 22:34

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@coderabbitai coderabbitai Bot removed the api-design label Jul 3, 2026

@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.

♻️ Duplicate comments (1)
src/send/tctoken_lifecycle.rs (1)

155-169: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Good, the self-call hole is patched. We flagged this before and it's fixed now — no more issuing tokens to ourselves. That's the kind of correctness bar we need on privacy-sensitive paths.

One thing though: this exact block is now copy-pasted from maybe_include_tc_token (lines 30-40). We don't duplicate security-relevant logic like this at scale — if someone tweaks the self-check in one spot and forgets the other, we're back to square one. Pull it into a small private helper both call.

♻️ Proposed refactor
+    fn is_own_jid(&self, jid: &Jid) -> bool {
+        let snapshot = self.persistence_manager.get_device_snapshot();
+        snapshot.pn.as_ref().is_some_and(|pn| pn.is_same_user_as(jid))
+            || snapshot.lid.as_ref().is_some_and(|lid| lid.is_same_user_as(jid))
+    }
+
     pub(super) async fn maybe_include_tc_token(
         &self,
         to: &Jid,
         extra_nodes: &mut Vec<Node>,
     ) -> bool {
         ...
-        let snapshot = self.persistence_manager.get_device_snapshot();
-        let is_self = snapshot
-            .pn
-            .as_ref()
-            .is_some_and(|pn| pn.is_same_user_as(to))
-            || snapshot
-                .lid
-                .as_ref()
-                .is_some_and(|lid| lid.is_same_user_as(to));
-        if is_self {
+        if self.is_own_jid(to) {
             return false;
         }
     pub(crate) async fn should_issue_tc_token(&self, to: &Jid) -> bool {
         use wacore::iq::tctoken::should_send_new_tc_token_with;

-        let snapshot = self.persistence_manager.get_device_snapshot();
-        let is_self = snapshot
-            .pn
-            .as_ref()
-            .is_some_and(|pn| pn.is_same_user_as(to))
-            || snapshot
-                .lid
-                .as_ref()
-                .is_some_and(|lid| lid.is_same_user_as(to));
-        if is_self {
+        if self.is_own_jid(to) {
             return false;
         }
🤖 Prompt for 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.

In `@src/send/tctoken_lifecycle.rs` around lines 155 - 169, The self-call guard
logic in tctoken_lifecycle is duplicated between the token issuance path and
maybe_include_tc_token, so extract the shared self-check into a small private
helper and have both call it. Update the existing boolean logic around self
detection using persistence_manager.get_device_snapshot, pn/lid, and
is_same_user_as so there is one authoritative implementation, then replace both
inline copies with that helper.
🤖 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.

Duplicate comments:
In `@src/send/tctoken_lifecycle.rs`:
- Around line 155-169: The self-call guard logic in tctoken_lifecycle is
duplicated between the token issuance path and maybe_include_tc_token, so
extract the shared self-check into a small private helper and have both call it.
Update the existing boolean logic around self detection using
persistence_manager.get_device_snapshot, pn/lid, and is_same_user_as so there is
one authoritative implementation, then replace both inline copies with that
helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e7d4b10e-af80-44b3-8ba2-e1e56a06dbcf

📥 Commits

Reviewing files that changed from the base of the PR and between 3b7fbbe and 30b867a.

📒 Files selected for processing (1)
  • src/send/tctoken_lifecycle.rs

@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.

0 issues found across 1 file (changes from recent commits).

Requires human review: Adds trusted-contact token issuance to VoIP call path, a business logic change in core communication features with production infrastructure impact. Requires human review for correctness and security.

Re-trigger cubic

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 3, 2026
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.74 MiB 10.74 MiB 0
bin .text 8.74 MiB 8.74 MiB -128 B (-0.00%) 🔽
bin allocated (text+data+bss) 10.73 MiB 10.73 MiB -32 B (-0.00%) 🔽
llvm-lines wacore 503,148 503,148 0
llvm-lines wacore copies 17,243 17,243 0
llvm-lines whatsapp-rust lib 737,618 737,616 -2 (-0.00%) 🔽
llvm-lines whatsapp-rust lib copies 23,868 23,865 -3 (-0.01%) 🔽
deps crates (Cargo.lock) 466 466 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.58 MiB 1.58 MiB -119 B (-0.01%) 🔽
.text wacore 530.89 KiB 530.89 KiB 0
.text wacore_binary 157.49 KiB 157.49 KiB 0
.text wacore_libsignal 178.30 KiB 178.30 KiB 0
.text wacore_appstate 156.16 KiB 156.16 KiB 0
.text wacore_noise 26.05 KiB 26.05 KiB 0
.text waproto 1.60 MiB 1.60 MiB 0
.text whatsapp_rust_sqlite_storage 507.75 KiB 507.75 KiB 0
.text whatsapp_rust_tokio_transport 43.50 KiB 43.50 KiB 0
.text whatsapp_rust_ureq_http_client 9.08 KiB 9.08 KiB 0
.text std 1020.91 KiB 1020.91 KiB +6 B (+0.00%) 🔺
.text other deps 2.94 MiB 2.94 MiB 0

Baseline: d1fe9fe93 (latest main run) · Head: b72f7b297 · Graphs

The self-JID guard was duplicated between maybe_include_tc_token and
should_issue_tc_token. Extract it into a single is_own_jid helper so the
message and call paths can't drift apart.
@greptile-apps
greptile-apps Bot dismissed their stale review July 3, 2026 22:45

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@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.

0 issues found across 1 file (changes from recent commits).

Requires human review: Adds trusted-contact token logic to outgoing VoIP calls, modifying call setup and token issuance. These are business logic changes in a core path that require human review.

Re-trigger cubic

@jlucaso1
jlucaso1 merged commit dbdd240 into main Jul 3, 2026
18 checks passed
@jlucaso1
jlucaso1 deleted the claude/whatsapp-rust-token-review-7jsnxz branch July 3, 2026 22:55
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