Compare commits

...

52 Commits

Author SHA1 Message Date
github-actions[bot] 8669fa981b chore: update proto/version to v2.3000.1039067946 (#414)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-05-08 01:03:15 -03:00
Renato Alcara 2c0ec5ef18 chore: update WhatsApp Web version to v2.3000.1038989433 (#413)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-07 06:49:42 -03:00
github-actions[bot] e2dce302f2 chore: update proto/version to v2.3000.1038967158 (#412)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-05-07 01:13:54 -03:00
github-actions[bot] 0300f48348 chore: update proto/version to v2.3000.1038839325 (#409)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-05-06 01:14:15 -03:00
Renato Alcara 6d677636ce chore: update WhatsApp Web version to v2.3000.1038802702 (#411)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-05 06:34:47 -03:00
Renato Alcara 3e0c54f18d chore: update WhatsApp Web version to v2.3000.1038706941 (#410)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-04 06:42:21 -03:00
Renato Alcara 718eacd898 chore: update WhatsApp Web version to v2.3000.1038685473 (#408)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-03 06:29:04 -03:00
github-actions[bot] 1185631f6d chore: update proto/version to v2.3000.1038684444 (#407)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-05-03 01:16:27 -03:00
Renato Alcara a6ba925474 chore: update WhatsApp Web version to v2.3000.1038669233 (#406)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-02 06:23:31 -03:00
github-actions[bot] 1459888643 chore: update proto/version to v2.3000.1038661716 (#405)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-05-02 01:04:18 -03:00
Renato Alcara 4a58c014ca fix(socket): give init queries headroom + add CB kill-switch env var
Production report: after scanning QR for a brand-new channel the user gets
"WhatsApp não inicializado" / "1 sem ativação" because the `socket-query`
circuit breaker opens during the post-pairing `init queries` (USync,
device-list, app-state-sync). Existing channels are unaffected — only fresh
pairings trip the CB because their first round of metadata fetches is slow
(no warm server-side cache for the client, larger payload).

Two small changes:

1. Bump the `socket-query` CB timeout floor to 120 s (was
   `defaultQueryTimeoutMs || 60000`, which evaluated to 30 s in this
   build). The per-query `waitForMessage` timeout still enforces a tighter
   bound for individual operations — this only widens the *cumulative*
   window the CB watches before tripping.

2. Add `BAILEYS_DISABLE_CIRCUIT_BREAKER=true` env var as an emergency
   kill-switch so operators can disable all three CBs (query, connection,
   prekey) without touching code or rebuilding zpro. The default
   `enableCircuitBreaker` config flag still works for callers that pass it
   in code.

No behavior change for existing channels under normal load — the bump only
matters when init queries actually take longer than 30 s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:40:13 -03:00
Renato Alcara d25c5055a2 chore: bump WhatsApp Web version to v2.3000.1038585534 on Mar/18 base
Restoring master to commit d04a3e1af8 ("fix: restore working carousel
send/render path (#297)") which is the LAST KNOWN GOOD state before the
inbound latency regression. PR #303 (pastParticipants HistorySync LID)
and subsequent commits introduced the per-message LID/PN session drift
that locked the inbound pipeline behind Bad MAC retry storms in
production.

WHAT IS PRESERVED:
- Tag `archive/master-pre-restore-2026-05-01` points at the previous
  master tip (d03b625a89, "chore: update WhatsApp Web version (#404)")
- Branch `master-archive-pre-restore-2026-05-01` mirrors that tip for
  easy browsing
- All 137 commits between Mar/18 and the previous master tip remain in
  git history; downstream forks lose nothing

WHAT IS LOST FROM MASTER (preserved in archive):
- View-once features (#310, #316, #322, #355)
- USync username inbound (#382)
- Early-ignore JIDs (#383)
- History sync memory/CPU optimisation (#385)
- tcToken full lifecycle (#386, #387)
- Async LID mapping restore (#390)
- Decrypt-error noise reduction (#391)
- Detach post-upsert work (#392)
- Bounded-retry replacing CircuitBreaker (#393)
- wire address lock (#396)
- Decryption fail-fast (#402)
- ~30 WhatsApp Web/proto version updates (only the latest is reapplied)

Re-add features one at a time on top of this base, testing each so the
inbound regression doesn't silently sneak back in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:53:29 -03:00
Renato Alcara d04a3e1af8 fix: restore working carousel send/render path (#297)
fix: restore working carousel send/render path (#297)
2026-03-18 20:30:18 -03:00
Renato Alcara 21462c26d5 chore: update WhatsApp Web version to v2.3000.1035406580 (#299)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 06:42:44 -03:00
github-actions[bot] fc2122b11a chore: update proto/version to v2.3000.1035370989 (#298)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-18 01:49:58 -03:00
Renato Alcara 11eed3aedc chore: update WhatsApp Web version to v2.3000.1035317910 (#296)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 06:16:35 -03:00
github-actions[bot] dbc6dd6aed chore: update proto/version to v2.3000.1035302375 (#295)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-17 00:46:29 -03:00
Renato Alcara e152aee740 Fix/pn lid session reuse (#294)
* fix: unify PN and LID session reuse for 1:1 sends

* fix: limit PN/LID reuse changes to session cache
2026-03-16 23:22:05 -03:00
Renato Alcara ff9b84c561 fix: unify PN and LID session reuse for 1:1 sends (#293)
fix: unify PN and LID session reuse for 1:1 sends (#293)
2026-03-16 22:59:28 -03:00
Renato Alcara 50c13398c4 fix: harden carousel live rendering path (#292)
* fix: harden carousel live rendering path

* fix: flatten carousel thumbnail fallback
2026-03-16 22:09:14 -03:00
Renato Alcara d991d0212d fix: add biz node injection and quality_control for carousel WhatsApp Web rendering (#291)
- Add interactive message detection helpers (getButtonType, isCarouselMessage, etc.)
- Inject biz node with interactive/native_flow for non-carousel interactive messages
- Inject biz + quality_control with decision_id for carousel messages
- Skip bot node for native_flow/carousel/catalog (breaks Web rendering)
- Force device-identity inclusion for carousel messages
- Append deferred biz/bot/quality_control nodes after device-identity and tctoken
- Store tctoken under both LID and PN for reliable lookup
2026-03-16 21:25:29 -03:00
Renato Alcara 7c095c84a5 chore: update WhatsApp Web version to v2.3000.1035223526 (#289)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-16 06:20:34 -03:00
github-actions[bot] a25e6740fd chore: update proto/version to v2.3000.1035216863 (#288)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-16 00:58:19 -03:00
Renato Alcara 986ac588a9 chore: update WhatsApp Web version to v2.3000.1035203283 (#287)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-15 06:08:25 -03:00
github-actions[bot] 64c73faab2 chore: update proto/version to v2.3000.1035198198 (#286)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-15 00:56:35 -03:00
Renato Alcara b5464bec73 chore: update WhatsApp Web version to v2.3000.1035176028 (#285)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-14 06:07:31 -03:00
github-actions[bot] 9f8f74fc7a chore: update proto/version to v2.3000.1035169204 (#284)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-14 00:44:33 -03:00
Renato Alcara aaf3bd83eb chore: update WhatsApp Web version to v2.3000.1035102652 (#283)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-13 06:11:31 -03:00
github-actions[bot] 8fe71b03d5 chore: update proto/version to v2.3000.1035083696 (#282)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-13 00:45:20 -03:00
Renato Alcara c2c156d707 chore(deps): update music-metadata from 11.12.0 to 11.12.3 (#281)
Includes security fix for CWE-85 (infinite loop in ASF parsing),
Ogg/Vorbis duration fix, and restored TypeScript declaration files.
2026-03-12 23:52:05 -03:00
Renato Alcara e46b9750c2 chore: update WhatsApp Web version to v2.3000.1035018135 (#280)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 06:13:34 -03:00
github-actions[bot] 542b7a278d chore: update proto/version to v2.3000.1034989030 (#279)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-12 00:45:49 -03:00
Renato Alcara 6d7e0d7b35 chore: update WhatsApp Web version to v2.3000.1034928492 (#278)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-11 06:12:36 -03:00
github-actions[bot] 9daee4d8fd chore: update proto/version to v2.3000.1034906703 (#277)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-11 00:44:13 -03:00
Renato Alcara d7b2ceb161 chore: update WhatsApp Web version to v2.3000.1034844076 (#276)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-10 06:12:48 -03:00
github-actions[bot] 2236ef83a9 chore: update proto/version to v2.3000.1034813620 (#275)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-10 00:44:00 -03:00
Renato Alcara 07f775dd18 chore: update WhatsApp Web version to v2.3000.1034756368 (#274)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 06:14:04 -03:00
github-actions[bot] ad81d1535d chore: update proto/version to v2.3000.1034750325 (#273)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-09 00:46:14 -03:00
Renato Alcara 7ecfaefcfa chore: update WhatsApp Web version to v2.3000.1034735671 (#272)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 06:05:18 -03:00
github-actions[bot] ad4f676e2b chore: update proto/version to v2.3000.1034732001 (#271)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-08 00:45:02 -03:00
Renato Alcara 54d6a023a4 chore: update WhatsApp Web version to v2.3000.1034715486 (#270)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 06:05:25 -03:00
github-actions[bot] 935484c790 chore: update proto/version to v2.3000.1034696150 (#269)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-07 00:37:49 -03:00
Renato Alcara f2c14f9ad6 fix: carousel renders on WhatsApp Web without F5
Two fixes:
1. Stanza type="media" for carousel messages — WhatsApp Web needs this
   to render carousel in real-time. Without it, only shows header until
   page refresh (F5).
2. Fix jimp detection: typeof Jimp === 'function' (not just 'object')
   so jpegThumbnail is generated for card images.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:25:07 -03:00
Renato Alcara ac2acf3d87 Fix/carousel working minimal (#268)
Fix/carousel working minimal (#268)
2026-03-06 23:58:59 -03:00
Renato Alcara 0b527b0855 fix: inline carousel handler for WhatsApp Web rendering (#267)
fix: inline carousel handler for WhatsApp Web rendering (#267)
2026-03-06 23:26:43 -03:00
Renato Alcara 9f8b603e28 fix: skip biz node for carousel messages (fixes WhatsApp Web rendering) (#265)
fix: skip biz node for carousel messages (fixes WhatsApp Web rendering) (#265)
2026-03-06 21:29:26 -03:00
Renato Alcara 6c904484e4 chore: update WhatsApp Web version to v2.3000.1034641024 (#264)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 06:09:40 -03:00
github-actions[bot] 5382d03f93 chore: update proto/version to v2.3000.1034621774 (#263)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-06 00:44:39 -03:00
Renato Alcara 5e602ebbf8 chore: update WhatsApp Web version to v2.3000.1034553229 (#262)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 06:12:30 -03:00
github-actions[bot] 12c400cf1e chore: update proto/version to v2.3000.1034530777 (#261)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-05 00:45:05 -03:00
Renato Alcara 92085d283a fix(sticker): align sticker pack with WABA Android (#260)
fix(sticker): align sticker pack with WABA Android (#260)
2026-03-04 10:19:25 -03:00
Renato Alcara 38aa34ffb0 chore: update WhatsApp Web version to v2.3000.1034439448 (#259)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-04 06:10:49 -03:00
14 changed files with 9052 additions and 268 deletions
+407 -4
View File
@@ -1,7 +1,7 @@
syntax = "proto3"; syntax = "proto3";
package proto; package proto;
/// WhatsApp Version: 2.3000.1034427372 /// WhatsApp Version: 2.3000.1039067946
message ADVDeviceIdentity { message ADVDeviceIdentity {
optional uint32 rawId = 1; optional uint32 rawId = 1;
@@ -14,6 +14,7 @@ message ADVDeviceIdentity {
enum ADVEncryptionType { enum ADVEncryptionType {
E2EE = 0; E2EE = 0;
HOSTED = 1; HOSTED = 1;
NON_E2EE = 2;
} }
message ADVKeyIndexList { message ADVKeyIndexList {
optional uint32 rawId = 1; optional uint32 rawId = 1;
@@ -61,6 +62,7 @@ message AIHomeState {
ANIMATE_PHOTO = 2; ANIMATE_PHOTO = 2;
ANALYZE_FILE = 3; ANALYZE_FILE = 3;
COLLABORATE = 4; COLLABORATE = 4;
OPEN_GREETING_CARD = 5;
} }
} }
@@ -74,6 +76,7 @@ message AIMediaCollectionMessage {
message AIMediaCollectionMetadata { message AIMediaCollectionMetadata {
optional string collectionId = 1; optional string collectionId = 1;
optional uint32 uploadOrderIndex = 2;
} }
message AIQueryFanout { message AIQueryFanout {
@@ -246,15 +249,27 @@ message AIRichResponseUnifiedResponse {
optional bytes data = 1; 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 { message AIThreadInfo {
optional AIThreadServerInfo serverInfo = 1; optional AIThreadServerInfo serverInfo = 1;
optional AIThreadClientInfo clientInfo = 2; optional AIThreadClientInfo clientInfo = 2;
message AIThreadClientInfo { message AIThreadClientInfo {
optional AIThreadType type = 1; optional AIThreadType type = 1;
optional string sourceChatJid = 2;
enum AIThreadType { enum AIThreadType {
UNKNOWN = 0; UNKNOWN = 0;
DEFAULT = 1; DEFAULT = 1;
INCOGNITO = 2; INCOGNITO = 2;
SIDE_CHAT = 3;
} }
} }
@@ -342,6 +357,14 @@ message BotAgeCollectionMetadata {
} }
} }
message BotAgentDeepLinkMetadata {
optional string token = 1;
}
message BotAgentMetadata {
optional BotAgentDeepLinkMetadata deepLinkMetadata = 1;
}
message BotCapabilityMetadata { message BotCapabilityMetadata {
repeated BotCapabilityType capabilities = 1; repeated BotCapabilityType capabilities = 1;
enum BotCapabilityType { enum BotCapabilityType {
@@ -403,9 +426,19 @@ message BotCapabilityMetadata {
RICH_RESPONSE_UR_BLOKS_ENABLED = 55; RICH_RESPONSE_UR_BLOKS_ENABLED = 55;
RICH_RESPONSE_INLINE_LINKS_ENABLED = 56; RICH_RESPONSE_INLINE_LINKS_ENABLED = 56;
RICH_RESPONSE_UR_IMAGINE_VIDEO = 57; RICH_RESPONSE_UR_IMAGINE_VIDEO = 57;
JSON_PATCH_STREAMING = 58;
AI_TAB_FORCE_CLIPPY = 59;
UNIFIED_RESPONSE_EMBEDDED_SCREENS = 60;
AI_SUBSCRIPTION_ENABLED = 61;
} }
} }
message BotCommandMetadata {
optional string commandName = 1;
optional string commandDescription = 2;
optional string commandPrompt = 3;
}
message BotDocumentMessageMetadata { message BotDocumentMessageMetadata {
optional DocumentPluginType pluginType = 1; optional DocumentPluginType pluginType = 1;
enum DocumentPluginType { enum DocumentPluginType {
@@ -637,6 +670,9 @@ message BotMetadata {
optional BotRenderingConfigMetadata botRenderingConfigMetadata = 36; optional BotRenderingConfigMetadata botRenderingConfigMetadata = 36;
optional BotInfrastructureDiagnostics botInfrastructureDiagnostics = 37; optional BotInfrastructureDiagnostics botInfrastructureDiagnostics = 37;
optional AIMediaCollectionMetadata aiMediaCollectionMetadata = 38; optional AIMediaCollectionMetadata aiMediaCollectionMetadata = 38;
optional BotCommandMetadata commandMetadata = 39;
optional BotResolvedToolCallMetadata resolvedToolCallMetadata = 40;
optional AISubscriptionUpsellMetadata subscriptionUpsellMetadata = 41;
optional bytes internalMetadata = 999; optional bytes internalMetadata = 999;
} }
@@ -687,6 +723,8 @@ enum BotMetricsEntryPoint {
WEB_INTRO_PANEL = 46; WEB_INTRO_PANEL = 46;
WEB_NAVIGATION_BAR = 47; WEB_NAVIGATION_BAR = 47;
GROUP_MEMBER = 54; GROUP_MEMBER = 54;
CHATLIST_SEARCH = 55;
NEW_CHAT_LIST = 56;
} }
message BotMetricsMetadata { message BotMetricsMetadata {
optional string destinationId = 1; optional string destinationId = 1;
@@ -873,6 +911,11 @@ message BotRenderingMetadata {
} }
message BotResolvedToolCallMetadata {
optional string toolCallId = 1;
optional string resolutionDataSerialized = 2;
}
message BotSessionMetadata { message BotSessionMetadata {
optional string sessionId = 1; optional string sessionId = 1;
optional BotSessionSource sessionSource = 2; optional BotSessionSource sessionSource = 2;
@@ -1069,6 +1112,7 @@ message ClientPairingProps {
optional bool isSyncdPureLidSession = 2; optional bool isSyncdPureLidSession = 2;
optional bool isSyncdSnapshotRecoveryEnabled = 3; optional bool isSyncdSnapshotRecoveryEnabled = 3;
optional bool isHsThumbnailSyncEnabled = 4; optional bool isHsThumbnailSyncEnabled = 4;
optional bytes subscriptionSyncPayload = 5;
} }
message ClientPayload { message ClientPayload {
@@ -1106,6 +1150,8 @@ message ClientPayload {
optional bool paaLink = 44; optional bool paaLink = 44;
optional int32 preacksCount = 45; optional int32 preacksCount = 45;
optional int32 processingQueueSize = 46; optional int32 processingQueueSize = 46;
repeated string pairedPeripherals = 47;
optional bytes testIsolationId = 48;
enum AccountType { enum AccountType {
DEFAULT = 0; DEFAULT = 0;
GUEST = 1; GUEST = 1;
@@ -1201,6 +1247,7 @@ message ClientPayload {
optional string deviceExpId = 14; optional string deviceExpId = 14;
optional DeviceType deviceType = 15; optional DeviceType deviceType = 15;
optional string deviceModelType = 16; optional string deviceModelType = 16;
optional DistributionChannel distributionChannel = 17;
message AppVersion { message AppVersion {
optional uint32 primary = 1; optional uint32 primary = 1;
optional uint32 secondary = 2; optional uint32 secondary = 2;
@@ -1216,6 +1263,12 @@ message ClientPayload {
WEARABLE = 3; WEARABLE = 3;
VR = 4; VR = 4;
} }
enum DistributionChannel {
APPSTORE = 0;
WEBSITE = 1;
TESTFLIGHT = 2;
INTERNAL = 3;
}
enum Platform { enum Platform {
ANDROID = 0; ANDROID = 0;
IOS = 1; IOS = 1;
@@ -1385,6 +1438,9 @@ message ContextInfo {
optional MediaDomainInfo mediaDomainInfo = 74; optional MediaDomainInfo mediaDomainInfo = 74;
optional PartiallySelectedContent partiallySelectedContent = 75; optional PartiallySelectedContent partiallySelectedContent = 75;
optional uint32 afterReadDuration = 76; optional uint32 afterReadDuration = 76;
optional CrossAppSource crossAppSource = 77;
optional BusinessInteractionPills businessInteractionPills = 78;
optional string posterStatusId = 79;
message AdReplyInfo { message AdReplyInfo {
optional string advertiserName = 1; optional string advertiserName = 1;
optional MediaType mediaType = 2; optional MediaType mediaType = 2;
@@ -1397,10 +1453,47 @@ 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 { message BusinessMessageForwardInfo {
optional string businessOwnerJid = 1; optional string businessOwnerJid = 1;
} }
enum CrossAppSource {
CROSS_APP_SOURCE_UNKNOWN = 0;
CROSS_APP_SOURCE_INSTAGRAM = 1;
CROSS_APP_SOURCE_FACEBOOK = 2;
}
message DataSharingContext { message DataSharingContext {
optional bool showMmDisclosure = 1; optional bool showMmDisclosure = 1;
optional string encryptedSignalTokenConsented = 2; optional string encryptedSignalTokenConsented = 2;
@@ -1448,6 +1541,11 @@ message ContextInfo {
optional AdType adType = 25; optional AdType adType = 25;
optional string wtwaWebsiteUrl = 26; optional string wtwaWebsiteUrl = 26;
optional string adPreviewUrl = 27; optional string adPreviewUrl = 27;
optional bool containsCtwaFlowsAutoReply = 28;
optional int32 agmThumbnailStrategy = 29;
optional int32 agmTitleStrategy = 30;
optional int32 agmSubtitleStrategy = 31;
optional int32 agmHeaderInteractionStrategy = 32;
enum AdType { enum AdType {
CTWA = 0; CTWA = 0;
CAWC = 1; CAWC = 1;
@@ -1602,12 +1700,25 @@ message Conversation {
optional bool limitSharingInitiatedByMe = 53; optional bool limitSharingInitiatedByMe = 53;
optional bool maibaAiThreadEnabled = 54; optional bool maibaAiThreadEnabled = 54;
optional bool isMarketingMessageThread = 55; optional bool isMarketingMessageThread = 55;
optional bool isSenderNewAccount = 56;
optional uint32 afterReadDuration = 57;
optional bool isSenderSuspicious = 58;
optional GroupAppealStatus appealStatus = 59;
optional uint64 appealUpdateTime = 60;
optional string authAgentParentCompanyName = 61;
optional string authAgentObaPhoneNumber = 62;
enum EndOfHistoryTransferType { enum EndOfHistoryTransferType {
COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY = 0; COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY = 0;
COMPLETE_AND_NO_MORE_MESSAGE_REMAIN_ON_PRIMARY = 1; COMPLETE_AND_NO_MORE_MESSAGE_REMAIN_ON_PRIMARY = 1;
COMPLETE_ON_DEMAND_SYNC_BUT_MORE_MSG_REMAIN_ON_PRIMARY = 2; COMPLETE_ON_DEMAND_SYNC_BUT_MORE_MSG_REMAIN_ON_PRIMARY = 2;
COMPLETE_ON_DEMAND_SYNC_WITH_MORE_MSG_ON_PRIMARY_BUT_NO_ACCESS = 3; COMPLETE_ON_DEMAND_SYNC_WITH_MORE_MSG_ON_PRIMARY_BUT_NO_ACCESS = 3;
} }
enum GroupAppealStatus {
NO_APPEAL = 0;
APPEAL_IN_REVIEW = 1;
APPEAL_APPROVED = 2;
APPEAL_REJECTED = 3;
}
} }
message DeviceCapabilities { message DeviceCapabilities {
@@ -1628,6 +1739,10 @@ message DeviceCapabilities {
message BusinessBroadcast { message BusinessBroadcast {
optional bool importListEnabled = 1; optional bool importListEnabled = 1;
optional bool companionSupportEnabled = 2;
optional bool campaignSyncEnabled = 3;
optional bool insightsSyncEnabled = 4;
optional int32 recipientLimit = 5;
} }
enum ChatLockSupportLevel { enum ChatLockSupportLevel {
@@ -1702,6 +1817,9 @@ message DeviceProps {
optional uint32 thumbnailSyncDaysLimit = 19; optional uint32 thumbnailSyncDaysLimit = 19;
optional uint32 initialSyncMaxMessagesPerChat = 20; optional uint32 initialSyncMaxMessagesPerChat = 20;
optional bool supportManusHistory = 21; optional bool supportManusHistory = 21;
optional bool supportHatchHistory = 22;
repeated string supportedBotChannelFbids = 23;
optional bool supportInlineContacts = 24;
} }
enum PlatformType { enum PlatformType {
@@ -1888,6 +2006,16 @@ message GroupParticipant {
} }
} }
message GroupRootKeyShare {
repeated GroupRootKeyShareEntry keys = 1;
}
message GroupRootKeyShareEntry {
optional bytes groupRootKey = 1;
optional string keyId = 2;
optional int64 expiryTimestampMs = 3;
}
message HandshakeMessage { message HandshakeMessage {
optional ClientHello clientHello = 2; optional ClientHello clientHello = 2;
optional ServerHello serverHello = 3; optional ServerHello serverHello = 3;
@@ -1896,6 +2024,8 @@ message HandshakeMessage {
optional bytes static = 1; optional bytes static = 1;
optional bytes payload = 2; optional bytes payload = 2;
optional bytes extendedCiphertext = 3; optional bytes extendedCiphertext = 3;
optional bytes paddedBytes = 4;
optional bool simulateXxkemFs = 5;
} }
message ClientHello { message ClientHello {
@@ -1904,13 +2034,31 @@ message HandshakeMessage {
optional bytes payload = 3; optional bytes payload = 3;
optional bool useExtended = 4; optional bool useExtended = 4;
optional bytes extendedCiphertext = 5; optional bytes extendedCiphertext = 5;
optional bytes paddedBytes = 6;
optional bool sendServerHelloPaddedBytes = 7;
optional bool simulateXxkemFs = 8;
optional HandshakeMessage.HandshakePqMode pqMode = 9;
optional bytes extendedEphemeral = 10;
} }
enum HandshakePqMode {
HANDSHAKE_PQ_MODE_UNKNOWN = 0;
XXKEM = 1;
XXKEM_FS = 2;
WA_CLASSICAL = 3;
WA_PQ = 4;
IKKEM = 5;
IKKEM_FS = 6;
XXKEM_2 = 7;
IKKEM_2 = 8;
}
message ServerHello { message ServerHello {
optional bytes ephemeral = 1; optional bytes ephemeral = 1;
optional bytes static = 2; optional bytes static = 2;
optional bytes payload = 3; optional bytes payload = 3;
optional bytes extendedStatic = 4; optional bytes extendedStatic = 4;
optional bytes paddingBytes = 5;
optional bytes extendedCiphertext = 6;
} }
} }
@@ -1934,6 +2082,8 @@ message HistorySync {
optional bytes shareableChatIdentifierEncryptionKey = 17; optional bytes shareableChatIdentifierEncryptionKey = 17;
repeated Account accounts = 18; repeated Account accounts = 18;
optional bytes nctSalt = 19; optional bytes nctSalt = 19;
repeated InlineContact inlineContacts = 20;
optional bool inlineContactsProvided = 21;
enum BotAIWaitListState { enum BotAIWaitListState {
IN_WAITLIST = 0; IN_WAITLIST = 0;
AI_AVAILABLE = 1; AI_AVAILABLE = 1;
@@ -2028,6 +2178,14 @@ message InThreadSurveyMetadata {
} }
message InlineContact {
optional string pnJid = 1;
optional string lidJid = 2;
optional string fullName = 3;
optional string firstName = 4;
optional string username = 5;
}
message InteractiveAnnotation { message InteractiveAnnotation {
repeated Point polygonVertices = 1; repeated Point polygonVertices = 1;
optional bool shouldSkipConfirmation = 4; optional bool shouldSkipConfirmation = 4;
@@ -2106,7 +2264,6 @@ message LimitSharing {
CHAT_SETTING = 1; CHAT_SETTING = 1;
BIZ_SUPPORTS_FB_HOSTING = 2; BIZ_SUPPORTS_FB_HOSTING = 2;
UNKNOWN_GROUP = 3; UNKNOWN_GROUP = 3;
DEPRECATION = 4;
} }
} }
@@ -2267,6 +2424,10 @@ message Message {
optional PollCreationMessage pollCreationMessageV6 = 119; optional PollCreationMessage pollCreationMessageV6 = 119;
optional ConditionalRevealMessage conditionalRevealMessage = 120; optional ConditionalRevealMessage conditionalRevealMessage = 120;
optional PollAddOptionMessage pollAddOptionMessage = 121; optional PollAddOptionMessage pollAddOptionMessage = 121;
optional EventInviteMessage eventInviteMessage = 122;
optional GroupRootKeyShare groupRootKeyShare = 123;
optional P2PPaymentReminderNotification p2PPaymentReminderNotification = 124;
optional SplitPaymentMessage splitPaymentMessage = 125;
message AlbumMessage { message AlbumMessage {
optional uint32 expectedImageCount = 2; optional uint32 expectedImageCount = 2;
optional uint32 expectedVideoCount = 3; optional uint32 expectedVideoCount = 3;
@@ -2447,6 +2608,41 @@ message Message {
optional string id = 2; 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 { message CloudAPIThreadControlNotification {
optional CloudAPIThreadControl status = 1; optional CloudAPIThreadControl status = 1;
optional int64 senderNotificationTimestampMs = 2; optional int64 senderNotificationTimestampMs = 2;
@@ -2458,6 +2654,7 @@ message Message {
UNKNOWN = 0; UNKNOWN = 0;
CONTROL_PASSED = 1; CONTROL_PASSED = 1;
CONTROL_TAKEN = 2; CONTROL_TAKEN = 2;
INFO = 3;
} }
message CloudAPIThreadControlNotificationContent { message CloudAPIThreadControlNotificationContent {
optional string handoffNotificationText = 1; optional string handoffNotificationText = 1;
@@ -2547,6 +2744,17 @@ message Message {
optional bytes encIv = 3; optional bytes encIv = 3;
} }
message EventInviteMessage {
optional ContextInfo contextInfo = 1;
optional string eventId = 2;
optional string eventTitle = 3;
optional bytes jpegThumbnail = 4;
optional int64 startTime = 5;
optional string caption = 6;
optional bool isCanceled = 7;
optional int64 endTime = 8;
}
message EventMessage { message EventMessage {
optional ContextInfo contextInfo = 1; optional ContextInfo contextInfo = 1;
optional bool isCanceled = 2; optional bool isCanceled = 2;
@@ -2798,6 +3006,13 @@ message Message {
optional bool securityNotificationEnabled = 1; optional bool securityNotificationEnabled = 1;
} }
enum InsightDeliveryState {
SENT = 0;
DELIVERED = 1;
READ = 2;
REPLIED = 3;
QUICK_REPLIED = 4;
}
message InteractiveMessage { message InteractiveMessage {
optional Header header = 1; optional Header header = 1;
optional Body body = 2; optional Body body = 2;
@@ -3066,9 +3281,10 @@ message Message {
message MessageHistoryMetadata { message MessageHistoryMetadata {
repeated string historyReceivers = 1; repeated string historyReceivers = 1;
optional int64 oldestMessageTimestamp = 2; optional int64 oldestMessageTimestampInWindow = 2;
optional int64 messageCount = 3; optional int64 messageCount = 3;
repeated string nonHistoryReceivers = 4; repeated string nonHistoryReceivers = 4;
optional int64 oldestMessageTimestampInBundle = 5;
} }
message MessageHistoryNotice { message MessageHistoryNotice {
@@ -3119,6 +3335,35 @@ 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 { message PaymentExtendedMetadata {
optional uint32 type = 1; optional uint32 type = 1;
optional string platform = 2; optional string platform = 2;
@@ -3129,6 +3374,11 @@ message Message {
optional int64 expiryTimestamp = 2; optional int64 expiryTimestamp = 2;
optional bool incentiveEligible = 3; optional bool incentiveEligible = 3;
optional string referralId = 4; optional string referralId = 4;
optional InviteType inviteType = 5;
enum InviteType {
DEFAULT = 0;
MAPPER = 1;
}
enum ServiceType { enum ServiceType {
UNKNOWN = 0; UNKNOWN = 0;
FBPAY = 1; FBPAY = 1;
@@ -3170,6 +3420,16 @@ message Message {
optional HistorySyncChunkRetryRequest historySyncChunkRetryRequest = 8; optional HistorySyncChunkRetryRequest historySyncChunkRetryRequest = 8;
optional GalaxyFlowAction galaxyFlowAction = 9; optional GalaxyFlowAction galaxyFlowAction = 9;
optional CompanionCanonicalUserNonceFetchRequest companionCanonicalUserNonceFetchRequest = 10; optional CompanionCanonicalUserNonceFetchRequest companionCanonicalUserNonceFetchRequest = 10;
optional BizBroadcastInsightsContactListRequest bizBroadcastInsightsContactListRequest = 11;
optional BizBroadcastInsightsRefreshRequest bizBroadcastInsightsRefreshRequest = 12;
message BizBroadcastInsightsContactListRequest {
optional string campaignId = 1;
}
message BizBroadcastInsightsRefreshRequest {
optional string campaignId = 1;
}
message CompanionCanonicalUserNonceFetchRequest { message CompanionCanonicalUserNonceFetchRequest {
optional string registrationTraceId = 1; optional string registrationTraceId = 1;
} }
@@ -3206,6 +3466,7 @@ message Message {
optional int32 onDemandMsgCount = 4; optional int32 onDemandMsgCount = 4;
optional int64 oldestMsgTimestampMs = 5; optional int64 oldestMsgTimestampMs = 5;
optional string accountLid = 6; optional string accountLid = 6;
optional bool supportInlineResponse = 7;
} }
message PlaceholderMessageResendRequest { message PlaceholderMessageResendRequest {
@@ -3244,6 +3505,18 @@ message Message {
optional CompanionCanonicalUserNonceFetchResponse companionCanonicalUserNonceFetchRequestResponse = 9; optional CompanionCanonicalUserNonceFetchResponse companionCanonicalUserNonceFetchRequestResponse = 9;
optional HistorySyncChunkRetryResponse historySyncChunkRetryResponse = 10; optional HistorySyncChunkRetryResponse historySyncChunkRetryResponse = 10;
optional FlowResponsesCsvBundle flowResponsesCsvBundle = 11; optional FlowResponsesCsvBundle flowResponsesCsvBundle = 11;
optional BizBroadcastInsightsContactListResponse bizBroadcastInsightsContactListResponse = 12;
message BizBroadcastInsightsContactListResponse {
optional string campaignId = 1;
optional int64 timestampMs = 2;
repeated Message.PeerDataOperationRequestResponseMessage.PeerDataOperationResult.BizBroadcastInsightsContactState contacts = 3;
}
message BizBroadcastInsightsContactState {
optional string contactJid = 1;
optional Message.InsightDeliveryState state = 2;
}
message CompanionCanonicalUserNonceFetchResponse { message CompanionCanonicalUserNonceFetchResponse {
optional string nonce = 1; optional string nonce = 1;
optional string waFbid = 2; optional string waFbid = 2;
@@ -3280,6 +3553,7 @@ message Message {
ERROR_REQUEST_ON_NON_SMB_PRIMARY = 4; ERROR_REQUEST_ON_NON_SMB_PRIMARY = 4;
ERROR_HOSTED_DEVICE_NOT_CONNECTED = 5; ERROR_HOSTED_DEVICE_NOT_CONNECTED = 5;
ERROR_HOSTED_DEVICE_LOGIN_TIME_NOT_SET = 6; ERROR_HOSTED_DEVICE_LOGIN_TIME_NOT_SET = 6;
ERROR_MULTI_PROVIDER_NOT_CONFIGURED = 7;
} }
message HistorySyncChunkRetryResponse { message HistorySyncChunkRetryResponse {
optional Message.HistorySyncType syncType = 1; optional Message.HistorySyncType syncType = 1;
@@ -3357,6 +3631,8 @@ message Message {
COMPANION_CANONICAL_USER_NONCE_FETCH = 9; COMPANION_CANONICAL_USER_NONCE_FETCH = 9;
HISTORY_SYNC_CHUNK_RETRY = 10; HISTORY_SYNC_CHUNK_RETRY = 10;
GALAXY_FLOW_ACTION = 11; GALAXY_FLOW_ACTION = 11;
BUSINESS_BROADCAST_INSIGHTS_DELIVERED_TO = 12;
BUSINESS_BROADCAST_INSIGHTS_REFRESH = 13;
} }
message PinInChatMessage { message PinInChatMessage {
optional MessageKey key = 1; optional MessageKey key = 1;
@@ -3497,6 +3773,7 @@ message Message {
optional MemberLabel memberLabel = 27; optional MemberLabel memberLabel = 27;
optional AIMediaCollectionMessage aiMediaCollectionMessage = 28; optional AIMediaCollectionMessage aiMediaCollectionMessage = 28;
optional uint32 afterReadDuration = 29; optional uint32 afterReadDuration = 29;
optional Message.ChatThemeSetting chatThemeSetting = 30;
enum Type { enum Type {
REVOKE = 0; REVOKE = 0;
EPHEMERAL_SETTING = 3; EPHEMERAL_SETTING = 3;
@@ -3526,6 +3803,7 @@ message Message {
GROUP_MEMBER_LABEL_CHANGE = 30; GROUP_MEMBER_LABEL_CHANGE = 30;
AI_MEDIA_COLLECTION_MESSAGE = 31; AI_MEDIA_COLLECTION_MESSAGE = 31;
MESSAGE_UNSCHEDULE = 32; MESSAGE_UNSCHEDULE = 32;
CHAT_THEME_SETTING = 34;
} }
} }
@@ -3558,6 +3836,7 @@ message Message {
message RequestWelcomeMessageMetadata { message RequestWelcomeMessageMetadata {
optional LocalChatState localChatState = 1; optional LocalChatState localChatState = 1;
optional WelcomeTrigger welcomeTrigger = 2; optional WelcomeTrigger welcomeTrigger = 2;
optional BotAgentMetadata botAgentMetadata = 3;
enum LocalChatState { enum LocalChatState {
EMPTY = 0; EMPTY = 0;
NON_EMPTY = 1; NON_EMPTY = 1;
@@ -3616,6 +3895,26 @@ message Message {
optional bytes axolotlSenderKeyDistributionMessage = 2; 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 { message StatusNotificationMessage {
optional MessageKey responseMessageKey = 1; optional MessageKey responseMessageKey = 1;
optional MessageKey originalMessageKey = 2; optional MessageKey originalMessageKey = 2;
@@ -3675,6 +3974,7 @@ message Message {
optional bool isLottie = 21; optional bool isLottie = 21;
optional string accessibilityLabel = 22; optional string accessibilityLabel = 22;
optional int32 premium = 24; optional int32 premium = 24;
optional string emojis = 25;
} }
message StickerPackMessage { message StickerPackMessage {
@@ -3895,6 +4195,7 @@ message MessageContextInfo {
optional LimitSharing limitSharingV2 = 14; optional LimitSharing limitSharingV2 = 14;
repeated ThreadID threadId = 15; repeated ThreadID threadId = 15;
optional WebLinkRenderConfig weblinkRenderConfig = 16; optional WebLinkRenderConfig weblinkRenderConfig = 16;
optional bytes teeBotMetadata = 17;
enum MessageAddonExpiryType { enum MessageAddonExpiryType {
STATIC = 1; STATIC = 1;
DEPENDENT_ON_PARENT = 2; DEPENDENT_ON_PARENT = 2;
@@ -3967,6 +4268,7 @@ message MsgOpaqueData {
optional string quarantineExtractedText = 48; optional string quarantineExtractedText = 48;
optional int64 pollEndTime = 49; optional int64 pollEndTime = 49;
optional bool pollHideVoterNames = 50; optional bool pollHideVoterNames = 50;
optional bool pollAllowAddOption = 52;
message EventLocation { message EventLocation {
optional double degreesLatitude = 1; optional double degreesLatitude = 1;
optional double degreesLongitude = 2; optional double degreesLongitude = 2;
@@ -4083,6 +4385,11 @@ enum MutationProps {
NCT_SALT_SYNC_ACTION = 80; NCT_SALT_SYNC_ACTION = 80;
BUSINESS_BROADCAST_CAMPAIGN_ACTION = 81; BUSINESS_BROADCAST_CAMPAIGN_ACTION = 81;
BUSINESS_BROADCAST_INSIGHTS_ACTION = 82; BUSINESS_BROADCAST_INSIGHTS_ACTION = 82;
CUSTOMER_DATA_ACTION = 83;
SUBSCRIPTIONS_SYNC_V2_ACTION = 84;
THREAD_PIN_ACTION = 85;
AUTO_ORGANIZE_BUSINESS_CHAT_SETTING = 86;
BIZ_AI_SETTINGS_NUDGE_ACTION = 87;
SHARE_OWN_PN = 10001; SHARE_OWN_PN = 10001;
BUSINESS_BROADCAST_ACTION = 10002; BUSINESS_BROADCAST_ACTION = 10002;
AI_THREAD_DELETE_ACTION = 10003; AI_THREAD_DELETE_ACTION = 10003;
@@ -4396,6 +4703,12 @@ message ReportingTokenInfo {
optional bytes reportingTag = 1; optional bytes reportingTag = 1;
} }
message ScheduledMessageMetadata {
optional string revealKeyId = 1;
optional bytes revealKey = 2;
optional uint64 scheduledTime = 3;
}
message SenderKeyDistributionMessage { message SenderKeyDistributionMessage {
optional uint32 id = 1; optional uint32 id = 1;
optional uint32 iteration = 2; optional uint32 iteration = 2;
@@ -4551,6 +4864,8 @@ message StatusAttribution {
APPLE_MUSIC = 8; APPLE_MUSIC = 8;
SHARECHAT = 9; SHARECHAT = 9;
GOOGLE_PHOTOS = 10; GOOGLE_PHOTOS = 10;
SOUNDCLOUD = 11;
SHAZAM = 12;
} }
} }
@@ -4608,6 +4923,7 @@ message StatusAttribution {
LAYOUTS = 8; LAYOUTS = 8;
NEWSLETTER_STATUS = 9; NEWSLETTER_STATUS = 9;
STATUS_CLOSE_SHARING = 10; STATUS_CLOSE_SHARING = 10;
PAID_PARTNERSHIP = 11;
} }
} }
@@ -4718,6 +5034,11 @@ message SyncActionValue {
optional NctSaltSyncAction nctSaltSyncAction = 80; optional NctSaltSyncAction nctSaltSyncAction = 80;
optional BusinessBroadcastCampaignAction businessBroadcastCampaignAction = 81; optional BusinessBroadcastCampaignAction businessBroadcastCampaignAction = 81;
optional BusinessBroadcastInsightsAction businessBroadcastInsightsAction = 82; optional BusinessBroadcastInsightsAction businessBroadcastInsightsAction = 82;
optional CustomerDataAction customerDataAction = 83;
optional SubscriptionsSyncV2Action subscriptionsSyncV2Action = 84;
optional ThreadPinAction threadPinAction = 85;
optional AutoOrganizeBusinessChatSetting autoOrganizeBusinessChatSetting = 86;
optional BizAISettingsNudgeAction bizAiSettingsNudgeAction = 87;
message AgentAction { message AgentAction {
optional string name = 1; optional string name = 1;
optional int32 deviceID = 2; optional int32 deviceID = 2;
@@ -4737,6 +5058,10 @@ message SyncActionValue {
optional SyncActionValue.SyncActionMessageRange messageRange = 2; optional SyncActionValue.SyncActionMessageRange messageRange = 2;
} }
message AutoOrganizeBusinessChatSetting {
optional bool autoOrganize = 1;
}
message AvatarUpdatedAction { message AvatarUpdatedAction {
optional AvatarEventType eventType = 1; optional AvatarEventType eventType = 1;
repeated SyncActionValue.StickerAction recentAvatarStickers = 2; repeated SyncActionValue.StickerAction recentAvatarStickers = 2;
@@ -4747,6 +5072,20 @@ message SyncActionValue {
} }
} }
message BizAISettingsNudgeAction {
optional BizAISettingsCategory category = 1;
optional int64 version = 2;
optional int64 updatedAtMs = 3;
enum BizAISettingsCategory {
UNKNOWN = 0;
INSTRUCTIONS = 1;
RESPONSE_SETTINGS = 2;
EXAMPLE_RESPONSES = 3;
KNOWLEDGE = 4;
LEAD_GEN = 5;
}
}
message BotWelcomeRequestAction { message BotWelcomeRequestAction {
optional bool isSent = 1; optional bool isSent = 1;
} }
@@ -4792,6 +5131,7 @@ message SyncActionValue {
repeated SyncActionValue.BroadcastListParticipant participants = 2; repeated SyncActionValue.BroadcastListParticipant participants = 2;
optional string listName = 3; optional string listName = 3;
repeated string labelIds = 4; repeated string labelIds = 4;
optional string audienceExpression = 5;
} }
message CallLogAction { message CallLogAction {
@@ -4839,6 +5179,20 @@ message SyncActionValue {
repeated SyncActionValue.CustomPaymentMethod customPaymentMethods = 1; repeated SyncActionValue.CustomPaymentMethod customPaymentMethods = 1;
} }
message CustomerDataAction {
optional string chatJid = 1;
optional int32 contactType = 2;
optional string email = 3;
optional string altPhoneNumbers = 4;
optional int64 birthday = 5;
optional string address = 6;
optional int32 acquisitionSource = 7;
optional int32 leadStage = 8;
optional int64 lastOrder = 9;
optional int64 createdAt = 10;
optional int64 modifiedAt = 11;
}
message DeleteChatAction { message DeleteChatAction {
optional SyncActionValue.SyncActionMessageRange messageRange = 1; optional SyncActionValue.SyncActionMessageRange messageRange = 1;
} }
@@ -4907,6 +5261,7 @@ message SyncActionValue {
DRAFTED = 8; DRAFTED = 8;
AI_HANDOFF = 9; AI_HANDOFF = 9;
CHANNELS = 10; CHANNELS = 10;
AI_RESPONDING = 11;
} }
} }
@@ -5080,6 +5435,7 @@ message SyncActionValue {
repeated string keywords = 3; repeated string keywords = 3;
optional int32 count = 4; optional int32 count = 4;
optional bool deleted = 5; optional bool deleted = 5;
repeated string associatedLabelIds = 6;
} }
message RecentEmojiWeightsAction { message RecentEmojiWeightsAction {
@@ -5122,6 +5478,8 @@ message SyncActionValue {
optional bool isStatusNotificationEnabled = 29; optional bool isStatusNotificationEnabled = 29;
optional int32 statusNotificationToneId = 30; optional int32 statusNotificationToneId = 30;
optional bool shouldPlaySoundForCallNotification = 31; optional bool shouldPlaySoundForCallNotification = 31;
optional string chatThemeId = 32;
optional string colorSchemeId = 33;
enum DisplayMode { enum DisplayMode {
DISPLAY_MODE_UNKNOWN = 0; DISPLAY_MODE_UNKNOWN = 0;
ALWAYS = 1; ALWAYS = 1;
@@ -5166,6 +5524,8 @@ message SyncActionValue {
IS_STATUS_NOTIFICATION_ENABLED = 29; IS_STATUS_NOTIFICATION_ENABLED = 29;
STATUS_NOTIFICATION_TONE_ID = 30; STATUS_NOTIFICATION_TONE_ID = 30;
SHOULD_PLAY_SOUND_FOR_CALL_NOTIFICATION = 31; SHOULD_PLAY_SOUND_FOR_CALL_NOTIFICATION = 31;
CHAT_THEME_ID = 32;
COLOR_SCHEME_ID = 33;
} }
enum SettingPlatform { enum SettingPlatform {
PLATFORM_UNKNOWN = 0; PLATFORM_UNKNOWN = 0;
@@ -5189,11 +5549,22 @@ message SyncActionValue {
repeated string userJid = 2; repeated string userJid = 2;
optional bool shareToFB = 3; optional bool shareToFB = 3;
optional bool shareToIG = 4; optional bool shareToIG = 4;
repeated CustomList customLists = 5;
repeated StatusDistributionMode modes = 6;
message CustomList {
optional string listId = 1;
optional string name = 2;
optional string emoji = 3;
optional bool isSelected = 4;
repeated string userJid = 5;
}
enum StatusDistributionMode { enum StatusDistributionMode {
ALLOW_LIST = 0; ALLOW_LIST = 0;
DENY_LIST = 1; DENY_LIST = 1;
CONTACTS = 2; CONTACTS = 2;
CLOSE_FRIENDS = 3; CLOSE_FRIENDS = 3;
CUSTOM_LIST = 4;
} }
} }
@@ -5219,6 +5590,29 @@ message SyncActionValue {
optional int64 expirationDate = 3; optional int64 expirationDate = 3;
} }
message SubscriptionsSyncV2Action {
repeated SubscriptionInfo subscriptions = 1;
repeated PaidFeature paidFeature = 2;
message PaidFeature {
optional string name = 1;
optional bool enabled = 2;
optional int32 limit = 3;
optional int64 expirationTime = 4;
}
message SubscriptionInfo {
optional string id = 1;
optional int32 tier = 2;
optional string status = 3;
optional int64 startTime = 4;
optional int64 endTime = 5;
optional bool isPlatformChanged = 6;
optional string source = 7;
optional int64 creationTime = 8;
}
}
message SyncActionMessage { message SyncActionMessage {
optional MessageKey key = 1; optional MessageKey key = 1;
optional int64 timestamp = 2; optional int64 timestamp = 2;
@@ -5230,6 +5624,10 @@ message SyncActionValue {
repeated SyncActionValue.SyncActionMessage messages = 3; repeated SyncActionValue.SyncActionMessage messages = 3;
} }
message ThreadPinAction {
optional bool pinned = 1;
}
message TimeFormatAction { message TimeFormatAction {
optional bool isTwentyFourHourFormatEnabled = 1; optional bool isTwentyFourHourFormatEnabled = 1;
} }
@@ -5351,7 +5749,6 @@ message TemplateButton {
message ThreadID { message ThreadID {
optional ThreadType threadType = 1; optional ThreadType threadType = 1;
optional MessageKey threadKey = 2; optional MessageKey threadKey = 2;
optional string sourceChatJid = 3;
enum ThreadType { enum ThreadType {
UNKNOWN = 0; UNKNOWN = 0;
VIEW_REPLIES = 1; VIEW_REPLIES = 1;
@@ -5554,6 +5951,10 @@ message WebMessageInfo {
optional QuarantinedMessage quarantinedMessage = 77; optional QuarantinedMessage quarantinedMessage = 77;
optional uint32 nonJidMentions = 78; optional uint32 nonJidMentions = 78;
optional string hsmTag = 79; optional string hsmTag = 79;
optional uint64 ephemeralExpirationTimestamp = 80;
optional ScheduledMessageMetadata scheduledMessageMetadata = 81;
optional string decisionId = 82;
repeated string decisionSources = 83;
enum BizPrivacyStatus { enum BizPrivacyStatus {
E2EE = 0; E2EE = 0;
FB = 2; FB = 2;
@@ -5793,6 +6194,8 @@ message WebMessageInfo {
GROUP_MEMBER_SHARE_GROUP_HISTORY_MODE = 221; GROUP_MEMBER_SHARE_GROUP_HISTORY_MODE = 221;
GROUP_OPEN_BOT_ADDED = 222; GROUP_OPEN_BOT_ADDED = 222;
GROUP_TEE_BOT_ADDED = 223; GROUP_TEE_BOT_ADDED = 223;
CONTACT_INFO = 224;
SCHEDULED_MESSAGE_CREATED = 225;
} }
} }
+988 -21
View File
File diff suppressed because it is too large Load Diff
+7136 -49
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -47,7 +47,7 @@
"fflate": "^0.8.2", "fflate": "^0.8.2",
"libsignal": "github:whiskeysockets/libsignal-node", "libsignal": "github:whiskeysockets/libsignal-node",
"lru-cache": "^11.2.6", "lru-cache": "^11.2.6",
"music-metadata": "^11.12.0", "music-metadata": "^11.12.3",
"p-queue": "^9.0.0", "p-queue": "^9.0.0",
"pino": "^10.3.1", "pino": "^10.3.1",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
+1 -1
View File
@@ -1 +1 @@
{"version": [2, 3000, 1034427372]} {"version": [2, 3000, 1039067946]}
+263 -101
View File
@@ -1,3 +1,4 @@
import { randomBytes } from 'crypto'
import NodeCache from '@cacheable/node-cache' import NodeCache from '@cacheable/node-cache'
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import { proto } from '../../WAProto/index.js' import { proto } from '../../WAProto/index.js'
@@ -507,6 +508,38 @@ export const makeMessagesSocket = (config: SocketConfig) => {
return didFetchNewSession return didFetchNewSession
} }
const canonicalizeCarouselRecipients = async (recipientJids: string[]): Promise<string[]> => {
if (!recipientJids.length) {
return recipientJids
}
const pnRecipients = [...new Set(recipientJids.filter(jid => isAnyPnUser(jid)))]
if (!pnRecipients.length) {
return recipientJids
}
const mappings = (await signalRepository.lidMapping.getLIDsForPNs(pnRecipients)) || []
if (!mappings.length) {
logger.debug({ recipientCount: recipientJids.length }, '[CAROUSEL] No PN→LID recipient mappings found')
return recipientJids
}
const pnToLid = new Map(mappings.map(item => [item.pn, item.lid]))
const mapped = recipientJids.map(jid => pnToLid.get(jid) || jid)
const changed = mapped.filter((jid, index) => jid !== recipientJids[index]).length
logger.info(
{
recipientCount: recipientJids.length,
pnRecipients: pnRecipients.length,
mappedRecipients: changed
},
'[CAROUSEL] Canonicalized recipient addressing to LID before session assert/encrypt'
)
return mapped
}
const sendPeerDataOperationMessage = async ( const sendPeerDataOperationMessage = async (
pdoMessage: proto.Message.IPeerDataOperationRequestMessage pdoMessage: proto.Message.IPeerDataOperationRequestMessage
): Promise<string> => { ): Promise<string> => {
@@ -541,6 +574,82 @@ export const makeMessagesSocket = (config: SocketConfig) => {
return msgId return msgId
} }
const resolveDsmMessageForRecipient = (
jid: string,
patchedMessage: proto.IMessage,
dsmMessage: proto.IMessage | undefined,
meId: string,
meLid: string | undefined,
meLidUser: string | null | undefined
) => {
if (!dsmMessage) {
return patchedMessage
}
const { user: targetUser } = jidDecode(jid)!
const { user: ownPnUser } = jidDecode(meId)!
const isOwnUser = targetUser === ownPnUser || (meLidUser && targetUser === meLidUser)
const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
if (isOwnUser && !isExactSenderDevice) {
logger.debug({ jid, targetUser }, 'Using DSM for own device')
return dsmMessage
}
return patchedMessage
}
const encryptPatchedMessageForRecipient = async ({
jid,
patchedMessage,
dsmMessage,
meId,
meLid,
meLidUser,
extraAttrs,
onPkmsg
}: {
jid: string
patchedMessage: proto.IMessage
dsmMessage: proto.IMessage | undefined
meId: string
meLid: string | undefined
meLidUser: string | null | undefined
extraAttrs: BinaryNode['attrs'] | undefined
onPkmsg: () => void
}) => {
if (!jid) return null
try {
const msgToEncrypt = resolveDsmMessageForRecipient(jid, patchedMessage, dsmMessage, meId, meLid, meLidUser)
const bytes = encodeWAMessage(msgToEncrypt)
const mutexKey = jid
return await encryptionMutex.mutex(mutexKey, async () => {
const { type, ciphertext } = await signalRepository.encryptMessage({ jid, data: bytes })
if (type === 'pkmsg') {
onPkmsg()
}
return {
tag: 'to',
attrs: { jid },
content: [
{
tag: 'enc',
attrs: { v: '2', type, ...(extraAttrs || {}) },
content: ciphertext
}
]
} as BinaryNode
})
} catch (err) {
logger.error({ jid, err }, 'Failed to encrypt for recipient')
return null
}
}
const createParticipantNodes = async ( const createParticipantNodes = async (
recipientJids: string[], recipientJids: string[],
message: proto.IMessage, message: proto.IMessage,
@@ -560,58 +669,23 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const meId = authState.creds.me?.id const meId = authState.creds.me?.id
if (!meId) throw new Boom('Not authenticated', { statusCode: 401 }) if (!meId) throw new Boom('Not authenticated', { statusCode: 401 })
const meLid = authState.creds.me?.lid const meLid = authState.creds.me?.lid
const meLidUser = meLid ? jidDecode(meLid)?.user : null const meLidUser = meLid ? (jidDecode(meLid)?.user ?? null) : null
const onPkmsg = () => {
shouldIncludeDeviceIdentity = true
}
const encryptionPromises = (patchedMessages as { recipientJid: string; message: proto.IMessage }[]).map( const encryptionPromises = (patchedMessages as { recipientJid: string; message: proto.IMessage }[]).map(
async ({ recipientJid: jid, message: patchedMessage }: { recipientJid: string; message: proto.IMessage }) => { ({ recipientJid: jid, message: patchedMessage }: { recipientJid: string; message: proto.IMessage }) =>
try { encryptPatchedMessageForRecipient({
if (!jid) return null jid,
patchedMessage,
let msgToEncrypt = patchedMessage dsmMessage,
meId,
if (dsmMessage) { meLid,
const { user: targetUser } = jidDecode(jid)! meLidUser,
const { user: ownPnUser } = jidDecode(meId)! extraAttrs,
const ownLidUser = meLidUser onPkmsg
})
const isOwnUser = targetUser === ownPnUser || (ownLidUser && targetUser === ownLidUser)
const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
if (isOwnUser && !isExactSenderDevice) {
msgToEncrypt = dsmMessage
logger.debug({ jid, targetUser }, 'Using DSM for own device')
}
}
const bytes = encodeWAMessage(msgToEncrypt)
const mutexKey = jid
const node = await encryptionMutex.mutex(mutexKey, async () => {
const { type, ciphertext } = await signalRepository.encryptMessage({ jid, data: bytes })
if (type === 'pkmsg') {
shouldIncludeDeviceIdentity = true
}
return {
tag: 'to',
attrs: { jid },
content: [
{
tag: 'enc',
attrs: { v: '2', type, ...(extraAttrs || {}) },
content: ciphertext
}
]
}
})
return node
} catch (err) {
logger.error({ jid, err }, 'Failed to encrypt for recipient')
return null
}
}
) )
const nodes = (await Promise.all(encryptionPromises)).filter(node => node !== null) as BinaryNode[] const nodes = (await Promise.all(encryptionPromises)).filter(node => node !== null) as BinaryNode[]
@@ -1166,21 +1240,30 @@ export const makeMessagesSocket = (config: SocketConfig) => {
allRecipients.push(jid) allRecipients.push(jid)
} }
await assertSessions(allRecipients) const isCarouselFanout = isCarouselMessage(message)
const effectiveMeRecipients = isCarouselFanout
? await canonicalizeCarouselRecipients(meRecipients)
: meRecipients
const effectiveOtherRecipients = isCarouselFanout
? await canonicalizeCarouselRecipients(otherRecipients)
: otherRecipients
const effectiveAllRecipients = [...effectiveMeRecipients, ...effectiveOtherRecipients]
await assertSessions(effectiveAllRecipients)
const [ const [
{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: meNodes, shouldIncludeDeviceIdentity: s1 },
{ nodes: otherNodes, shouldIncludeDeviceIdentity: s2 } { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }
] = await Promise.all([ ] = await Promise.all([
// For own devices: use DSM (deviceSentMessage) wrapper // For own devices: use DSM (deviceSentMessage) wrapper
createParticipantNodes(meRecipients, meMsg || message, extraAttrs), createParticipantNodes(effectiveMeRecipients, meMsg || message, extraAttrs),
createParticipantNodes(otherRecipients, message, extraAttrs) createParticipantNodes(effectiveOtherRecipients, message, extraAttrs)
]) ])
participants.push(...meNodes) participants.push(...meNodes)
participants.push(...otherNodes) participants.push(...otherNodes)
if (meRecipients.length > 0 || otherRecipients.length > 0) { if (effectiveMeRecipients.length > 0 || effectiveOtherRecipients.length > 0) {
extraAttrs['phash'] = generateParticipantHashV2([...meRecipients, ...otherRecipients]) extraAttrs['phash'] = generateParticipantHashV2(effectiveAllRecipients)
} }
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2 shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
@@ -1253,13 +1336,15 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const isCarousel = isCarouselMessage(message) const isCarousel = isCarouselMessage(message)
// Collect biz/bot nodes to append AFTER device-identity and tctoken // Collect biz/bot nodes to append AFTER device-identity and tctoken
// Stanza order: participants → device-identity → tctoken → biz // Stanza order: participants → device-identity → tctoken → biz (when applicable)
// The biz node MUST be last for WhatsApp Web carousel rendering // CDP capture confirms Pastorini INCLUDES biz node for carousel (native_flow v=9, name=mixed)
const deferredNodes: BinaryNode[] = [] const deferredNodes: BinaryNode[] = []
// Inject biz node for interactive messages (including carousel) // Inject biz node for interactive messages (including carousel — CDP evidence from Pastorini)
if (buttonType && enableInteractiveMessages) { if ((buttonType || isCarousel) && enableInteractiveMessages) {
const startTime = Date.now() const startTime = Date.now()
// When entering via isCarousel, buttonType may be undefined — default to 'native_flow'
const effectiveButtonType = buttonType || 'native_flow'
// Debug: Log message structure to diagnose list detection // Debug: Log message structure to diagnose list detection
const interactiveMsg = const interactiveMsg =
@@ -1297,7 +1382,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
) )
// Track that we're sending an interactive message // Track that we're sending an interactive message
metrics.interactiveMessagesSent.inc({ type: buttonType }) metrics.interactiveMessagesSent.inc({ type: effectiveButtonType })
try { try {
// Classify button types for native_flow name and bot node decisions // Classify button types for native_flow name and bot node decisions
@@ -1340,27 +1425,49 @@ export const makeMessagesSocket = (config: SocketConfig) => {
) )
const interactiveType = 'native_flow' const interactiveType = 'native_flow'
const bizContent: BinaryNode[] = [
{
tag: 'interactive',
attrs: {
type: interactiveType,
v: '1'
},
content: [
{
tag: interactiveType,
attrs: {
v: '9',
name: nativeFlowName
}
}
]
}
]
// CDP capture shows Pastorini includes quality_control inside biz for carousel
if (isCarousel) {
const decisionId = randomBytes(20).toString('hex')
bizContent.push({
tag: 'quality_control',
attrs: {
decision_id: decisionId
},
content: [
{
tag: 'decision_source',
attrs: {
value: 'df'
}
}
]
})
logger.info({ msgId, decisionId }, '[BIZ NODE] Added quality_control for carousel')
}
deferredNodes.push({ deferredNodes.push({
tag: 'biz', tag: 'biz',
attrs: {}, attrs: {},
content: [ content: bizContent
{
tag: 'interactive',
attrs: {
type: interactiveType,
v: '1'
},
content: [
{
tag: interactiveType,
attrs: {
v: '9',
name: nativeFlowName
}
}
]
}
]
}) })
} }
@@ -1386,11 +1493,11 @@ export const makeMessagesSocket = (config: SocketConfig) => {
} }
// Track success and latency after message is sent // Track success and latency after message is sent
metrics.interactiveMessagesSuccess.inc({ type: buttonType }) metrics.interactiveMessagesSuccess.inc({ type: effectiveButtonType })
metrics.interactiveMessagesLatency.observe({ type: buttonType }, Date.now() - startTime) metrics.interactiveMessagesLatency.observe({ type: effectiveButtonType }, Date.now() - startTime)
} catch (error) { } catch (error) {
logger.error({ error, msgId, buttonType }, '[BIZ NODE] Failed to inject biz node') logger.error({ error, msgId, buttonType: effectiveButtonType }, '[BIZ NODE] Failed to inject biz node')
metrics.interactiveMessagesFailures.inc({ type: buttonType, reason: 'injection_failed' }) metrics.interactiveMessagesFailures.inc({ type: effectiveButtonType, reason: 'injection_failed' })
} }
} else if (buttonType && !enableInteractiveMessages) { } else if (buttonType && !enableInteractiveMessages) {
logger.warn( logger.warn(
@@ -1417,7 +1524,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
stanza.attrs.to = destinationJid stanza.attrs.to = destinationJid
} }
if (shouldIncludeDeviceIdentity) { // Always include device-identity for carousel (Pastorini stanza always has it)
if (shouldIncludeDeviceIdentity || isCarousel) {
;(stanza.content as BinaryNode[]).push({ ;(stanza.content as BinaryNode[]).push({
tag: 'device-identity', tag: 'device-identity',
attrs: {}, attrs: {},
@@ -1474,27 +1582,79 @@ export const makeMessagesSocket = (config: SocketConfig) => {
} }
} }
// If tctoken is missing for a 1:1 send, fire-and-forget fetch so the // If tctoken is missing for a 1:1 send, fetch it
// retry path (error 463 → handleBadAck) can pick it up on resend // CDP capture confirms Pastorini stanza includes tctoken for carousel
if (!tcTokenBuffer?.length && is1on1Send && !tcTokenFetchingJids.has(tcTokenJid)) { if (!tcTokenBuffer?.length && is1on1Send && !tcTokenFetchingJids.has(tcTokenJid)) {
tcTokenFetchingJids.add(tcTokenJid) tcTokenFetchingJids.add(tcTokenJid)
logTcToken('fetch', { jid: destinationJid }) logTcToken('fetch', { jid: destinationJid })
getPrivacyTokens([destinationJid])
.then(async fetchResult => { if (isCarousel) {
// BLOCKING fetch for carousel — tctoken is required (Pastorini stanza has it)
try {
const fetchResult = await getPrivacyTokens([destinationJid])
// Direct extraction from IQ result — bypass store/read key mismatch
const tokensNode = getBinaryNodeChild(fetchResult, 'tokens')
if (tokensNode) {
const tokenNodes = getBinaryNodeChildren(tokensNode, 'token')
for (const tokenNode of tokenNodes) {
if (tokenNode.attrs.type === 'trusted_contact' && tokenNode.content instanceof Uint8Array) {
tcTokenBuffer = Buffer.from(tokenNode.content)
logger.info(
{
jid: destinationJid,
tokenLen: tcTokenBuffer.length,
tokenJid: tokenNode.attrs.jid,
timestamp: tokenNode.attrs.t
},
'[CAROUSEL] tctoken extracted directly from IQ result'
)
break
}
}
}
if (!tcTokenBuffer?.length) {
// Debug: dump the IQ result structure
const childTags = Array.isArray(fetchResult.content)
? (fetchResult.content as BinaryNode[]).map(n => `${n.tag}(${JSON.stringify(n.attrs)})`)
: []
logger.warn(
{ jid: destinationJid, resultTag: fetchResult.tag, resultAttrs: fetchResult.attrs, childTags },
'[CAROUSEL] tctoken fetch completed but NO valid token in IQ result'
)
}
// Also store for future use
await storeTcTokensFromIqResult({ await storeTcTokensFromIqResult({
result: fetchResult, result: fetchResult,
fallbackJid: destinationJid, fallbackJid: destinationJid,
keys: authState.keys, keys: authState.keys,
getLIDForPN getLIDForPN
// onNewJidStored not passed — the pruning index lives in messages-recv (higher layer) }).catch(() => {})
}) } catch (err: any) {
}) logger.warn({ jid: destinationJid, err: err?.message }, '[CAROUSEL] Blocking tctoken fetch failed')
.catch(err => { } finally {
logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken fetch failed')
})
.finally(() => {
tcTokenFetchingJids.delete(tcTokenJid) tcTokenFetchingJids.delete(tcTokenJid)
}) }
} else {
// Fire-and-forget for non-carousel
getPrivacyTokens([destinationJid])
.then(async fetchResult => {
await storeTcTokensFromIqResult({
result: fetchResult,
fallbackJid: destinationJid,
keys: authState.keys,
getLIDForPN
})
})
.catch(err => {
logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken fetch failed')
})
.finally(() => {
tcTokenFetchingJids.delete(tcTokenJid)
})
}
} }
if (tcTokenBuffer?.length) { if (tcTokenBuffer?.length) {
@@ -1525,8 +1685,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
logger.debug({ msgId }, `sending message to ${participants.length} devices`) logger.debug({ msgId }, `sending message to ${participants.length} devices`)
// ======= PROTOBUF ROUNDTRIP TEST: Verify encoding preserves carousel ======= // ======= PROTOBUF ROUNDTRIP TEST: Verify encoding preserves carousel =======
// Only runs at debug level to avoid performance overhead in production if (isCarousel) {
if (isCarousel && logger.level === 'debug') {
try { try {
const encoded = proto.Message.encode(message).finish() const encoded = proto.Message.encode(message).finish()
const decoded = proto.Message.decode(encoded) const decoded = proto.Message.decode(encoded)
@@ -1535,15 +1694,19 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const card0 = decodedInteractive?.carouselMessage?.cards?.[0] const card0 = decodedInteractive?.carouselMessage?.cards?.[0]
const card0Header = card0?.header const card0Header = card0?.header
logger.debug( logger.info(
{ {
msgId, msgId,
encodedSize: encoded.length, encodedSize: encoded.length,
messageKeys: Object.keys(message),
hasInteractive: !!message.interactiveMessage,
hasViewOnce: !!message.viewOnceMessage,
hasCarouselAfterDecode: !!decodedInteractive?.carouselMessage, hasCarouselAfterDecode: !!decodedInteractive?.carouselMessage,
cardsCount, cardsCount,
card0Title: card0Header?.title, card0Title: card0Header?.title,
card0HasImage: !!card0Header?.imageMessage, card0HasImage: !!card0Header?.imageMessage,
card0Buttons: card0?.nativeFlowMessage?.buttons?.length || 0 card0Buttons: card0?.nativeFlowMessage?.buttons?.length || 0,
messageVersion: decodedInteractive?.carouselMessage?.messageVersion
}, },
'[ROUNDTRIP] Protobuf encode→decode verification' '[ROUNDTRIP] Protobuf encode→decode verification'
) )
@@ -1553,8 +1716,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
} }
// ======= PROTOCOL INTERCEPTOR: Dump complete stanza for debugging ======= // ======= PROTOCOL INTERCEPTOR: Dump complete stanza for debugging =======
// Only runs at debug level to avoid logging sensitive content in production if (buttonType || isCarousel) {
if ((buttonType || isCarousel) && logger.level === 'debug') {
const dumpBinaryNode = (node: BinaryNode, indent = 0): string => { const dumpBinaryNode = (node: BinaryNode, indent = 0): string => {
if (!node) return '' if (!node) return ''
const pad = ' '.repeat(indent) const pad = ' '.repeat(indent)
+15 -4
View File
@@ -107,20 +107,31 @@ export const makeSocket = (config: SocketConfig) => {
const enableUnifiedSession = const enableUnifiedSession =
enableUnifiedSessionConfig !== undefined ? enableUnifiedSessionConfig : shouldEnableUnifiedSession() enableUnifiedSessionConfig !== undefined ? enableUnifiedSessionConfig : shouldEnableUnifiedSession()
// Initialize circuit breakers if enabled // Initialize circuit breakers if enabled.
//
// Env var override: BAILEYS_DISABLE_CIRCUIT_BREAKER=true completely disables
// all three circuit breakers without requiring a code change. Useful when the
// query CB is opening on slow `init queries` after a fresh QR pairing — those
// initial USync / device-list / app-state queries can legitimately take longer
// than the per-call timeout when the auth state is fresh and the server has
// no warm caches for this client.
const envDisableCB = process.env.BAILEYS_DISABLE_CIRCUIT_BREAKER === 'true'
let queryCircuitBreaker: CircuitBreaker | undefined let queryCircuitBreaker: CircuitBreaker | undefined
let connectionCircuitBreaker: CircuitBreaker | undefined let connectionCircuitBreaker: CircuitBreaker | undefined
let preKeyCircuitBreaker: CircuitBreaker | undefined let preKeyCircuitBreaker: CircuitBreaker | undefined
if (enableCircuitBreaker) { if (enableCircuitBreaker && !envDisableCB) {
// Circuit breaker for query operations (most critical) // Circuit breaker for query operations (most critical).
// timeout=120s gives `init queries` (USync, device-list, app-state-sync)
// enough room on fresh QR pairings; the per-query timeout in waitForMessage
// already enforces a tighter bound for individual operations.
queryCircuitBreaker = createConnectionCircuitBreaker({ queryCircuitBreaker = createConnectionCircuitBreaker({
name: 'socket-query', name: 'socket-query',
failureThreshold: 5, failureThreshold: 5,
failureWindow: 60000, failureWindow: 60000,
resetTimeout: 30000, resetTimeout: 30000,
successThreshold: 2, successThreshold: 2,
timeout: defaultQueryTimeoutMs || 60000, timeout: Math.max(defaultQueryTimeoutMs || 0, 120_000),
onStateChange: (from, to) => { onStateChange: (from, to) => {
logger.info({ from, to }, 'Query circuit breaker state changed') logger.info({ from, to }, 'Query circuit breaker state changed')
}, },
+3 -1
View File
@@ -54,12 +54,14 @@ export type WAMediaUpload = Buffer | WAMediaPayloadStream | WAMediaPayloadURL
* Individual sticker in a sticker pack * Individual sticker in a sticker pack
*/ */
export type Sticker = { export type Sticker = {
/** Buffer, Stream or URL of the sticker image (will be converted to WebP if needed) */ /** Buffer, Stream or URL of the sticker image (WebP or Lottie/WAS format) */
data: WAMediaUpload data: WAMediaUpload
/** Array of emojis associated with this sticker (max 3 recommended per WhatsApp standards) */ /** Array of emojis associated with this sticker (max 3 recommended per WhatsApp standards) */
emojis?: string[] emojis?: string[]
/** Accessibility label for screen readers (max 125 chars for static, 255 for animated) */ /** Accessibility label for screen readers (max 125 chars for static, 255 for animated) */
accessibilityLabel?: string accessibilityLabel?: string
/** Force Lottie format detection (auto-detected if omitted) */
isLottie?: boolean
} }
/** /**
+1 -1
View File
@@ -160,7 +160,7 @@ export const extractImageThumb = async (bufferOrFilePath: Readable | Buffer | st
height: dimensions.height height: dimensions.height
} }
} }
} else if ('jimp' in lib && typeof lib.jimp?.Jimp === 'object') { } else if ('jimp' in lib && typeof lib.jimp?.Jimp === 'function') {
const jimp = await (lib.jimp.Jimp as any).read(bufferOrFilePath) const jimp = await (lib.jimp.Jimp as any).read(bufferOrFilePath)
const dimensions = { const dimensions = {
width: jimp.width, width: jimp.width,
+62 -1
View File
@@ -41,9 +41,11 @@ import type { ILogger } from './logger'
import { import {
downloadContentFromMessage, downloadContentFromMessage,
encryptedStream, encryptedStream,
extractImageThumb,
generateThumbnail, generateThumbnail,
getAudioDuration, getAudioDuration,
getAudioWaveform, getAudioWaveform,
getStream,
getRawMediaUploadData, getRawMediaUploadData,
type MediaDownloadOptions type MediaDownloadOptions
} from './messages-media' } from './messages-media'
@@ -633,6 +635,59 @@ export const generateCarouselMessage = async (
) )
} }
const recoverCarouselImageMetadata = async (
imageMessage: proto.Message.IImageMessage | null | undefined,
cardImage: WAMediaUpload,
cardTitle: string | undefined,
uploadOptions: MessageContentGenerationOptions
) => {
if (!imageMessage) {
return
}
const missingThumbnail = !imageMessage.jpegThumbnail
const missingWidth = !imageMessage.width
const missingHeight = !imageMessage.height
if (!missingThumbnail && !missingWidth && !missingHeight) {
return
}
try {
const { stream } = await getStream(cardImage, uploadOptions.options)
const { buffer, original } = await extractImageThumb(stream)
if (missingThumbnail) {
imageMessage.jpegThumbnail = buffer.toString('base64')
}
if (missingWidth && original.width) {
imageMessage.width = original.width
}
if (missingHeight && original.height) {
imageMessage.height = original.height
}
uploadOptions.logger?.info(
{
cardTitle,
recoveredThumbnail: !!imageMessage.jpegThumbnail,
width: imageMessage.width,
height: imageMessage.height
},
'[CAROUSEL] Recovered image metadata from source media'
)
} catch (error) {
uploadOptions.logger?.warn(
{
cardTitle,
trace: error instanceof Error ? error.stack : String(error)
},
'[CAROUSEL] Failed source-media thumbnail fallback'
)
}
}
// Map cards to the carousel format (processing media) // Map cards to the carousel format (processing media)
const carouselCards = await Promise.all( const carouselCards = await Promise.all(
cards.map(async card => { cards.map(async card => {
@@ -648,6 +703,11 @@ export const generateCarouselMessage = async (
if (hasMedia && mediaOptions) { if (hasMedia && mediaOptions) {
if (card.image) { if (card.image) {
const { imageMessage } = await prepareWAMessageMedia({ image: card.image }, mediaOptions) const { imageMessage } = await prepareWAMessageMedia({ image: card.image }, mediaOptions)
// Mirror the working Pastorini-style result: every carousel image card should
// carry a jpegThumbnail and dimensions before it reaches the Web live renderer.
await recoverCarouselImageMetadata(imageMessage, card.image, card.title, mediaOptions)
// Validate image fields needed for WhatsApp rendering // Validate image fields needed for WhatsApp rendering
if (imageMessage && !imageMessage.jpegThumbnail) { if (imageMessage && !imageMessage.jpegThumbnail) {
mediaOptions.logger?.warn( mediaOptions.logger?.warn(
@@ -1239,7 +1299,8 @@ export const generateWAMessageContent = async (
} }
// Pass options for media processing if cards have images/videos // Pass options for media processing if cards have images/videos
const generated = await generateCarouselMessage(carouselOptions, options) const generated = await generateCarouselMessage(carouselOptions, options)
// Direct interactiveMessage — no viewOnceMessage wrapper, no root header/body/footer // Frida capture shows interactiveMessage DIRECT (field 45) in DSM — no viewOnceMessage wrapper
// Testing without wrapper: biz node + quality_control already match Pastorini CDP stanza
m.interactiveMessage = generated.interactiveMessage m.interactiveMessage = generated.interactiveMessage
return m return m
} }
+120 -19
View File
@@ -2,6 +2,7 @@ import { Boom } from '@hapi/boom'
import { createHash } from 'crypto' import { createHash } from 'crypto'
import { zipSync } from 'fflate' import { zipSync } from 'fflate'
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import { gzipSync, gunzipSync } from 'zlib'
import { proto } from '../../WAProto/index.js' import { proto } from '../../WAProto/index.js'
import type { MediaType } from '../Defaults/index.js' import type { MediaType } from '../Defaults/index.js'
import type { StickerPack, WAMediaUpload, WAMediaUploadFunction } from '../Types/Message.js' import type { StickerPack, WAMediaUpload, WAMediaUploadFunction } from '../Types/Message.js'
@@ -101,6 +102,49 @@ export const isAnimatedWebP = (buffer: Buffer): boolean => {
return false return false
} }
/**
* Detecta se um buffer é Lottie JSON (raw ou gzip-compressed/WAS)
*
* WABA usa mimetype `application/was` para stickers Lottie.
* WAS (WhatsApp Animated Sticker) = gzip-compressed Lottie JSON.
*
* Detecção:
* - Gzip (0x1f 0x8b): descomprime e verifica se é Lottie JSON
* - JSON bruto: verifica campos Lottie obrigatórios (v, ip, op, layers)
*
* @param buffer - Buffer to check
* @returns true if buffer is Lottie/WAS format
*/
export const isLottieBuffer = (buffer: Buffer): boolean => {
if (buffer.length < 2) return false
let jsonBuffer: Buffer
// Check if gzip-compressed (WAS format)
if (buffer[0] === 0x1f && buffer[1] === 0x8b) {
try {
// SECURITY: Limit decompressed output to 50MB to prevent decompression bombs
jsonBuffer = gunzipSync(buffer, { maxOutputLength: 50 * 1024 * 1024 }) as Buffer
} catch {
return false
}
} else if (buffer[0] === 0x7b) {
// Starts with '{' - could be raw Lottie JSON
jsonBuffer = buffer
} else {
return false
}
// Validate Lottie JSON structure: must have v, ip, op, AND layers
try {
const str = jsonBuffer.toString('utf8', 0, Math.min(jsonBuffer.length, 4096))
// Quick check for ALL required Lottie fields
return str.includes('"v"') && str.includes('"layers"') && str.includes('"ip"') && str.includes('"op"')
} catch {
return false
}
}
/** /**
* Converte uma imagem para WebP usando Sharp * Converte uma imagem para WebP usando Sharp
* Preserva o buffer original se for WebP para manter EXIF e animações * Preserva o buffer original se for WebP para manter EXIF e animações
@@ -114,12 +158,25 @@ export const isAnimatedWebP = (buffer: Buffer): boolean => {
const convertToWebP = async ( const convertToWebP = async (
buffer: Buffer, buffer: Buffer,
logger?: ILogger logger?: ILogger
): Promise<{ webpBuffer: Buffer; isAnimated: boolean }> => { ): Promise<{ webpBuffer: Buffer; isAnimated: boolean; isLottie: boolean }> => {
// Lottie/WAS: ensure gzip-compressed format (WAS = gzip Lottie JSON)
if (isLottieBuffer(buffer)) {
let wasBuffer = buffer
// Raw Lottie JSON (starts with '{') must be gzipped to produce valid WAS
if (buffer[0] === 0x7b) {
logger?.trace('Raw Lottie JSON detected, gzip-compressing to WAS format')
wasBuffer = gzipSync(buffer) as Buffer
}
logger?.trace('Input is Lottie/WAS format')
return { webpBuffer: wasBuffer, isAnimated: true, isLottie: true }
}
// Se já é WebP, preserva o buffer original (mantém EXIF e animações) // Se já é WebP, preserva o buffer original (mantém EXIF e animações)
if (isWebPBuffer(buffer)) { if (isWebPBuffer(buffer)) {
const isAnimated = isAnimatedWebP(buffer) const isAnimated = isAnimatedWebP(buffer)
logger?.trace({ isAnimated }, 'Input is already WebP, preserving original buffer') logger?.trace({ isAnimated }, 'Input is already WebP, preserving original buffer')
return { webpBuffer: buffer, isAnimated } return { webpBuffer: buffer, isAnimated, isLottie: false }
} }
// Tenta usar Sharp para converter // Tenta usar Sharp para converter
@@ -135,7 +192,7 @@ const convertToWebP = async (
logger?.trace('Converting image to WebP using Sharp') logger?.trace('Converting image to WebP using Sharp')
const webpBuffer = await lib.sharp.default(buffer).webp().toBuffer() const webpBuffer = await lib.sharp.default(buffer).webp().toBuffer()
return { webpBuffer, isAnimated: false } return { webpBuffer, isAnimated: false, isLottie: false }
} }
/** /**
@@ -338,9 +395,11 @@ export type PrepareStickerPackMessageOptions = {
* *
* **Especificações WhatsApp:** * **Especificações WhatsApp:**
* - 3-30 stickers por pack (oficial) * - 3-30 stickers por pack (oficial)
* - WebP obrigatório * - WebP ou Lottie/WAS (application/was)
* - Stickers: 512x512 pixels (auto-resize)
* - Recomendado: 100KB por sticker estático, 500KB animado * - Recomendado: 100KB por sticker estático, 500KB animado
* - Tray icon: 252x252 pixels * - Tray icon: 96x96 pixels (PNG no ZIP)
* - Thumbnail: 252x252 pixels (JPEG, upload separado)
* *
* @param stickerPack - Sticker pack data with stickers, cover, name, publisher * @param stickerPack - Sticker pack data with stickers, cover, name, publisher
* @param options - Upload function and optional logger * @param options - Upload function and optional logger
@@ -464,9 +523,36 @@ export const prepareStickerPackMessage = async (
// Obtém buffer do sticker // Obtém buffer do sticker
const buffer = await mediaToBuffer(sticker.data, `sticker ${i + 1}`) const buffer = await mediaToBuffer(sticker.data, `sticker ${i + 1}`)
// Converte para WebP // Detecta formato e converte se necessário
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let { webpBuffer, isAnimated } = await convertToWebP(buffer, logger) let { webpBuffer, isAnimated, isLottie } = await convertToWebP(buffer, logger)
// Validate explicit isLottie flag — must match detected format
if (sticker.isLottie !== undefined && sticker.isLottie !== isLottie) {
throw new Boom(
`Sticker ${i + 1}: explicit isLottie=${sticker.isLottie} does not match detected format (detected=${isLottie})`,
{ statusCode: 400 }
)
}
// WABA: Enforce 512x512 dimensions for WebP stickers (Lottie is vector, skip)
if (!isLottie && isWebPBuffer(webpBuffer)) {
const lib = await getImageProcessingLibrary()
if (lib?.sharp) {
const metadata = await lib.sharp.default(webpBuffer).metadata()
if (metadata.width !== 512 || metadata.height !== 512) {
logger?.trace(
{ index: i, width: metadata.width, height: metadata.height },
'Resizing sticker to 512x512 (WABA standard)'
)
webpBuffer = await lib.sharp
.default(webpBuffer)
.resize(512, 512, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
.webp()
.toBuffer()
}
}
}
// ENHANCEMENT: Auto-compression if exceeds 1MB (try quality 70, then 50) // ENHANCEMENT: Auto-compression if exceeds 1MB (try quality 70, then 50)
const MAX_STICKER_SIZE = 1024 * 1024 // 1MB const MAX_STICKER_SIZE = 1024 * 1024 // 1MB
@@ -483,7 +569,7 @@ export const prepareStickerPackMessage = async (
if (lib?.sharp) { if (lib?.sharp) {
// Try quality 70 // Try quality 70
try { try {
const compressed70 = await lib.sharp.default(buffer).webp({ quality: 70 }).toBuffer() const compressed70 = await lib.sharp.default(webpBuffer).webp({ quality: 70 }).toBuffer()
// eslint-disable-next-line max-depth // eslint-disable-next-line max-depth
if (compressed70.length <= MAX_STICKER_SIZE) { if (compressed70.length <= MAX_STICKER_SIZE) {
@@ -498,7 +584,7 @@ export const prepareStickerPackMessage = async (
) )
} else { } else {
// Try quality 50 // Try quality 50
const compressed50 = await lib.sharp.default(buffer).webp({ quality: 50 }).toBuffer() const compressed50 = await lib.sharp.default(webpBuffer).webp({ quality: 50 }).toBuffer()
// eslint-disable-next-line max-depth // eslint-disable-next-line max-depth
if (compressed50.length <= MAX_STICKER_SIZE) { if (compressed50.length <= MAX_STICKER_SIZE) {
@@ -547,12 +633,13 @@ export const prepareStickerPackMessage = async (
) )
} }
// Gera nome do arquivo: hash.webp (deduplicação automática) // Gera nome do arquivo: hash.webp ou hash.was (WABA: plain_file_hash.ext)
const sha256Hash = generateSha256Hash(webpBuffer) const sha256Hash = generateSha256Hash(webpBuffer)
const fileName = `${sha256Hash}.webp` const extension = isLottie ? 'was' : 'webp'
const fileName = `${sha256Hash}.${extension}`
logger?.trace( logger?.trace(
{ index: i, fileName, sizeKB: finalSizeKB.toFixed(2), isAnimated }, { index: i, fileName, sizeKB: finalSizeKB.toFixed(2), isAnimated, isLottie },
'Sticker processed successfully' 'Sticker processed successfully'
) )
@@ -560,6 +647,7 @@ export const prepareStickerPackMessage = async (
fileName, fileName,
webpBuffer, webpBuffer,
isAnimated, isAnimated,
isLottie,
emojis: sticker.emojis || [], emojis: sticker.emojis || [],
accessibilityLabel: sticker.accessibilityLabel accessibilityLabel: sticker.accessibilityLabel
} }
@@ -578,7 +666,7 @@ export const prepareStickerPackMessage = async (
for (const result of processedStickers) { for (const result of processedStickers) {
if (!result) continue if (!result) continue
const { fileName, webpBuffer, isAnimated, emojis, accessibilityLabel } = result const { fileName, webpBuffer, isAnimated, isLottie, emojis, accessibilityLabel } = result
// SECURITY FIX #7: Check if this hash already exists (duplicate sticker) // SECURITY FIX #7: Check if this hash already exists (duplicate sticker)
const existingMetadata = metadataByHash.get(fileName) const existingMetadata = metadataByHash.get(fileName)
@@ -605,13 +693,14 @@ export const prepareStickerPackMessage = async (
// New sticker - add to ZIP and create metadata // New sticker - add to ZIP and create metadata
stickerData[fileName] = [new Uint8Array(webpBuffer), { level: 0 as 0 }] stickerData[fileName] = [new Uint8Array(webpBuffer), { level: 0 as 0 }]
// WABA: mimetype é 'application/was' para Lottie, 'image/webp' para WebP
const metadata: proto.Message.StickerPackMessage.ISticker = { const metadata: proto.Message.StickerPackMessage.ISticker = {
fileName, fileName,
isAnimated, isAnimated,
emojis, emojis,
accessibilityLabel, accessibilityLabel,
isLottie: false, isLottie,
mimetype: 'image/webp' mimetype: isLottie ? 'application/was' : 'image/webp'
} }
metadataByHash.set(fileName, metadata) metadataByHash.set(fileName, metadata)
@@ -636,10 +725,22 @@ export const prepareStickerPackMessage = async (
logger?.trace('Processing cover image') logger?.trace('Processing cover image')
coverBuffer = await mediaToBuffer(cover, 'cover image') coverBuffer = await mediaToBuffer(cover, 'cover image')
// Converte cover para WebP e adiciona ao ZIP // Tray icon uses PNG format, 96x96 pixels (official client standard)
const result = await convertToWebP(coverBuffer, logger) const lib = await getImageProcessingLibrary()
coverWebP = result.webpBuffer if (!lib?.sharp) {
coverFileName = `${stickerPackId}.webp` throw new Boom(
'Sharp library is required for cover/tray icon processing. Install with: yarn add sharp',
{ statusCode: 400 }
)
}
coverWebP = await lib.sharp
.default(coverBuffer)
.resize(96, 96, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
.png()
.toBuffer()
coverFileName = `${stickerPackId}.png`
stickerData[coverFileName] = [new Uint8Array(coverWebP), { level: 0 as 0 }] stickerData[coverFileName] = [new Uint8Array(coverWebP), { level: 0 as 0 }]
} catch (error) { } catch (error) {
throw new Boom(`Failed to process cover image: ${(error as Error).message}`, { throw new Boom(`Failed to process cover image: ${(error as Error).message}`, {
+20 -12
View File
@@ -154,18 +154,26 @@ export async function storeTcTokensFromIqResult({
continue continue
} }
await keys.set({ const tokenEntry = {
tctoken: { ...existingEntry,
[storageJid]: { token: Buffer.from(tokenNode.content),
...existingEntry, timestamp: tokenNode.attrs.t,
token: Buffer.from(tokenNode.content), // WABA Android: resets real_issue_timestamp to null when storing a new token
timestamp: tokenNode.attrs.t, // (UPDATE wa_trusted_contacts_send SET real_issue_timestamp=null)
// WABA Android: resets real_issue_timestamp to null when storing a new token realIssueTimestamp: null
// (UPDATE wa_trusted_contacts_send SET real_issue_timestamp=null) }
realIssueTimestamp: null
} // Store under resolved storageJid AND under fallbackJid (PN) for reliable lookup
} // The read path may resolve to a different LID than the store path
}) const normalizedFallback = jidNormalizedUser(fallbackJid)
const keysToStore: Record<string, typeof tokenEntry | null> = {
[storageJid]: tokenEntry
}
if (normalizedFallback !== storageJid) {
keysToStore[normalizedFallback] = tokenEntry
}
await keys.set({ tctoken: keysToStore })
onNewJidStored?.(storageJid) onNewJidStored?.(storageJid)
} }
} }
+17 -42
View File
@@ -14,24 +14,19 @@ import { encodeBigEndian } from './generics'
import { createSignalIdentity } from './signal' import { createSignalIdentity } from './signal'
const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => { const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
// Always use MACOS/Desktop identity for UserAgent — we connect via web
// protocol (WA\x06\x03) so the server expects a web-like UserAgent.
// Using SMB_ANDROID here causes pair code registration to fail with
// "não é possível conectar" even though the phone shows the confirmation.
// Android identity is only set in DeviceProps (registration node) which
// determines the display name in "Linked Devices".
return { return {
appVersion: { appVersion: {
primary: config.version[0], primary: config.version[0],
secondary: config.version[1], secondary: config.version[1],
tertiary: config.version[2] tertiary: config.version[2]
}, },
platform: proto.ClientPayload.UserAgent.Platform.MACOS, platform: proto.ClientPayload.UserAgent.Platform.WEB,
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE, releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
osVersion: '0.1', osVersion: '0.1',
device: 'Desktop', device: 'Desktop',
osBuildNumber: '0.1', osBuildNumber: '0.1',
localeLanguageIso6391: 'en', localeLanguageIso6391: 'en',
mnc: '000', mnc: '000',
mcc: '000', mcc: '000',
localeCountryIso31661Alpha2: config.countryCode localeCountryIso31661Alpha2: config.countryCode
@@ -60,20 +55,16 @@ const getClientPayload = (config: SocketConfig) => {
const payload: proto.IClientPayload = { const payload: proto.IClientPayload = {
connectType: proto.ClientPayload.ConnectType.WIFI_UNKNOWN, connectType: proto.ClientPayload.ConnectType.WIFI_UNKNOWN,
connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED, connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED,
userAgent: getUserAgent(config), userAgent: getUserAgent(config)
webInfo: getWebInfo(config)
} }
payload.webInfo = getWebInfo(config)
return payload return payload
} }
export const generateLoginNode = (userJid: string, config: SocketConfig): proto.IClientPayload => { export const generateLoginNode = (userJid: string, config: SocketConfig): proto.IClientPayload => {
const decoded = jidDecode(userJid) const { user, device } = jidDecode(userJid)!
if (!decoded) {
throw new Boom('Invalid user JID', { statusCode: 400 })
}
const { user, device } = decoded
const payload: proto.IClientPayload = { const payload: proto.IClientPayload = {
...getClientPayload(config), ...getClientPayload(config),
passive: true, passive: true,
@@ -88,10 +79,6 @@ export const generateLoginNode = (userJid: string, config: SocketConfig): proto.
const getPlatformType = (platform: string): proto.DeviceProps.PlatformType => { const getPlatformType = (platform: string): proto.DeviceProps.PlatformType => {
const platformType = platform.toUpperCase() const platformType = platform.toUpperCase()
if (platformType === 'ANDROID') {
return proto.DeviceProps.PlatformType.ANDROID_PHONE
}
return ( return (
proto.DeviceProps.PlatformType[platformType as keyof typeof proto.DeviceProps.PlatformType] || proto.DeviceProps.PlatformType[platformType as keyof typeof proto.DeviceProps.PlatformType] ||
proto.DeviceProps.PlatformType.CHROME proto.DeviceProps.PlatformType.CHROME
@@ -166,9 +153,6 @@ export const configureSuccessfulPairing = (
}: Pick<AuthenticationCreds, 'advSecretKey' | 'signedIdentityKey' | 'signalIdentities'> }: Pick<AuthenticationCreds, 'advSecretKey' | 'signedIdentityKey' | 'signalIdentities'>
) => { ) => {
const msgId = stanza.attrs.id const msgId = stanza.attrs.id
if (!msgId) {
throw new Boom('Missing message ID', { statusCode: 400 })
}
const pairSuccessNode = getBinaryNodeChild(stanza, 'pair-success') const pairSuccessNode = getBinaryNodeChild(stanza, 'pair-success')
@@ -184,51 +168,42 @@ export const configureSuccessfulPairing = (
const bizName = businessNode?.attrs.name const bizName = businessNode?.attrs.name
const jid = deviceNode.attrs.jid const jid = deviceNode.attrs.jid
const lid = deviceNode.attrs.lid const lid = deviceNode.attrs.lid
if (!jid || !lid) {
throw new Boom('Missing JID or LID in device node', { statusCode: 400 })
}
const { details, hmac, accountType } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityNode.content as Buffer) const { details, hmac, accountType } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityNode.content as Buffer)
if (!details || !hmac) {
throw new Boom('Missing ADV signature fields', { statusCode: 400 })
}
let hmacPrefix = Buffer.from([]) let hmacPrefix = Buffer.from([])
if (accountType !== undefined && accountType === proto.ADVEncryptionType.HOSTED) { if (accountType !== undefined && accountType === proto.ADVEncryptionType.HOSTED) {
hmacPrefix = WA_ADV_HOSTED_ACCOUNT_SIG_PREFIX hmacPrefix = WA_ADV_HOSTED_ACCOUNT_SIG_PREFIX
} }
const advSign = hmacSign(Buffer.concat([hmacPrefix, details]), Buffer.from(advSecretKey, 'base64')) const advSign = hmacSign(Buffer.concat([hmacPrefix, details!]), Buffer.from(advSecretKey, 'base64'))
if (Buffer.compare(hmac, advSign) !== 0) { if (Buffer.compare(hmac!, advSign) !== 0) {
throw new Boom('Invalid account signature') throw new Boom('Invalid account signature')
} }
const account = proto.ADVSignedDeviceIdentity.decode(details) const account = proto.ADVSignedDeviceIdentity.decode(details!)
const { accountSignatureKey, accountSignature, details: deviceDetails } = account const { accountSignatureKey, accountSignature, details: deviceDetails } = account
if (!accountSignatureKey || !accountSignature || !deviceDetails) {
throw new Boom('Missing ADV account fields', { statusCode: 400 })
}
const deviceIdentity = proto.ADVDeviceIdentity.decode(deviceDetails) const deviceIdentity = proto.ADVDeviceIdentity.decode(deviceDetails!)
const accountSignaturePrefix = const accountSignaturePrefix =
deviceIdentity.deviceType === proto.ADVEncryptionType.HOSTED deviceIdentity.deviceType === proto.ADVEncryptionType.HOSTED
? WA_ADV_HOSTED_ACCOUNT_SIG_PREFIX ? WA_ADV_HOSTED_ACCOUNT_SIG_PREFIX
: WA_ADV_ACCOUNT_SIG_PREFIX : WA_ADV_ACCOUNT_SIG_PREFIX
const accountMsg = Buffer.concat([accountSignaturePrefix, deviceDetails, signedIdentityKey.public]) const accountMsg = Buffer.concat([accountSignaturePrefix, deviceDetails!, signedIdentityKey.public])
if (!Curve.verify(accountSignatureKey, accountMsg, accountSignature)) { if (!Curve.verify(accountSignatureKey!, accountMsg, accountSignature!)) {
throw new Boom('Failed to verify account signature') throw new Boom('Failed to verify account signature')
} }
const deviceMsg = Buffer.concat([ const deviceMsg = Buffer.concat([
WA_ADV_DEVICE_SIG_PREFIX, WA_ADV_DEVICE_SIG_PREFIX,
deviceDetails, deviceDetails!,
signedIdentityKey.public, signedIdentityKey.public,
accountSignatureKey accountSignatureKey!
]) ])
account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg) account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg)
const identity = createSignalIdentity(lid, accountSignatureKey) const identity = createSignalIdentity(lid!, accountSignatureKey!)
const accountEnc = encodeSignedDeviceIdentity(account, false) const accountEnc = encodeSignedDeviceIdentity(account, false)
const reply: BinaryNode = { const reply: BinaryNode = {
@@ -236,7 +211,7 @@ export const configureSuccessfulPairing = (
attrs: { attrs: {
to: S_WHATSAPP_NET, to: S_WHATSAPP_NET,
type: 'result', type: 'result',
id: msgId id: msgId!
}, },
content: [ content: [
{ {
@@ -255,7 +230,7 @@ export const configureSuccessfulPairing = (
const authUpdate: Partial<AuthenticationCreds> = { const authUpdate: Partial<AuthenticationCreds> = {
account, account,
me: { id: jid, name: bizName, lid }, me: { id: jid!, name: bizName, lid },
signalIdentities: [...(signalIdentities || []), identity], signalIdentities: [...(signalIdentities || []), identity],
platform: platformNode?.attrs.name platform: platformNode?.attrs.name
} }
+18 -11
View File
@@ -388,6 +388,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@borewit/text-codec@npm:^0.2.2":
version: 0.2.2
resolution: "@borewit/text-codec@npm:0.2.2"
checksum: 10c0/2d3fb132bc6a132914a8fbf8e9ff2fa1ead210ecc395b28bb7355bd7719548a5e351ffe39f21c3bee8048f6cabd99eabd404bb5cc809cad9cba25abed19d271f
languageName: node
linkType: hard
"@cacheable/node-cache@npm:^2.0.1": "@cacheable/node-cache@npm:^2.0.1":
version: 2.0.1 version: 2.0.1
resolution: "@cacheable/node-cache@npm:2.0.1" resolution: "@cacheable/node-cache@npm:2.0.1"
@@ -3105,7 +3112,7 @@ __metadata:
libsignal: "github:whiskeysockets/libsignal-node" libsignal: "github:whiskeysockets/libsignal-node"
link-preview-js: "npm:^4.0.0" link-preview-js: "npm:^4.0.0"
lru-cache: "npm:^11.2.6" lru-cache: "npm:^11.2.6"
music-metadata: "npm:^11.12.0" music-metadata: "npm:^11.12.3"
open: "npm:^11.0.0" open: "npm:^11.0.0"
p-queue: "npm:^9.0.0" p-queue: "npm:^9.0.0"
pino: "npm:^10.3.1" pino: "npm:^10.3.1"
@@ -4556,15 +4563,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"file-type@npm:^21.3.0": "file-type@npm:^21.3.1":
version: 21.3.0 version: 21.3.1
resolution: "file-type@npm:21.3.0" resolution: "file-type@npm:21.3.1"
dependencies: dependencies:
"@tokenizer/inflate": "npm:^0.4.1" "@tokenizer/inflate": "npm:^0.4.1"
strtok3: "npm:^10.3.4" strtok3: "npm:^10.3.4"
token-types: "npm:^6.1.1" token-types: "npm:^6.1.1"
uint8array-extras: "npm:^1.4.0" uint8array-extras: "npm:^1.4.0"
checksum: 10c0/1b1fa909e6063044a6da1d2ea348ee4d747ed9286382d3f0d4d6532c11fb2ea9f2e7e67b2bc7d745d1bc937e05dee1aa8cb912c64250933bcb393a3744f4e284 checksum: 10c0/66a8eda781c803c6fc372464abba88cee564cfe30f029716f06909da1564bc51d0a4e8d34654b881dc751cdb0513338b3c0747e06a608cee0dd5c5499b018d47
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6504,21 +6511,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"music-metadata@npm:^11.12.0": "music-metadata@npm:^11.12.3":
version: 11.12.0 version: 11.12.3
resolution: "music-metadata@npm:11.12.0" resolution: "music-metadata@npm:11.12.3"
dependencies: dependencies:
"@borewit/text-codec": "npm:^0.2.1" "@borewit/text-codec": "npm:^0.2.2"
"@tokenizer/token": "npm:^0.3.0" "@tokenizer/token": "npm:^0.3.0"
content-type: "npm:^1.0.5" content-type: "npm:^1.0.5"
debug: "npm:^4.4.3" debug: "npm:^4.4.3"
file-type: "npm:^21.3.0" file-type: "npm:^21.3.1"
media-typer: "npm:^1.1.0" media-typer: "npm:^1.1.0"
strtok3: "npm:^10.3.4" strtok3: "npm:^10.3.4"
token-types: "npm:^6.1.2" token-types: "npm:^6.1.2"
uint8array-extras: "npm:^1.5.0" uint8array-extras: "npm:^1.5.0"
win-guid: "npm:^0.2.1" win-guid: "npm:^0.2.1"
checksum: 10c0/014a120b8941ab80452171def0ec3cba013041ba4b94f8061ad493a037d3423e6d316131a13fb0433404685ebd3c8e3ae2255c1bafaeb20673fcd73af2df0148 checksum: 10c0/57df905f51ec792e5b4c994645eab64d47d5608b9de11151f54b26fbd5f563598ec04d8aed7f244ef1479030d78ddab8af1ee84dfcee40a93b3241941b50999d
languageName: node languageName: node
linkType: hard linkType: hard