diff --git a/api/tctoken.mdx b/api/tctoken.mdx index 3e07c66a..2206735e 100644 --- a/api/tctoken.mdx +++ b/api/tctoken.mdx @@ -224,10 +224,11 @@ client.send_message(jid, message).await?; let picture = client.contacts().get_profile_picture(&jid, true).await?; ``` -The tctoken (no cstoken fallback) is also attached automatically on two other privacy-gated IQs, matching WA Web's `USyncStatusProtocol`/`OutSpamTCTokenMixin`: +The tctoken (no cstoken fallback) is also attached automatically at three other call sites that use privacy tokens, matching WA Web's `USyncStatusProtocol`/`OutSpamTCTokenMixin`/`StartCall.js`: - **`get_user_info`** — a per-recipient tctoken is attached to each queried `` node in the usync IQ, so status/about for a privacy-restricted contact resolves instead of coming back hidden. Gated behind `profile_scraping_privacy_token_in_about_usync`. - **`send_spam_report`** — when `SpamReportRequest::from_jid` is set, that contact's tctoken is attached to the spam report IQ so the report is accepted for a privacy-restricted account. Gated behind `enable_spam_report_iq_with_privacy_token`. Group reports that only set `group_jid`/`participant_jid` (no `from_jid`) don't get a token attached. +- **Outgoing 1:1 call offers** (`voip` feature) — placing a call attaches the callee's stored, unexpired tctoken as the offer's leading `` node, and issues a fresh token to the callee in the background after the offer sends. This mirrors WA Web's `sendTcToken` in `StartCall.js` and prevents 463 nacks on later offers to a privacy-restricted contact. Unlike the other paths, it isn't gated by any AB prop — issuance is rate-limited only by the same sender bucket that governs message reissuance (see [Post-send issuance](#post-send-issuance)). Group-call initiation isn't implemented yet. ```rust // Attaches the queried contact's tctoken automatically when the AB prop is on @@ -235,6 +236,9 @@ let info = client.contacts().get_user_info(&[jid]).await?; // Attaches the reported contact's tctoken automatically when the AB prop is on let result = client.send_spam_report(request).await?; + +// Attaches the callee's tctoken automatically, then issues a fresh one after the offer sends +let handle = client.voip().call(&peer).audio(mic_source, speaker_sink).start().await?; ``` ### AB prop gating @@ -248,6 +252,8 @@ Token *issuance scheduling* (requesting new tokens from the server after sending The usync and spam-report attachment points each have their own independent gating prop (`profile_scraping_privacy_token_in_about_usync`, `enable_spam_report_iq_with_privacy_token`) — unrelated to the message-stanza props above, and with no cstoken fallback. +Outgoing call offers (`voip` feature) attach and issue tokens **unconditionally** — there is no AB prop gate for this path at all, matching WA Web's `StartCall.js`. + ### Post-send issuance After sending a 1:1 message, the library checks whether a new token should be issued for the recipient. If the sender-side bucket has rolled over since the last issuance, a background IQ request fires. @@ -380,6 +386,17 @@ sequenceDiagram Client->>Client: Resolve sender LID Client->>Backend: store_received_tc_token (monotonicity guard, preserves sender_timestamp) Client->>Server: Re-subscribe presence + + Note over App,Server: Outgoing 1:1 call offer (voip feature, unconditional — no AB prop gate) + App->>Client: voip().call(&peer).audio(...).start() + Client->>Backend: Get tc_token for peer + Backend-->>Client: TcTokenEntry or None + Client->>Server: Call offer, tctoken as leading child if stored + Server-->>Client: Offer ack + Client-->>App: CallHandle + Client->>Server: IQ set (issue fresh token to callee, if sender bucket rolled over) + Server-->>Client: IQ success + Client->>Backend: touch_tc_token_sender_timestamp ``` ## Expiration diff --git a/guides/voip-calls.mdx b/guides/voip-calls.mdx index 48ea029d..cd930686 100644 --- a/guides/voip-calls.mdx +++ b/guides/voip-calls.mdx @@ -90,6 +90,10 @@ client.voip() handle.wait_ended().await; ``` + + If the callee has a stored [trusted-contact token](/api/tctoken), it's attached to the offer automatically, and a fresh token is issued to them in the background afterward if the sender-side bucket has rolled over — matching WhatsApp Web's `sendTcToken` in `StartCall.js`. This prevents 463 nacks on calls to privacy-restricted contacts and needs no action from the caller. Group-call initiation doesn't implement this yet. + + ## Audio I/O You supply the audio I/O by implementing the `AudioSource` and `AudioSink` traits. Both traits are channel-based — the library reads from a `Receiver` and writes decoded PCM to a `Sender`. The bundled `examples/voip-cli/src/main.rs` wires up [cpal](https://crates.io/crates/cpal)/PipeWire as a reference.