Compare commits

..

19 Commits

Author SHA1 Message Date
Renato Alcara 4d00e8f9ce docs(audit): clean 2 more stale circuit-breaker mentions found in /audit
- structured-logger.ts header bullet "External system integration via
  hooks with circuit breaker" -> updated to describe current
  fire-and-forget hook semantics + hookFailures metric counter.
- retry-utils.ts retryConfigs.rsocket comment referenced the
  prometheus-metrics -> circuit-breaker.ts circular import chain.
  circuit-breaker.ts is gone; trimmed the parenthetical to keep just
  the core reasoning (avoid ESM init-order issues via prometheus-metrics).

No code change. Build clean, 35/35 suites, 809/809 tests pass.
2026-04-27 01:03:27 -03:00
Renato Alcara 4687a2a86f docs(unified-session): update class docstring (no more circuit breaker)
PR #393 review (Copilot): the class-level JSDoc still listed "Circuit
breaker protection" as a feature, but the protection was removed in
commit b531b46999 in favour of best-effort single-attempt semantics
(telemetry is non-critical — failures are swallowed and counted in the
hookFailures metric).

Updated the bullet point to describe what actually happens now.

Note: not addressing the breaking-API change comments (removed
enableCircuitBreaker option from UnifiedSessionOptions, removed
circuitBreakers/getCircuitBreakerStats/resetCircuitBreakers from
makeSocket() return, removed circuit_breaker_* Prometheus metrics)
per maintainer policy — this fork has a single consumer (zpro.io),
downstream forks absorb on merge.
2026-04-27 00:58:26 -03:00
Renato Alcara d5796b36b6 docs(retry): clarify abort semantics + remove stale circuit-breaker mention
Two doc-only fixes from PR #393 review (Copilot):

1. **bounded-retry.ts BoundedRetryOptions.signal**
   The previous one-line "if aborted, give up immediately" was technically
   correct from the loop's perspective but misleading about what happens
   to an in-flight operation. Expanded to describe the three observation
   points: sleep wakes immediately, in-flight attempt is forwarded the
   per-attempt signal (only truly immediate if the operation itself
   observes it), and the next loop iteration throws BoundedRetryAbortedError.

2. **retry-utils.ts module-level JSDoc**
   The header still listed "Circuit breaker integration" as a feature.
   The circuitBreaker option was removed in commit b531b46999 and the
   withRetry/retryable/RetryManager helpers in 0f2402da58. Updated the
   header to describe the actual current surface (used for Bad MAC
   recovery on decrypt path) and recommend withBoundedRetry for socket
   operations.

Build clean, 35/35 suites, 809/809 tests pass.

Note: not addressing the suppressed comment about removed APIs being a
breaking change — per maintainer policy this fork has a single consumer,
downstream forks absorb on merge.
2026-04-27 00:42:09 -03:00
Renato Alcara f914ffe272 fix(bounded-retry): address PR #393 second-round review
All 5 issues verified as REAL (not false positives) and fixed:

1. **CRITICAL: query() swallows timeouts → bounded-retry treats as success**
   (CodeRabbit Critical, line 319 socket.ts)
   waitForMessage() returns undefined on timeout instead of throwing.
   queryInternal() returns that undefined unchanged. uploadToServer
   was `await query(...)` then logging success regardless — a timed-out
   upload would update lastUploadTime with no retry triggered.

   Fix: validate response shape inside uploadToServer:
       if (!result) throw new Boom('Pre-key upload query timed out (no response)', { statusCode: 408 })
   Bounded-retry now sees the throw and properly retries.

2. **MAJOR: Outer Promise.race + bounded-retry can run in parallel**
   (CodeRabbit Major + Copilot, line 674 socket.ts)
   With safety margin (ttlMs=28s vs UPLOAD_TIMEOUT=30s) bounded-retry
   should always reach give-up first, but if anything bypasses that
   bound the outer race rejects while bounded-retry keeps running.

   Fix: wire AbortController. Pass its signal to bounded-retry.
   On outer race timeout, abort the controller → bounded-retry stops
   cleanly with BoundedRetryAbortedError. uploadToServer also honours
   the signal before logging success.

3. **MINOR: sleep-leak test does not observe the leak**
   (CodeRabbit Minor)
   Test only triggered 4 sleeps — below Node's 11-listener warning —
   and never captured warning events.

   Fix: spy on signal.addEventListener / removeEventListener and
   assert add count == remove count.

4. **MINOR: structured-logger thenable .catch can throw**
   (Copilot)
   Code called (hookResult as Promise).catch(...) directly. Fails on
   thenables that only implement .then.

   Fix: use Promise.resolve(hookResult).catch(...) which adapts any
   thenable safely.

5. **Internal: bounded-retry catch was double-removing outer-abort listener**
   Found by the new spy-based test. Both catch and finally called
   removeEventListener. Removed redundant catch-block remove; finally
   is single source of truth.

Build clean, 35/35 suites, 809/809 tests pass.
2026-04-27 00:30:37 -03:00
Renato Alcara 73d63b6304 fix(prekey): align bounded-retry ttl with outer UPLOAD_TIMEOUT race
Bug found while reviewing pre-key auto-replenishment behavior with the
user. uploadPreKeys is wrapped in:

    Promise.race([uploadLogic(), 30s timeout])    ← UPLOAD_TIMEOUT

Inside uploadLogic, my previous commit configured bounded-retry with
ttlMs=60_000 (60s). Result: outer race always wins at 30s, bounded-retry
never reaches its natural give-up. We lose the structured logs / metrics
emitted on TTL exhaustion, AND if a retry was about to succeed at second
29, it gets killed by the outer race.

Fix:
- ttlMs reduced to UPLOAD_TIMEOUT - 2_000 = 28_000ms (safety margin so
  bounded-retry always reaches give-up before the outer race).
- perAttemptTimeoutMs reduced 20s -> 8s so multiple attempts fit in 28s.
- delays trimmed to [1000, 2000, 4000, 8000] (4 attempts, ≈15s with
  delays plus per-attempt time = comfortably within 28s budget).
- query(node) timeout aligned with PER_ATTEMPT_TIMEOUT_MS=8s.

Pre-key replenishment system (existed before this PR, untouched):
- MIN_PREKEY_COUNT=200 (replenishment threshold)
- INITIAL_PREKEY_COUNT=800 (top-up target — matches WA Business CDP
  capture: prekey-store=812 on registration)
- Triggered on: CB:success login, every 6h auto-sync, Bad MAC recovery,
  specific peer prekey miss.

Bounded-retry only handles HOW the upload retries on transient failure,
not WHEN. The two layers are orthogonal.

Build clean, 35/35 suites, 809/809 tests pass.
2026-04-27 00:14:23 -03:00
Renato Alcara 0700c44d59 chore(socket): remove dead retryCount param from uploadPreKeys
PR #393 review (low-confidence comment): the retryCount parameter and
its `=== 0` guard were leftovers from the previous manual recursive
backoff. Now that bounded-retry handles retries internally, the
recursion is gone and retryCount is always 0.

Drop the parameter, remove the guard (the check applies always), and
simplify the log statement (no more "retryCount" in the structured
data — bounded-retry's own logger logs attempt counts).

Build clean, 35/35 suites, 809/809 tests pass.
2026-04-27 00:07:05 -03:00
Renato Alcara b0030a0482 fix(bounded-retry): address PR #393 review — strict TTL, abort cancel, no leaks, structured logs
Addresses 7 issues from Codex P2 + Copilot + CodeRabbit Critical/Major:

1. **TTL enforced BEFORE each attempt** (Codex P2 + Copilot + CodeRabbit Major)
   Previously TTL was only checked AFTER a failure, so an attempt could
   start with 0ms remaining and run for the full perAttemptTimeoutMs.
   Now: check at top of loop. If already exceeded, throw immediately.

2. **Per-attempt timeout capped by remaining TTL** (Copilot)
   `attemptTimeoutMs = min(perAttemptTimeoutMs, ttlMs - elapsed)` so a
   single attempt cannot run past the wall-clock deadline.

3. **Operation receives AbortSignal for cancellation** (CodeRabbit Critical)
   `withTimeout` now aborts an `AbortController` when the timeout fires.
   The signal is forwarded to the operation, so callers can opt into
   cancelling in-flight work (e.g. `(signal) => fetch({ signal })`).
   Without this, a timed-out attempt's network call kept running while
   bounded-retry started the next attempt — wasted work + duplicate
   listeners.

4. **sleep() removes abort listener on normal completion** (Codex P2 +
   Copilot + CodeRabbit)
   Previously `signal.addEventListener('abort', onAbort, { once: true })`
   was added on every retry but never removed when the timer resolved.
   Long-lived signals reused across retries accumulated listeners. Now
   explicit cleanup on both timer-resolve and abort paths.

5. **Outer abort listener properly cleaned up in main loop**
   Adds + removes via try/finally to prevent leaks when the loop throws.

6. **Metrics use optional chaining** (Copilot)
   `metrics.socketEvents?.inc(...)` everywhere, matching the rest of the
   codebase pattern (handles partial mocks in tests).

7. **Structured logging** (user request)
   New optional `logger?: ILogger` field. Emits:
   - debug: each retry scheduling (`{op, attempt, delayMs, elapsedMs, error}`)
   - info: recovery after retries (`{op, attempts, elapsedMs}`)
   - warn: give-up via TTL or shouldRetry predicate

8. **socket.ts: align uploadPreKeys query timeout with bounded-retry**
   `query(node, PER_ATTEMPT_TIMEOUT_MS)` so a stale attempt does not
   keep an iq listener registered past bounded-retry's per-attempt
   deadline (Copilot).

9. **Doc updated**: removed claim about `maxAttempts` (option doesn't
   exist; bounded by ttlMs + delay-cap + per-attempt-timeout).

Tests: 18 pass (was 12; added 6 covering each fix).
Full suite: 35/35 suites, 809/809 tests pass.

NB: The "breaking API change" comments (Types/Socket.ts, Utils/index.ts,
structured-logger.ts) are intentional and not addressed — this fork has
a single consumer; downstream forks merging will absorb the change.
2026-04-27 00:05:28 -03:00
Renato Alcara d0bbf85e6a fix(lint): resolve no-unused-vars errors flagged by CI
- bounded-retry.ts:103 — drop unused generic parameter <T> from
  BoundedRetryOptions interface (and its single use at line 220).
  The generic served no purpose: the caller's return type is inferred
  from the operation function, not from the options.
- retry-utils.test.ts:5 — remove `beforeEach` from imports. It was
  only used by the RetryManager describe block, removed in commit
  0f2402da58.

Build clean, 58 tests pass (12 bounded-retry + 46 retry-utils).
2026-04-26 23:53:55 -03:00
Renato Alcara 0f2402da58 refactor(retry): remove dead helpers from retry-utils (withRetry, retryable, RetryManager)
Audit follow-up. After verifying the codebase, three exports of
retry-utils.ts have ZERO production callers:

- `withRetry` decorator (lines 419-440): unused
- `retryable` wrapper (lines 442-453): unused (1 self-test only)
- `RetryManager` class (lines 455-549): unused (1 self-test only)
  Note: `MessageRetryManager` in messages-send.ts is a separate class,
  not the same thing.

The kept exports are still used:
- `retry()` function: used by decode-wa-message.ts for Bad MAC recovery
- `RetryExhaustedError`, `RetryOptions`, `RetryContext`: used same place
- `RETRY_BACKOFF_DELAYS`, `RETRY_JITTER_FACTOR`: re-exported via Defaults
- `retryPredicates`, `retryConfigs`: helper presets (kept; small)

Removed `EventEmitter` import (was only used by the deleted RetryManager).
Removed `retryable` and `RetryManager` from test imports + their describe
blocks (5 tests removed, all targeting the deleted code).

Net: -148 lines from retry-utils.ts, -90 lines from its test file.
803 tests pass (was 809; 6 deleted tests covered the removed code).

Now the InfiniteAPI retry surface is:
- `retry-utils.ts:retry()` for Bad MAC recovery (decryption layer)
- `bounded-retry.ts:withBoundedRetry()` for socket operation retry
  (uploadPreKeys), with WhatsApp Android empirical defaults

No redundancy: each helper handles a distinct concern.
2026-04-26 23:35:21 -03:00
Renato Alcara 5017393277 refactor(logger): remove private CircuitBreaker from structured-logger
The structured-logger had its OWN private CircuitBreaker class for
external-hook protection — gate hook calls when the user's hook keeps
throwing, to prevent log explosion.

Replaced by simple try/catch + hookFailures metric increment:
- If the user's hook throws synchronously → catch + increment metric.
- If the hook returns a rejected promise → .catch + increment metric.
- We do NOT gate further calls. If the hook is broken, the metric
  goes up rapidly and operators see it. Consistent with the rest of
  the codebase after the socket-layer circuit-breaker removal:
  fast-fail, no state machines, no cross-call gating.

Removed:
- The internal CircuitBreaker class (lines 244-298, 55 lines)
- circuitBreakerThreshold + circuitBreakerResetMs config options
- circuitBreakerTrips metric field (LoggerMetrics)
- circuitBreakerState statistics field (LoggerStatistics)
- circuitBreaker init in constructor + null assignment in destroy()

Build passes; 19 structured-logger tests pass; 35/35 suites pass.
2026-04-26 23:15:47 -03:00
Renato Alcara c557714e72 chore(audit): clean up stale circuit-breaker references found in audit
Sweep after the main substitution found three stale references that
predated this PR but became misleading once circuit breaker was removed:

- src/Socket/socket.ts: keep-alive ping comment claimed it bypassed the
  circuit breaker. Rewritten to reflect the actual reason for using
  sendNode() (avoid response correlator overhead) with a brief
  historical note.
- src/Socket/socket.ts: comment in sendNode() init mentioned avoiding
  duplicating circuit breakers; updated to "manager state".
- src/Socket/socket.ts: queryInternal comment described a self-tripping
  loop in the circuit breaker; trimmed to keep only the relevant point
  about the outer-timeout race.
- src/Utils/prometheus-metrics.ts: 6 circuit_breaker_* metrics removed
  (state/trips/recoveries/rejections/successes/failures) — no longer
  emitted by anything in the codebase. Also removed the
  initialize-with-zero call in initializeMetricsWithLabels.
2026-04-26 23:05:04 -03:00
Renato Alcara c8056a967c docs: explain bounded-retry rationale (Frida-empirical defaults)
Expand the file-level JSDoc on bounded-retry.ts to document:
- Why circuit breaker was removed (cascading failures in production:
  5 timeouts in 60s blocked all queries for 30s).
- Empirical justification: Frida hook captures of WhatsApp Android
  showing per-operation exponential backoff (3s -> 10s -> 60s ->
  ~64s -> 120s cap), bounded memory, no retry storm on recovery,
  no global state machine.
- Design properties (per-op isolation, bounded by attempts+TTL,
  per-attempt timeout, abort support, memory bound).
- Guidance: when to use plain query() vs. withBoundedRetry().
- Three usage examples covering common patterns.

No code changes — comment-only.
2026-04-26 22:53:36 -03:00
Renato Alcara 24329e9ba1 chore: remove circuit-breaker module and tests
Delete src/Utils/circuit-breaker.ts (767 lines) and the corresponding
test file src/__tests__/Utils/circuit-breaker.test.ts (371 lines).

Replaced by src/Utils/bounded-retry.ts (added in earlier commit). All
remaining circuit-breaker references in the codebase are explanatory
comments in code that previously used circuit breakers — kept for
git-history context.

Final state: 35/35 test suites pass, 809 tests pass.
2026-04-26 22:52:18 -03:00
Renato Alcara b70d144699 refactor(tests): adapt unified-session tests to no-circuit-breaker world
- Remove circuitBreakerTrips from the prometheus-metrics mock (metric
  no longer emitted now that telemetry path skips the circuit breaker).
- Drop the enableCircuitBreaker option from the default test setup.
- Replace the "with CircuitBreaker" describe block with an equivalent
  suite that asserts the same observable behavior (single send per call,
  failures swallowed gracefully) without requiring the option.

All 31 unified-session tests pass.
2026-04-26 22:50:55 -03:00
Renato Alcara d9e0e0e0af refactor(types): drop circuit-breaker SocketConfig fields and default
Removes the 5 optional circuit-breaker config fields exposed via SocketConfig
and the corresponding default in DEFAULT_CONNECTION_CONFIG:

  - enableCircuitBreaker
  - queryCircuitBreaker
  - connectionCircuitBreaker
  - preKeyCircuitBreaker
  - messageCircuitBreaker

Also drops the CircuitBreakerOptions import.

Backward compatibility: these were ALL optional. Callers that set them
will see no compile error (TypeScript accepts unknown extra props on
loose object literals through structural typing) and the values are
simply ignored. A future major version may want to mark these as
forbidden with a more aggressive type, but for now silent ignore is
the least intrusive transition.
2026-04-26 22:49:27 -03:00
Renato Alcara a6cf9b0b26 refactor(signal): replace circuit-breaker with bounded-retry in libsignal wrapper
The preKeyCircuitBreaker was passed into signalStorage() as an optional
parameter and was used in only ONE place: .reset() after detecting an
identity-key change (line 391, the 'contact may have reinstalled WhatsApp'
recovery path).

Bounded-retry is stateless — each retry is independent — so the equivalent
of "forget the past failures" happens automatically on the next operation.
There is no state to reset.

Changes:
- Drop CircuitBreaker import.
- Remove preKeyCircuitBreaker field from LibSignalRepositoryOptions.
- Remove preKeyCircuitBreaker parameter from signalStorage().
- Remove the .reset() call in the identity-key-change recovery path
  (replaced with explanatory comment).

Build passes.
2026-04-26 22:48:23 -03:00
Renato Alcara b531b46999 refactor(utils): remove circuit-breaker integration from helpers
- Utils/index.ts: re-export bounded-retry instead of circuit-breaker
- Utils/unified-session.ts: drop the optional circuit-breaker wrapping.
  Telemetry is non-critical; a single send attempt with the underlying
  sendNode timeout is sufficient. Removed enableCircuitBreaker option,
  circuitBreaker field, and CircuitOpenError handling.
- Utils/retry-utils.ts: drop the optional circuitBreaker option from
  withRetry. Retry mechanics are now self-contained; callers that need
  bounded retries should prefer withBoundedRetry directly.
- Utils/health-status.ts: stop importing globalCircuitRegistry. Keep the
  CircuitBreakerHealth shape for k8s probe schema stability — always
  reports an empty list now.

Note: src/Utils/structured-logger.ts has its own *private* CircuitBreaker
class for log-throttling — independent of the socket-level circuit breaker
being removed. Untouched.

baileys-logger.ts logCircuitBreaker() is a generic logging helper that
takes state strings as input — no import dependency. Untouched (no-op
for callers that no longer have circuit breakers).
2026-04-26 22:46:30 -03:00
Renato Alcara 3ece5f7446 refactor(socket): replace 3 circuit breakers with bounded-retry
Remove queryCircuitBreaker, connectionCircuitBreaker, and preKeyCircuitBreaker
from src/Socket/socket.ts. Empirical capture (Frida) of WhatsApp Android
shows it uses per-operation exponential backoff, not a global state machine
that gates unrelated operations after N failures.

Changes:
- query(): unwrap. Each iq stanza has its own waitForMessage timeout.
  Callers that need retry semantics (assertSessions etc.) wrap query()
  with withBoundedRetry() at their level.
- sendRawMessage(): unwrap. WS lifecycle errors were already filtered
  out of the circuit's failure count — the only failures it tripped on
  were rare native send() errors, where a state-machine adds no value
  over a fast propagated error. Reconnection logic in makeSocket
  handles WS recovery independently.
- uploadPreKeys(): replace circuit breaker AND the manual exponential
  backoff retry loop with withBoundedRetry. Cleaner: one mechanism
  instead of two stacked.
- Remove circuitBreakers / getCircuitBreakerStats / resetCircuitBreakers
  from the socket return type.
- Remove enableCircuitBreaker / queryCircuitBreaker / connectionCircuitBreaker /
  preKeyCircuitBreaker config destructures and the init block.
- Pass-through: enableCircuitBreaker option no longer forwarded to
  unifiedSessionManager (cleaned in next commit).

Build passes.
2026-04-26 22:42:31 -03:00
Renato Alcara 314dd9469d feat(retry): add bounded-retry utility (WhatsApp-aligned)
Per-operation exponential backoff with jitter, max attempts, and TTL.
Replaces the circuit breaker pattern, which was causing cascading
failures (5 timeouts in 60s blocked all queries for 30s).

Empirical defaults captured via Frida from WhatsApp Android:
- delays: [3000, 10000, 60000, 60000, 120000] ms (cap at 2 min)
- TTL: 600_000 ms (10 min — empirical 3-5 min stabilisation + buffer)
- per-attempt timeout: 30_000 ms
- jitter: ±15%

Properties (vs circuit breaker):
- Per-operation independent — failures in op A do NOT block op B
- No global state machine (no CLOSED/OPEN/HALF-OPEN cascade)
- Bounded by maxAttempts + ttlMs (no unbounded retry accumulation)
- AbortSignal cancellation support
- Aligned with WhatsApp Android empirical retry behavior

12 tests cover: default sequence, TTL exhaustion, per-attempt timeout,
shouldRetry predicate, AbortSignal, per-op isolation, delay cap, jitter,
budget cap. All pass in ~4s.
2026-04-26 22:39:04 -03:00
6 changed files with 74 additions and 3217 deletions
+1 -163
View File
@@ -1,7 +1,7 @@
syntax = "proto3";
package proto;
/// WhatsApp Version: 2.3000.1038469210
/// WhatsApp Version: 2.3000.1038164556
message ADVDeviceIdentity {
optional uint32 rawId = 1;
@@ -14,7 +14,6 @@ message ADVDeviceIdentity {
enum ADVEncryptionType {
E2EE = 0;
HOSTED = 1;
NON_E2EE = 2;
}
message ADVKeyIndexList {
optional uint32 rawId = 1;
@@ -62,7 +61,6 @@ message AIHomeState {
ANIMATE_PHOTO = 2;
ANALYZE_FILE = 3;
COLLABORATE = 4;
OPEN_GREETING_CARD = 5;
}
}
@@ -249,16 +247,6 @@ message AIRichResponseUnifiedResponse {
optional bytes data = 1;
}
enum AISubscriptionRequestType {
UNSPECIFIED = 0;
THINK_HARD = 1;
IMAGE_GEN = 2;
VIDEO_GEN = 3;
}
message AISubscriptionUpsellMetadata {
optional AISubscriptionRequestType requestType = 1;
}
message AIThreadInfo {
optional AIThreadServerInfo serverInfo = 1;
optional AIThreadClientInfo clientInfo = 2;
@@ -671,8 +659,6 @@ message BotMetadata {
optional BotInfrastructureDiagnostics botInfrastructureDiagnostics = 37;
optional AIMediaCollectionMetadata aiMediaCollectionMetadata = 38;
optional BotCommandMetadata commandMetadata = 39;
optional BotResolvedToolCallMetadata resolvedToolCallMetadata = 40;
optional AISubscriptionUpsellMetadata subscriptionUpsellMetadata = 41;
optional bytes internalMetadata = 999;
}
@@ -911,11 +897,6 @@ message BotRenderingMetadata {
}
message BotResolvedToolCallMetadata {
optional string toolCallId = 1;
optional string resolutionDataSerialized = 2;
}
message BotSessionMetadata {
optional string sessionId = 1;
optional BotSessionSource sessionSource = 2;
@@ -1151,7 +1132,6 @@ message ClientPayload {
optional int32 preacksCount = 45;
optional int32 processingQueueSize = 46;
repeated string pairedPeripherals = 47;
optional bytes testIsolationId = 48;
enum AccountType {
DEFAULT = 0;
GUEST = 1;
@@ -1247,7 +1227,6 @@ message ClientPayload {
optional string deviceExpId = 14;
optional DeviceType deviceType = 15;
optional string deviceModelType = 16;
optional DistributionChannel distributionChannel = 17;
message AppVersion {
optional uint32 primary = 1;
optional uint32 secondary = 2;
@@ -1263,12 +1242,6 @@ message ClientPayload {
WEARABLE = 3;
VR = 4;
}
enum DistributionChannel {
APPSTORE = 0;
WEBSITE = 1;
TESTFLIGHT = 2;
INTERNAL = 3;
}
enum Platform {
ANDROID = 0;
IOS = 1;
@@ -1438,9 +1411,6 @@ message ContextInfo {
optional MediaDomainInfo mediaDomainInfo = 74;
optional PartiallySelectedContent partiallySelectedContent = 75;
optional uint32 afterReadDuration = 76;
optional CrossAppSource crossAppSource = 77;
optional BusinessInteractionPills businessInteractionPills = 78;
optional string posterStatusId = 79;
message AdReplyInfo {
optional string advertiserName = 1;
optional MediaType mediaType = 2;
@@ -1453,47 +1423,10 @@ message ContextInfo {
}
}
message BusinessInteractionPills {
optional string businessJid = 1;
repeated Pill pills = 2;
optional EntryPoint entryPoint = 3;
enum EntryPoint {
ENTRY_POINT_UNKNOWN = 0;
P2P_LINK_SHARE = 1;
CONTACT_CARD_SHARING = 2;
PHONE_NUMBER = 3;
STATUS = 4;
IN_THREAD_CONTEXT_CARD = 5;
}
message Pill {
optional ContextInfo.BusinessInteractionPills.PillType pillType = 1;
optional string actionUrl = 2;
}
enum PillType {
UNKNOWN = 0;
VIEW_BUSINESS = 1;
CHAT = 2;
CALL = 3;
CATALOG = 4;
CHANNEL = 5;
BOOK_APPOINTMENT = 6;
OFFERS = 7;
BESTSELLERS = 8;
MENU = 9;
ABOUT = 10;
}
}
message BusinessMessageForwardInfo {
optional string businessOwnerJid = 1;
}
enum CrossAppSource {
CROSS_APP_SOURCE_UNKNOWN = 0;
CROSS_APP_SOURCE_INSTAGRAM = 1;
CROSS_APP_SOURCE_FACEBOOK = 2;
}
message DataSharingContext {
optional bool showMmDisclosure = 1;
optional string encryptedSignalTokenConsented = 2;
@@ -1702,7 +1635,6 @@ message Conversation {
optional bool isMarketingMessageThread = 55;
optional bool isSenderNewAccount = 56;
optional uint32 afterReadDuration = 57;
optional bool isSenderSuspicious = 58;
enum EndOfHistoryTransferType {
COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY = 0;
COMPLETE_AND_NO_MORE_MESSAGE_REMAIN_ON_PRIMARY = 1;
@@ -2416,8 +2348,6 @@ message Message {
optional PollAddOptionMessage pollAddOptionMessage = 121;
optional EventInviteMessage eventInviteMessage = 122;
optional GroupRootKeyShare groupRootKeyShare = 123;
optional P2PPaymentReminderNotification p2PPaymentReminderNotification = 124;
optional SplitPaymentMessage splitPaymentMessage = 125;
message AlbumMessage {
optional uint32 expectedImageCount = 2;
optional uint32 expectedVideoCount = 3;
@@ -2598,41 +2528,6 @@ message Message {
optional string id = 2;
}
message ChatCustomImageWallpaper {
optional string directPath = 1;
optional bytes mediaKey = 2;
optional bytes fileEncSha256 = 3;
optional bytes fileSha256 = 4;
optional float dimLevel = 5;
}
message ChatDefaultWallpaper {
optional bool isDoodleEnabled = 1;
}
message ChatSolidColorWallpaper {
optional string colorLight = 1;
optional string colorDark = 2;
optional bool isDoodleEnabled = 3;
}
message ChatStockImageWallpaper {
optional string stockImageId = 1;
optional float dimLevel = 2;
}
message ChatThemeSetting {
optional int64 settingTimestampMs = 1;
optional bool clearTheme = 2;
optional string colorSchemeId = 3;
oneof wallpaper {
Message.ChatDefaultWallpaper defaultWallpaper = 10;
Message.ChatSolidColorWallpaper solidColor = 11;
Message.ChatStockImageWallpaper stockImage = 12;
Message.ChatCustomImageWallpaper customImage = 13;
}
}
message CloudAPIThreadControlNotification {
optional CloudAPIThreadControl status = 1;
optional int64 senderNotificationTimestampMs = 2;
@@ -2742,7 +2637,6 @@ message Message {
optional int64 startTime = 5;
optional string caption = 6;
optional bool isCanceled = 7;
optional int64 endTime = 8;
}
message EventMessage {
@@ -3325,35 +3219,6 @@ message Message {
}
}
message P2PPaymentReminderNotification {
optional string reminderId = 1;
optional Money amount = 2;
optional ReminderFrequency frequency = 3;
optional int64 nextReminderTimestamp = 4;
optional int64 expiryTimestamp = 5;
optional ReminderState state = 6;
optional string description = 7;
optional string creatorJid = 8;
optional string receiverJid = 9;
optional string upiId = 10;
optional int64 createdTimestamp = 11;
enum ReminderFrequency {
UNKNOWN_FREQUENCY = 0;
WEEKLY = 1;
BIWEEKLY = 2;
MONTHLY = 3;
CUSTOM = 4;
}
enum ReminderState {
UNKNOWN_STATE = 0;
ACTIVE = 1;
PAUSED = 2;
STOPPED = 3;
EXPIRED = 4;
CANCELLED = 5;
}
}
message PaymentExtendedMetadata {
optional uint32 type = 1;
optional string platform = 2;
@@ -3763,7 +3628,6 @@ message Message {
optional MemberLabel memberLabel = 27;
optional AIMediaCollectionMessage aiMediaCollectionMessage = 28;
optional uint32 afterReadDuration = 29;
optional Message.ChatThemeSetting chatThemeSetting = 30;
enum Type {
REVOKE = 0;
EPHEMERAL_SETTING = 3;
@@ -3793,7 +3657,6 @@ message Message {
GROUP_MEMBER_LABEL_CHANGE = 30;
AI_MEDIA_COLLECTION_MESSAGE = 31;
MESSAGE_UNSCHEDULE = 32;
CHAT_THEME_SETTING = 34;
}
}
@@ -3885,26 +3748,6 @@ message Message {
optional bytes axolotlSenderKeyDistributionMessage = 2;
}
message SplitPaymentMessage {
optional string splitId = 1;
optional Money totalAmount = 2;
optional string description = 3;
optional string requesterJid = 4;
repeated Message.SplitPaymentParticipant participants = 5;
optional int64 createdAtMs = 6;
optional ContextInfo contextInfo = 17;
}
message SplitPaymentParticipant {
optional string jid = 1;
optional Money amount = 2;
optional SplitPaymentStatus status = 3;
enum SplitPaymentStatus {
PENDING = 0;
PAID = 1;
}
}
message StatusNotificationMessage {
optional MessageKey responseMessageKey = 1;
optional MessageKey originalMessageKey = 2;
@@ -4855,7 +4698,6 @@ message StatusAttribution {
SHARECHAT = 9;
GOOGLE_PHOTOS = 10;
SOUNDCLOUD = 11;
SHAZAM = 12;
}
}
@@ -4913,7 +4755,6 @@ message StatusAttribution {
LAYOUTS = 8;
NEWSLETTER_STATUS = 9;
STATUS_CLOSE_SHARING = 10;
PAID_PARTNERSHIP = 11;
}
}
@@ -5425,7 +5266,6 @@ message SyncActionValue {
repeated string keywords = 3;
optional int32 count = 4;
optional bool deleted = 5;
repeated string associatedLabelIds = 6;
}
message RecentEmojiWeightsAction {
@@ -5943,8 +5783,6 @@ message WebMessageInfo {
optional string hsmTag = 79;
optional uint64 ephemeralExpirationTimestamp = 80;
optional ScheduledMessageMetadata scheduledMessageMetadata = 81;
optional string decisionId = 82;
repeated string decisionSources = 83;
enum BizPrivacyStatus {
E2EE = 0;
FB = 2;
+5 -377
View File
@@ -28,8 +28,7 @@ export namespace proto {
enum ADVEncryptionType {
E2EE = 0,
HOSTED = 1,
NON_E2EE = 2
HOSTED = 1
}
interface IADVKeyIndexList {
@@ -177,8 +176,7 @@ export namespace proto {
CREATE_IMAGE = 1,
ANIMATE_PHOTO = 2,
ANALYZE_FILE = 3,
COLLABORATE = 4,
OPEN_GREETING_CARD = 5
COLLABORATE = 4
}
}
}
@@ -707,29 +705,6 @@ export namespace proto {
public static getTypeUrl(typeUrlPrefix?: string): string;
}
enum AISubscriptionRequestType {
UNSPECIFIED = 0,
THINK_HARD = 1,
IMAGE_GEN = 2,
VIDEO_GEN = 3
}
interface IAISubscriptionUpsellMetadata {
requestType?: (proto.AISubscriptionRequestType|null);
}
class AISubscriptionUpsellMetadata implements IAISubscriptionUpsellMetadata {
constructor(p?: proto.IAISubscriptionUpsellMetadata);
public requestType?: (proto.AISubscriptionRequestType|null);
public static create(properties?: proto.IAISubscriptionUpsellMetadata): proto.AISubscriptionUpsellMetadata;
public static encode(m: proto.IAISubscriptionUpsellMetadata, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.AISubscriptionUpsellMetadata;
public static fromObject(d: { [k: string]: any }): proto.AISubscriptionUpsellMetadata;
public static toObject(m: proto.AISubscriptionUpsellMetadata, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IAIThreadInfo {
serverInfo?: (proto.AIThreadInfo.IAIThreadServerInfo|null);
clientInfo?: (proto.AIThreadInfo.IAIThreadClientInfo|null);
@@ -1727,8 +1702,6 @@ export namespace proto {
botInfrastructureDiagnostics?: (proto.IBotInfrastructureDiagnostics|null);
aiMediaCollectionMetadata?: (proto.IAIMediaCollectionMetadata|null);
commandMetadata?: (proto.IBotCommandMetadata|null);
resolvedToolCallMetadata?: (proto.IBotResolvedToolCallMetadata|null);
subscriptionUpsellMetadata?: (proto.IAISubscriptionUpsellMetadata|null);
internalMetadata?: (Uint8Array|null);
}
@@ -1772,8 +1745,6 @@ export namespace proto {
public botInfrastructureDiagnostics?: (proto.IBotInfrastructureDiagnostics|null);
public aiMediaCollectionMetadata?: (proto.IAIMediaCollectionMetadata|null);
public commandMetadata?: (proto.IBotCommandMetadata|null);
public resolvedToolCallMetadata?: (proto.IBotResolvedToolCallMetadata|null);
public subscriptionUpsellMetadata?: (proto.IAISubscriptionUpsellMetadata|null);
public internalMetadata?: (Uint8Array|null);
public static create(properties?: proto.IBotMetadata): proto.BotMetadata;
public static encode(m: proto.IBotMetadata, w?: $protobuf.Writer): $protobuf.Writer;
@@ -2323,24 +2294,6 @@ export namespace proto {
}
}
interface IBotResolvedToolCallMetadata {
toolCallId?: (string|null);
resolutionDataSerialized?: (string|null);
}
class BotResolvedToolCallMetadata implements IBotResolvedToolCallMetadata {
constructor(p?: proto.IBotResolvedToolCallMetadata);
public toolCallId?: (string|null);
public resolutionDataSerialized?: (string|null);
public static create(properties?: proto.IBotResolvedToolCallMetadata): proto.BotResolvedToolCallMetadata;
public static encode(m: proto.IBotResolvedToolCallMetadata, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.BotResolvedToolCallMetadata;
public static fromObject(d: { [k: string]: any }): proto.BotResolvedToolCallMetadata;
public static toObject(m: proto.BotResolvedToolCallMetadata, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IBotSessionMetadata {
sessionId?: (string|null);
sessionSource?: (proto.BotSessionSource|null);
@@ -2929,7 +2882,6 @@ export namespace proto {
preacksCount?: (number|null);
processingQueueSize?: (number|null);
pairedPeripherals?: (string[]|null);
testIsolationId?: (Uint8Array|null);
}
class ClientPayload implements IClientPayload {
@@ -2969,7 +2921,6 @@ export namespace proto {
public preacksCount?: (number|null);
public processingQueueSize?: (number|null);
public pairedPeripherals: string[];
public testIsolationId?: (Uint8Array|null);
public static create(properties?: proto.IClientPayload): proto.ClientPayload;
public static encode(m: proto.IClientPayload, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ClientPayload;
@@ -3132,7 +3083,6 @@ export namespace proto {
deviceExpId?: (string|null);
deviceType?: (proto.ClientPayload.UserAgent.DeviceType|null);
deviceModelType?: (string|null);
distributionChannel?: (proto.ClientPayload.UserAgent.DistributionChannel|null);
}
class UserAgent implements IUserAgent {
@@ -3153,7 +3103,6 @@ export namespace proto {
public deviceExpId?: (string|null);
public deviceType?: (proto.ClientPayload.UserAgent.DeviceType|null);
public deviceModelType?: (string|null);
public distributionChannel?: (proto.ClientPayload.UserAgent.DistributionChannel|null);
public static create(properties?: proto.ClientPayload.IUserAgent): proto.ClientPayload.UserAgent;
public static encode(m: proto.ClientPayload.IUserAgent, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ClientPayload.UserAgent;
@@ -3197,13 +3146,6 @@ export namespace proto {
VR = 4
}
enum DistributionChannel {
APPSTORE = 0,
WEBSITE = 1,
TESTFLIGHT = 2,
INTERNAL = 3
}
enum Platform {
ANDROID = 0,
IOS = 1,
@@ -3469,9 +3411,6 @@ export namespace proto {
mediaDomainInfo?: (proto.IMediaDomainInfo|null);
partiallySelectedContent?: (proto.ContextInfo.IPartiallySelectedContent|null);
afterReadDuration?: (number|null);
crossAppSource?: (proto.ContextInfo.CrossAppSource|null);
businessInteractionPills?: (proto.ContextInfo.IBusinessInteractionPills|null);
posterStatusId?: (string|null);
}
class ContextInfo implements IContextInfo {
@@ -3535,9 +3474,6 @@ export namespace proto {
public mediaDomainInfo?: (proto.IMediaDomainInfo|null);
public partiallySelectedContent?: (proto.ContextInfo.IPartiallySelectedContent|null);
public afterReadDuration?: (number|null);
public crossAppSource?: (proto.ContextInfo.CrossAppSource|null);
public businessInteractionPills?: (proto.ContextInfo.IBusinessInteractionPills|null);
public posterStatusId?: (string|null);
public static create(properties?: proto.IContextInfo): proto.ContextInfo;
public static encode(m: proto.IContextInfo, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ContextInfo;
@@ -3580,70 +3516,6 @@ export namespace proto {
}
}
interface IBusinessInteractionPills {
businessJid?: (string|null);
pills?: (proto.ContextInfo.BusinessInteractionPills.IPill[]|null);
entryPoint?: (proto.ContextInfo.BusinessInteractionPills.EntryPoint|null);
}
class BusinessInteractionPills implements IBusinessInteractionPills {
constructor(p?: proto.ContextInfo.IBusinessInteractionPills);
public businessJid?: (string|null);
public pills: proto.ContextInfo.BusinessInteractionPills.IPill[];
public entryPoint?: (proto.ContextInfo.BusinessInteractionPills.EntryPoint|null);
public static create(properties?: proto.ContextInfo.IBusinessInteractionPills): proto.ContextInfo.BusinessInteractionPills;
public static encode(m: proto.ContextInfo.IBusinessInteractionPills, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ContextInfo.BusinessInteractionPills;
public static fromObject(d: { [k: string]: any }): proto.ContextInfo.BusinessInteractionPills;
public static toObject(m: proto.ContextInfo.BusinessInteractionPills, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
namespace BusinessInteractionPills {
enum EntryPoint {
ENTRY_POINT_UNKNOWN = 0,
P2P_LINK_SHARE = 1,
CONTACT_CARD_SHARING = 2,
PHONE_NUMBER = 3,
STATUS = 4,
IN_THREAD_CONTEXT_CARD = 5
}
interface IPill {
pillType?: (proto.ContextInfo.BusinessInteractionPills.PillType|null);
actionUrl?: (string|null);
}
class Pill implements IPill {
constructor(p?: proto.ContextInfo.BusinessInteractionPills.IPill);
public pillType?: (proto.ContextInfo.BusinessInteractionPills.PillType|null);
public actionUrl?: (string|null);
public static create(properties?: proto.ContextInfo.BusinessInteractionPills.IPill): proto.ContextInfo.BusinessInteractionPills.Pill;
public static encode(m: proto.ContextInfo.BusinessInteractionPills.IPill, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ContextInfo.BusinessInteractionPills.Pill;
public static fromObject(d: { [k: string]: any }): proto.ContextInfo.BusinessInteractionPills.Pill;
public static toObject(m: proto.ContextInfo.BusinessInteractionPills.Pill, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
enum PillType {
UNKNOWN = 0,
VIEW_BUSINESS = 1,
CHAT = 2,
CALL = 3,
CATALOG = 4,
CHANNEL = 5,
BOOK_APPOINTMENT = 6,
OFFERS = 7,
BESTSELLERS = 8,
MENU = 9,
ABOUT = 10
}
}
interface IBusinessMessageForwardInfo {
businessOwnerJid?: (string|null);
}
@@ -3660,12 +3532,6 @@ export namespace proto {
public static getTypeUrl(typeUrlPrefix?: string): string;
}
enum CrossAppSource {
CROSS_APP_SOURCE_UNKNOWN = 0,
CROSS_APP_SOURCE_INSTAGRAM = 1,
CROSS_APP_SOURCE_FACEBOOK = 2
}
interface IDataSharingContext {
showMmDisclosure?: (boolean|null);
encryptedSignalTokenConsented?: (string|null);
@@ -4055,7 +3921,6 @@ export namespace proto {
isMarketingMessageThread?: (boolean|null);
isSenderNewAccount?: (boolean|null);
afterReadDuration?: (number|null);
isSenderSuspicious?: (boolean|null);
}
class Conversation implements IConversation {
@@ -4117,7 +3982,6 @@ export namespace proto {
public isMarketingMessageThread?: (boolean|null);
public isSenderNewAccount?: (boolean|null);
public afterReadDuration?: (number|null);
public isSenderSuspicious?: (boolean|null);
public static create(properties?: proto.IConversation): proto.Conversation;
public static encode(m: proto.IConversation, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Conversation;
@@ -5861,8 +5725,6 @@ export namespace proto {
pollAddOptionMessage?: (proto.Message.IPollAddOptionMessage|null);
eventInviteMessage?: (proto.Message.IEventInviteMessage|null);
groupRootKeyShare?: (proto.IGroupRootKeyShare|null);
p2PPaymentReminderNotification?: (proto.Message.IP2PPaymentReminderNotification|null);
splitPaymentMessage?: (proto.Message.ISplitPaymentMessage|null);
}
class Message implements IMessage {
@@ -5970,8 +5832,6 @@ export namespace proto {
public pollAddOptionMessage?: (proto.Message.IPollAddOptionMessage|null);
public eventInviteMessage?: (proto.Message.IEventInviteMessage|null);
public groupRootKeyShare?: (proto.IGroupRootKeyShare|null);
public p2PPaymentReminderNotification?: (proto.Message.IP2PPaymentReminderNotification|null);
public splitPaymentMessage?: (proto.Message.ISplitPaymentMessage|null);
public static create(properties?: proto.IMessage): proto.Message;
public static encode(m: proto.IMessage, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message;
@@ -6480,113 +6340,6 @@ export namespace proto {
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IChatCustomImageWallpaper {
directPath?: (string|null);
mediaKey?: (Uint8Array|null);
fileEncSha256?: (Uint8Array|null);
fileSha256?: (Uint8Array|null);
dimLevel?: (number|null);
}
class ChatCustomImageWallpaper implements IChatCustomImageWallpaper {
constructor(p?: proto.Message.IChatCustomImageWallpaper);
public directPath?: (string|null);
public mediaKey?: (Uint8Array|null);
public fileEncSha256?: (Uint8Array|null);
public fileSha256?: (Uint8Array|null);
public dimLevel?: (number|null);
public static create(properties?: proto.Message.IChatCustomImageWallpaper): proto.Message.ChatCustomImageWallpaper;
public static encode(m: proto.Message.IChatCustomImageWallpaper, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.ChatCustomImageWallpaper;
public static fromObject(d: { [k: string]: any }): proto.Message.ChatCustomImageWallpaper;
public static toObject(m: proto.Message.ChatCustomImageWallpaper, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IChatDefaultWallpaper {
isDoodleEnabled?: (boolean|null);
}
class ChatDefaultWallpaper implements IChatDefaultWallpaper {
constructor(p?: proto.Message.IChatDefaultWallpaper);
public isDoodleEnabled?: (boolean|null);
public static create(properties?: proto.Message.IChatDefaultWallpaper): proto.Message.ChatDefaultWallpaper;
public static encode(m: proto.Message.IChatDefaultWallpaper, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.ChatDefaultWallpaper;
public static fromObject(d: { [k: string]: any }): proto.Message.ChatDefaultWallpaper;
public static toObject(m: proto.Message.ChatDefaultWallpaper, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IChatSolidColorWallpaper {
colorLight?: (string|null);
colorDark?: (string|null);
isDoodleEnabled?: (boolean|null);
}
class ChatSolidColorWallpaper implements IChatSolidColorWallpaper {
constructor(p?: proto.Message.IChatSolidColorWallpaper);
public colorLight?: (string|null);
public colorDark?: (string|null);
public isDoodleEnabled?: (boolean|null);
public static create(properties?: proto.Message.IChatSolidColorWallpaper): proto.Message.ChatSolidColorWallpaper;
public static encode(m: proto.Message.IChatSolidColorWallpaper, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.ChatSolidColorWallpaper;
public static fromObject(d: { [k: string]: any }): proto.Message.ChatSolidColorWallpaper;
public static toObject(m: proto.Message.ChatSolidColorWallpaper, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IChatStockImageWallpaper {
stockImageId?: (string|null);
dimLevel?: (number|null);
}
class ChatStockImageWallpaper implements IChatStockImageWallpaper {
constructor(p?: proto.Message.IChatStockImageWallpaper);
public stockImageId?: (string|null);
public dimLevel?: (number|null);
public static create(properties?: proto.Message.IChatStockImageWallpaper): proto.Message.ChatStockImageWallpaper;
public static encode(m: proto.Message.IChatStockImageWallpaper, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.ChatStockImageWallpaper;
public static fromObject(d: { [k: string]: any }): proto.Message.ChatStockImageWallpaper;
public static toObject(m: proto.Message.ChatStockImageWallpaper, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IChatThemeSetting {
settingTimestampMs?: (number|Long|null);
clearTheme?: (boolean|null);
colorSchemeId?: (string|null);
defaultWallpaper?: (proto.Message.IChatDefaultWallpaper|null);
solidColor?: (proto.Message.IChatSolidColorWallpaper|null);
stockImage?: (proto.Message.IChatStockImageWallpaper|null);
customImage?: (proto.Message.IChatCustomImageWallpaper|null);
}
class ChatThemeSetting implements IChatThemeSetting {
constructor(p?: proto.Message.IChatThemeSetting);
public settingTimestampMs?: (number|Long|null);
public clearTheme?: (boolean|null);
public colorSchemeId?: (string|null);
public defaultWallpaper?: (proto.Message.IChatDefaultWallpaper|null);
public solidColor?: (proto.Message.IChatSolidColorWallpaper|null);
public stockImage?: (proto.Message.IChatStockImageWallpaper|null);
public customImage?: (proto.Message.IChatCustomImageWallpaper|null);
public wallpaper?: ("defaultWallpaper"|"solidColor"|"stockImage"|"customImage");
public static create(properties?: proto.Message.IChatThemeSetting): proto.Message.ChatThemeSetting;
public static encode(m: proto.Message.IChatThemeSetting, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.ChatThemeSetting;
public static fromObject(d: { [k: string]: any }): proto.Message.ChatThemeSetting;
public static toObject(m: proto.Message.ChatThemeSetting, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface ICloudAPIThreadControlNotification {
status?: (proto.Message.CloudAPIThreadControlNotification.CloudAPIThreadControl|null);
senderNotificationTimestampMs?: (number|Long|null);
@@ -6891,7 +6644,6 @@ export namespace proto {
startTime?: (number|Long|null);
caption?: (string|null);
isCanceled?: (boolean|null);
endTime?: (number|Long|null);
}
class EventInviteMessage implements IEventInviteMessage {
@@ -6903,7 +6655,6 @@ export namespace proto {
public startTime?: (number|Long|null);
public caption?: (string|null);
public isCanceled?: (boolean|null);
public endTime?: (number|Long|null);
public static create(properties?: proto.Message.IEventInviteMessage): proto.Message.EventInviteMessage;
public static encode(m: proto.Message.IEventInviteMessage, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.EventInviteMessage;
@@ -8424,62 +8175,6 @@ export namespace proto {
}
}
interface IP2PPaymentReminderNotification {
reminderId?: (string|null);
amount?: (proto.IMoney|null);
frequency?: (proto.Message.P2PPaymentReminderNotification.ReminderFrequency|null);
nextReminderTimestamp?: (number|Long|null);
expiryTimestamp?: (number|Long|null);
state?: (proto.Message.P2PPaymentReminderNotification.ReminderState|null);
description?: (string|null);
creatorJid?: (string|null);
receiverJid?: (string|null);
upiId?: (string|null);
createdTimestamp?: (number|Long|null);
}
class P2PPaymentReminderNotification implements IP2PPaymentReminderNotification {
constructor(p?: proto.Message.IP2PPaymentReminderNotification);
public reminderId?: (string|null);
public amount?: (proto.IMoney|null);
public frequency?: (proto.Message.P2PPaymentReminderNotification.ReminderFrequency|null);
public nextReminderTimestamp?: (number|Long|null);
public expiryTimestamp?: (number|Long|null);
public state?: (proto.Message.P2PPaymentReminderNotification.ReminderState|null);
public description?: (string|null);
public creatorJid?: (string|null);
public receiverJid?: (string|null);
public upiId?: (string|null);
public createdTimestamp?: (number|Long|null);
public static create(properties?: proto.Message.IP2PPaymentReminderNotification): proto.Message.P2PPaymentReminderNotification;
public static encode(m: proto.Message.IP2PPaymentReminderNotification, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.P2PPaymentReminderNotification;
public static fromObject(d: { [k: string]: any }): proto.Message.P2PPaymentReminderNotification;
public static toObject(m: proto.Message.P2PPaymentReminderNotification, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
namespace P2PPaymentReminderNotification {
enum ReminderFrequency {
UNKNOWN_FREQUENCY = 0,
WEEKLY = 1,
BIWEEKLY = 2,
MONTHLY = 3,
CUSTOM = 4
}
enum ReminderState {
UNKNOWN_STATE = 0,
ACTIVE = 1,
PAUSED = 2,
STOPPED = 3,
EXPIRED = 4,
CANCELLED = 5
}
}
interface IPaymentExtendedMetadata {
type?: (number|null);
platform?: (string|null);
@@ -9628,7 +9323,6 @@ export namespace proto {
memberLabel?: (proto.IMemberLabel|null);
aiMediaCollectionMessage?: (proto.IAIMediaCollectionMessage|null);
afterReadDuration?: (number|null);
chatThemeSetting?: (proto.Message.IChatThemeSetting|null);
}
class ProtocolMessage implements IProtocolMessage {
@@ -9659,7 +9353,6 @@ export namespace proto {
public memberLabel?: (proto.IMemberLabel|null);
public aiMediaCollectionMessage?: (proto.IAIMediaCollectionMessage|null);
public afterReadDuration?: (number|null);
public chatThemeSetting?: (proto.Message.IChatThemeSetting|null);
public static create(properties?: proto.Message.IProtocolMessage): proto.Message.ProtocolMessage;
public static encode(m: proto.Message.IProtocolMessage, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.ProtocolMessage;
@@ -9699,8 +9392,7 @@ export namespace proto {
AI_QUERY_FANOUT = 29,
GROUP_MEMBER_LABEL_CHANGE = 30,
AI_MEDIA_COLLECTION_MESSAGE = 31,
MESSAGE_UNSCHEDULE = 32,
CHAT_THEME_SETTING = 34
MESSAGE_UNSCHEDULE = 32
}
}
@@ -9952,62 +9644,6 @@ export namespace proto {
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface ISplitPaymentMessage {
splitId?: (string|null);
totalAmount?: (proto.IMoney|null);
description?: (string|null);
requesterJid?: (string|null);
participants?: (proto.Message.ISplitPaymentParticipant[]|null);
createdAtMs?: (number|Long|null);
contextInfo?: (proto.IContextInfo|null);
}
class SplitPaymentMessage implements ISplitPaymentMessage {
constructor(p?: proto.Message.ISplitPaymentMessage);
public splitId?: (string|null);
public totalAmount?: (proto.IMoney|null);
public description?: (string|null);
public requesterJid?: (string|null);
public participants: proto.Message.ISplitPaymentParticipant[];
public createdAtMs?: (number|Long|null);
public contextInfo?: (proto.IContextInfo|null);
public static create(properties?: proto.Message.ISplitPaymentMessage): proto.Message.SplitPaymentMessage;
public static encode(m: proto.Message.ISplitPaymentMessage, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.SplitPaymentMessage;
public static fromObject(d: { [k: string]: any }): proto.Message.SplitPaymentMessage;
public static toObject(m: proto.Message.SplitPaymentMessage, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface ISplitPaymentParticipant {
jid?: (string|null);
amount?: (proto.IMoney|null);
status?: (proto.Message.SplitPaymentParticipant.SplitPaymentStatus|null);
}
class SplitPaymentParticipant implements ISplitPaymentParticipant {
constructor(p?: proto.Message.ISplitPaymentParticipant);
public jid?: (string|null);
public amount?: (proto.IMoney|null);
public status?: (proto.Message.SplitPaymentParticipant.SplitPaymentStatus|null);
public static create(properties?: proto.Message.ISplitPaymentParticipant): proto.Message.SplitPaymentParticipant;
public static encode(m: proto.Message.ISplitPaymentParticipant, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.SplitPaymentParticipant;
public static fromObject(d: { [k: string]: any }): proto.Message.SplitPaymentParticipant;
public static toObject(m: proto.Message.SplitPaymentParticipant, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
namespace SplitPaymentParticipant {
enum SplitPaymentStatus {
PENDING = 0,
PAID = 1
}
}
interface IStatusNotificationMessage {
responseMessageKey?: (proto.IMessageKey|null);
originalMessageKey?: (proto.IMessageKey|null);
@@ -12362,8 +11998,7 @@ export namespace proto {
APPLE_MUSIC = 8,
SHARECHAT = 9,
GOOGLE_PHOTOS = 10,
SOUNDCLOUD = 11,
SHAZAM = 12
SOUNDCLOUD = 11
}
}
@@ -12497,8 +12132,7 @@ export namespace proto {
AI_CREATED = 7,
LAYOUTS = 8,
NEWSLETTER_STATUS = 9,
STATUS_CLOSE_SHARING = 10,
PAID_PARTNERSHIP = 11
STATUS_CLOSE_SHARING = 10
}
}
@@ -14035,7 +13669,6 @@ export namespace proto {
keywords?: (string[]|null);
count?: (number|null);
deleted?: (boolean|null);
associatedLabelIds?: (string[]|null);
}
class QuickReplyAction implements IQuickReplyAction {
@@ -14045,7 +13678,6 @@ export namespace proto {
public keywords: string[];
public count?: (number|null);
public deleted?: (boolean|null);
public associatedLabelIds: string[];
public static create(properties?: proto.SyncActionValue.IQuickReplyAction): proto.SyncActionValue.QuickReplyAction;
public static encode(m: proto.SyncActionValue.IQuickReplyAction, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.QuickReplyAction;
@@ -15327,8 +14959,6 @@ export namespace proto {
hsmTag?: (string|null);
ephemeralExpirationTimestamp?: (number|Long|null);
scheduledMessageMetadata?: (proto.IScheduledMessageMetadata|null);
decisionId?: (string|null);
decisionSources?: (string[]|null);
}
class WebMessageInfo implements IWebMessageInfo {
@@ -15403,8 +15033,6 @@ export namespace proto {
public hsmTag?: (string|null);
public ephemeralExpirationTimestamp?: (number|Long|null);
public scheduledMessageMetadata?: (proto.IScheduledMessageMetadata|null);
public decisionId?: (string|null);
public decisionSources: string[];
public static create(properties?: proto.IWebMessageInfo): proto.WebMessageInfo;
public static encode(m: proto.IWebMessageInfo, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.WebMessageInfo;
-2549
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
{"version":[2,3000,1038487394]}
{"version":[2,3000,1038167900]}
+55 -87
View File
@@ -299,6 +299,25 @@ export function makeLibSignalRepository(
// Promise instead of each spawning their own DB transactions.
const migrationInFlight = new Map<string, Promise<{ migrated: number; skipped: number; total: number }>>()
// Resolve PN JID to its canonical LID JID for transaction locking.
// This prevents PN/LID race conditions where concurrent operations for the
// same logical contact acquire different mutex locks because one uses PN
// and the other uses LID. (Aligned with WABA behavior — all operations use LID internally.)
const resolveCanonicalJid = async(jid: string): Promise<string> => {
if (isAnyLidUser(jid)) {
return jid
}
if (isAnyPnUser(jid)) {
const lid = await lidMapping.getLIDForPN(jid)
if (lid) {
return lid
}
}
return jid
}
const repository: SignalRepositoryWithLIDStore = {
decryptGroupMessage({ group, authorJid, msg }) {
const senderName = jidToSignalSenderKeyName(group, authorJid)
@@ -341,14 +360,6 @@ export function makeLibSignalRepository(
},
async decryptMessage({ jid, type, ciphertext }) {
const addr = jidToSignalProtocolAddress(jid)
// Wire address = the EXACT key that signalStorage's loadSession/storeSession
// uses internally (it canonicalizes PN→LID via resolveLIDSignalAddress).
// We MUST lock on this same key — locking on the raw `jid` lets two
// concurrent ops for the same logical contact (one arriving as PN, one as
// LID) acquire DIFFERENT mutex slots and interleave on the same session
// record, corrupting the ratchet → perpetual `Bad MAC` on own DSM after
// the WhatsApp PN→LID DSM rollout.
const wireAddr = await resolveLIDSignalAddress(addr.toString(), lidMapping)
const session = new libsignal.SessionCipher(storage, addr)
async function doDecrypt() {
@@ -397,23 +408,24 @@ export function makeLibSignalRepository(
return result
}
// Use canonical JID (PN→LID resolved) as transaction key to prevent
// PN/LID race conditions on the same logical session.
const canonicalJid = await resolveCanonicalJid(jid)
return parsedKeys.transaction(async () => {
return await doDecrypt()
}, wireAddr)
}, canonicalJid)
},
async encryptMessage({ jid, data }) {
const addr = jidToSignalProtocolAddress(jid)
// See decryptMessage above for the rationale: lock on the wire address,
// not the raw JID, so concurrent PN/LID ops on the same session serialize.
const wireAddr = await resolveLIDSignalAddress(addr.toString(), lidMapping)
const cipher = new libsignal.SessionCipher(storage, addr)
const canonicalJid = await resolveCanonicalJid(jid)
return parsedKeys.transaction(async () => {
const { type: sigType, body } = await cipher.encrypt(data)
const type = sigType === 3 ? 'pkmsg' : 'msg'
return { type, ciphertext: Buffer.from(body, 'binary') }
}, wireAddr)
}, canonicalJid)
},
async encryptGroupMessage({ group, meId, data }) {
@@ -441,16 +453,10 @@ export function makeLibSignalRepository(
async injectE2ESession({ jid, session }) {
logger.trace({ jid }, 'injecting E2EE session')
const addr = jidToSignalProtocolAddress(jid)
// Same wire-address locking as encrypt/decrypt — `SessionBuilder.initOutgoing`
// writes via `storeSession` which canonicalizes, so the lock key must be
// the wire address for the mutex to actually serialize concurrent ops on
// the same logical session.
const wireAddr = await resolveLIDSignalAddress(addr.toString(), lidMapping)
const cipher = new libsignal.SessionBuilder(storage, addr)
const cipher = new libsignal.SessionBuilder(storage, jidToSignalProtocolAddress(jid))
return parsedKeys.transaction(async () => {
await cipher.initOutgoing(session)
}, wireAddr)
}, jid)
},
jidToSignalProtocolAddress(jid) {
return jidToSignalProtocolAddress(jid).toString()
@@ -727,65 +733,6 @@ const jidToSignalSenderKeyName = (group: string, user: string): SenderKeyName =>
return new SenderKeyName(group, jidToSignalProtocolAddress(user))
}
/**
* Resolve a Signal protocol address (string form `signalUser.device`, where
* `signalUser` is `user` for the WhatsApp domain or `user_domainType` for any
* other domain e.g. `12345_1.0` for a LID device 0) to its canonical *wire*
* address the exact key that `loadSession`/`storeSession` uses when
* reading/writing `keys.get('session', [...])`.
*
* This is the SAME key the storage layer uses internally, so callers that need
* a transaction lock around session mutations MUST lock on this resolved
* address (not the raw JID), otherwise the lock key and storage key drift
* apart and concurrent ops on the same session interleave ratchet
* corruption perpetual `Bad MAC` errors.
*
* Behavior: if the input already targets a LID domain, returns it unchanged
* (cheap fast path no awaits). Otherwise looks up the PNLID mapping; if
* found, returns the LID-form Signal address. If no mapping exists, returns
* the input unchanged.
*
* Residual race window: the lock and the storage's internal re-resolution
* each call this function once. If a PNLID mapping is added between those
* two calls (microsecond window), the lock could be on the PN address while
* storage canonicalizes to LID. In production this is mitigated upstream:
* `messages-recv.ts` runs `migrateSession()` synchronously BEFORE
* `decryptMessage()` is called, so the mapping is already populated by the
* time we lock. Eliminating the residual race entirely would require storage
* to NOT re-resolve, which is a bigger refactor (other call sites depend on
* the current contract).
*
* Top-level (not inside `signalStorage`) so `makeLibSignalRepository` can
* compute the same wire address for transaction locking.
*/
const resolveLIDSignalAddress = async (id: string, lidMapping: LIDMappingStore): Promise<string> => {
if (id.includes('.')) {
const [deviceId, device] = id.split('.')
if (!deviceId) {
throw new Error(`Malformed signal address (empty user portion before '.'): "${id}"`)
}
if (device === undefined || device === '') {
throw new Error(`Malformed signal address (empty device portion after '.'): "${id}"`)
}
const [user, domainType_] = deviceId.split('_')
const domainType = parseInt(domainType_ || '0')
if (domainType === WAJIDDomains.LID || domainType === WAJIDDomains.HOSTED_LID) return id
const pnJid = `${user!}${device !== '0' ? `:${device}` : ''}@${domainType === WAJIDDomains.HOSTED ? 'hosted' : 's.whatsapp.net'}`
const lidForPN = await lidMapping.getLIDForPN(pnJid)
if (lidForPN) {
const lidAddr = jidToSignalProtocolAddress(lidForPN)
return lidAddr.toString()
}
}
return id
}
/**
* Extended SignalStorage with identity key management
* This type adds identity key operations to the standard Signal storage
@@ -817,9 +764,30 @@ function signalStorage(
ev?: BaileysEventEmitter,
logger?: ILogger
): ExtendedSignalStorage {
// Bind the top-level `resolveLIDSignalAddress` to this instance's lidMapping
// so call sites below stay readable.
const resolveAddr = (id: string) => resolveLIDSignalAddress(id, lidMapping)
// Shared function to resolve PN signal address to LID if mapping exists
const resolveLIDSignalAddress = async (id: string): Promise<string> => {
if (id.includes('.')) {
const [deviceId, device] = id.split('.')
if (!deviceId) {
throw new Error('Missing device ID')
}
const [user, domainType_] = deviceId.split('_')
const domainType = parseInt(domainType_ || '0')
if (domainType === WAJIDDomains.LID || domainType === WAJIDDomains.HOSTED_LID) return id
const pnJid = `${user!}${device !== '0' ? `:${device}` : ''}@${domainType === WAJIDDomains.HOSTED ? 'hosted' : 's.whatsapp.net'}`
const lidForPN = await lidMapping.getLIDForPN(pnJid)
if (lidForPN) {
const lidAddr = jidToSignalProtocolAddress(lidForPN)
return lidAddr.toString()
}
}
return id
}
// Delayed PreKey deletion: grace period to handle race conditions
// where two pkmsg with the same preKeyId arrive nearly simultaneously.
@@ -831,7 +799,7 @@ function signalStorage(
return {
loadSession: async (id: string) => {
try {
const wireJid = await resolveAddr(id)
const wireJid = await resolveLIDSignalAddress(id)
const { [wireJid]: sess } = await keys.get('session', [wireJid])
if (sess) {
@@ -844,7 +812,7 @@ function signalStorage(
return null
},
storeSession: async (id: string, session: libsignal.SessionRecord) => {
const wireJid = await resolveAddr(id)
const wireJid = await resolveLIDSignalAddress(id)
await keys.set({ session: { [wireJid]: session.serialize() } })
},
isTrustedIdentity: () => {
@@ -921,7 +889,7 @@ function signalStorage(
const timer = metrics.signalIdentityKeyOperations?.startTimer({ operation: 'load' })
try {
const wireJid = await resolveAddr(id)
const wireJid = await resolveLIDSignalAddress(id)
// Check cache first
const cached = identityKeyCache.get(wireJid)
@@ -955,7 +923,7 @@ function signalStorage(
const timer = metrics.signalIdentityKeyOperations?.startTimer({ operation: 'save' })
try {
const wireJid = await resolveAddr(id)
const wireJid = await resolveLIDSignalAddress(id)
const currentFingerprint = generateKeyFingerprint(identityKey)
// Load existing key (from cache or storage)
+12 -40
View File
@@ -67,61 +67,33 @@ export const DECRYPTION_RETRY_CONFIG = {
}
/**
* Retry options for decryption operations.
*
* IMPORTANT fail-fast policy for decryption:
* Both `sessionRecordErrors` ('No matching sessions found', etc.) and
* `corruptedSessionErrors` ('Bad MAC', 'MessageCounterError', missing keys)
* return `false` from `shouldRetry`. Rationale:
*
* 1. libsignal already scanned ALL stored sessions for the JID before
* throwing retrying immediately gives the SAME result (no new session
* record materialises in the 200-800ms backoff window).
* 2. The real recovery flow is upstream: failed decrypt retry receipt to
* WA phone re-sends as `pkmsg` libsignal builds a fresh session
* next message decrypts cleanly. That handshake takes ~300ms total.
* 3. Wrapping decrypt in 3 attempts × exponential backoff (200ms400ms800ms
* 1.4 s per failed message) just blocks the inbound buffer pipeline. At
* the rate own DSM messages flood in after a LID/PN mismatch, this
* compounds to tens of seconds of accumulated delay before live messages
* from real contacts can even reach the consumer.
*
* Only truly *unknown* errors get a single retry those might be transient
* (network blip, unexpected exception) and a quick 200ms retry is cheap.
*
* If we ever need transient-error retries again (e.g. the storage layer adds
* an async race that benefits from re-reading), set `sessionRecordErrors` to
* `attempt < 1` here, NOT `attempt < 3` one extra read at most.
* Retry options for decryption operations
* Uses exponential backoff with jitter to handle transient failures
*/
export const DECRYPTION_RETRY_OPTIONS: RetryOptions = {
maxAttempts: 2,
baseDelay: 200, // 200ms base delay (only used for unknown errors below)
maxDelay: 2000,
maxAttempts: 3,
baseDelay: 200, // 200ms base delay
maxDelay: 2000, // 2s max delay
backoffStrategy: 'exponential',
backoffMultiplier: 2,
jitter: 0.2,
collectMetrics: false,
jitter: 0.2, // 20% jitter
collectMetrics: false, // No Prometheus metrics
operationName: 'message_decryption',
shouldRetry: (error: Error, attempt: number) => {
const errorMsg = error?.message || ''
// Session record errors: libsignal already exhausted all stored sessions.
// Retrying immediately gives the same result; the real recovery path
// is the upstream retry-receipt → pkmsg flow. Fail fast.
// Always retry on session record errors (session might be syncing)
if (DECRYPTION_RETRY_CONFIG.sessionRecordErrors.some(err => errorMsg.includes(err))) {
return false
return attempt < 3 // Retry up to 3 times
}
// Corrupted session errors: Bad MAC / counter errors. Same reasoning —
// the keys are wrong and won't right themselves on retry.
// Don't retry on corrupted session errors (need cleanup first)
if (DECRYPTION_RETRY_CONFIG.corruptedSessionErrors.some(err => errorMsg.includes(err))) {
return false
}
// Unknown errors: one retry in case it was a transient blip.
// `attempt` is 1-based (retry-utils starts the loop at 1), so `attempt < 2`
// allows the second pass and returns false on the third.
return attempt < 2
// Retry other transient errors
return attempt < 2 // Retry up to 2 times for unknown errors
}
}