Compare commits

..

11 Commits

Author SHA1 Message Date
Renato Alcara ba1c1b0fdb fix: align Bad MAC retry receipt with WA Desktop behavior
Three issues corrected in sendRetryRequest (receiver side):

1. BUG — error code hardcoded as '0' (UnknownError) in retry receipt.
   The peer (especially another InfiniteAPI instance) could not detect
   the failure type and would fall back to the 1-hour session recreation
   timeout instead of recreating immediately.
   Fix: derive error code from the actual libsignal decryption error
   message stored in messageStubParameters[0].

2. BUG — shouldRecreateSession block was reading the wrong node: it
   called getBinaryNodeChild(node, 'retry') on the incoming bad-MAC
   message, which never has a <retry> child. errorCode was always
   undefined, so MAC_ERROR_CODES never matched — the logic was dead.

3. BUG (consequence of #2) — when shouldRecreateSession accidentally
   did fire (no-session or 1-hour timeout), it deleted the receiver's
   session BEFORE the sender's pkmsg arrived. This opened a race window
   where concurrent messages would fail with "No Session".
   Fix: remove the entire session-deletion block from sendRetryRequest.
   The Signal Protocol pkmsg automatically overwrites the corrupted
   session — no explicit delete needed on the receiver side.

WA Desktop reference (CDP capture 2026-03-19):
- Retry receipt sent ~99ms after Bad MAC with specific error code
- pkmsg arrives ~304ms later, new session established automatically
- Old session is never deleted; pkmsg overwrites it implicitly
- Full recovery in ~1.3s without any race window

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 23:10:59 -03:00
Renato Alcara 876af2e96c fix: HistorySync LID improvements — raw_id mapping, prekey pool strategy, keepalive jitter, CDN cleanup
fix: HistorySync LID improvements — raw_id mapping, prekey pool strategy, keepalive jitter, CDN cleanup
2026-03-19 18:24:20 -03:00
Renato Alcara 5bb18da6bf fix: address PR #305 review comments — Origin header, lowServerCount threshold, keepalive guard
- history.ts: add Origin: DEFAULT_ORIGIN to CDN DELETE request headers —
  the download path injects it internally but options does not carry it
- socket.ts: fix lowServerCount comparison — was preKeyCount <= topUpAmount
  which triggered uploads even when above MIN_PREKEY_COUNT (e.g. 250 prekeys
  → topUp=550 → 250<=550=true → unnecessary upload). Now uses correct
  threshold: preKeyCount < MIN_PREKEY_COUNT
- socket.ts: guard scheduleNextKeepAlive() with !closed check to prevent
  orphan timers when end() was called concurrently on a different path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:23:41 -03:00
Renato Alcara 52d8ddae32 fix: prevent double prekey upload when concurrent uploads race on count=0
uploadPreKeys() waited for a concurrent upload but then proceeded to upload
again. With top-up logic and count=0, both handleEncryptNotification and
uploadPreKeysToServerIfRequired fire simultaneously, both see count=0, first
wins and uploads 800, second waits then uploads another 800 → 1600 prekeys.

Fix: return immediately after awaiting the concurrent upload — it already
replenished the pool.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:23:41 -03:00
Renato Alcara 8c8c5a312e fix: address PR review comments — CDN DELETE timing, keepalive leak, spread order
- history.ts: move CDN DELETE from downloadHistory() to
  downloadAndProcessHistorySyncNotification(), after processHistoryMessage()
  succeeds — prevents permanent history loss if processing throws post-download
- history.ts: fix fetch spread order: { ...options, method: 'DELETE' } so
  options.method cannot shadow the intended DELETE verb
- socket.ts: add early return after void end() in onKeepAliveTick to prevent
  scheduleNextKeepAlive() from leaking a timer while connection is closing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:23:28 -03:00
Renato Alcara 52756fb50d fix: histsync LID improvements — raw_id mapping, prekeys, keepalive jitter, CDN DELETE
1. USyncQuery: extract raw_id attr from node attrs into rawId field; implement
   side_list parsing (was TODO/commented) reusing shared parseNodeList helper

2. getUSyncDevices: add 4th LID→PN source via raw_id pairing from device-list
   WA Business sends zero phoneNumberToLidMappings in HistorySync — raw_id is
   the only way to resolve LID↔PN for those accounts during message send

3. MIN_PREKEY_COUNT: 5 → 25 (WA Business maintains ~812; 5 was too low a buffer
   before triggering replenishment upload)

4. startKeepAliveRequest: replace fixed setInterval with recursive setTimeout
   + ±15% jitter, matching WA Desktop's ~25-30s variable heartbeat pattern;
   clearInterval → clearTimeout for the handle

5. downloadHistory: fire-and-forget DELETE to CDN after successful download,
   mirroring WA Desktop behaviour (server-side one-time file cleanup)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:23:27 -03:00
Renato Alcara bd8465d9b8 fix: histsync LID improvements — raw_id mapping, prekeys, keepalive j… (#304)
* fix: histsync LID improvements — raw_id mapping, prekeys, keepalive jitter, CDN DELETE

* fix: prekey pool strategy — 800 initial, top-up to 800 when below 200

- INITIAL_PREKEY_COUNT: 812 → 800 (rounded, matches WA Business ~812 from CDP capture)
- MIN_PREKEY_COUNT: 25 → 200 (replenishment trigger threshold)
- uploadPreKeysToServerIfRequired: top-up to INITIAL_PREKEY_COUNT instead of
  uploading a flat MIN_PREKEY_COUNT — restores full 800-key pool on each replenish
- handleEncryptNotification: same top-up logic (INITIAL_PREKEY_COUNT - count)
  so server notification path also restores to 800, not just adds 200

uploadPreKeys(5) in error recovery path intentionally left unchanged.
2026-03-19 17:58:30 -03:00
Renato Alcara 6c52b01ea7 fix: add pastParticipants to HistorySync with LID normalization and deduplication (#303)
Two bugs fixed compared to prior implementation:

1. history.ts — normalize userJid LID→PN
   pastParticipants[].userJid may arrive as a LID identifier (e.g. "46802258641027@lid").
   The consumer expects a phone number. After building the lidPnMap from conversations
   and phoneNumberToLidMappings, each userJid is resolved to its PN equivalent.
   If the mapping is not available the original value is preserved (no data loss).

2. event-buffer.ts — deduplicate by groupJid + userJid across chunks
   Multiple HistorySync chunks can contain the same group. The previous approach
   concatenated blindly, producing duplicate entries. Now a keyed map
   { [groupJid]: IPastParticipant[] } is used so each group appears once and
   each participant within a group appears at most once.

Types updated (Events.ts):
   - messaging-history.set event includes pastParticipants?: IPastParticipants[]
   - BufferedEventData.historySets.pastParticipants typed as keyed map
2026-03-19 16:44:14 -03:00
Renato Alcara a9e926b907 fix: correct w:mex QueryIds and response fields for newsletter operations (#302)
Reverse-engineered from WA Web JS bundle and live CDP interception.
All QueryIds and XWAPaths were wrong; mute/unmute also had wrong variables structure.

- FOLLOW QueryId: 7871414976211147 → 24404358912487870
- UNFOLLOW QueryId: 7238632346214362 → 9767147403369991
- MUTE QueryId: 29766401636284406 → 31938993655691868
- UNMUTE QueryId: 9864994326891137 → 31938993655691868 (same mutation as MUTE)
- xwa2_newsletter_follow: 'xwa2_newsletter_follow' → 'xwa2_newsletter_join_v2'
- xwa2_newsletter_unfollow: 'xwa2_newsletter_unfollow' → 'xwa2_newsletter_leave_v2'
- xwa2_newsletter_mute_v2: 'xwa2_newsletter_mute_v2' → 'xwa2_newsletter_update_user_setting'
- xwa2_newsletter_unmute_v2: 'xwa2_newsletter_unmute_v2' → 'xwa2_newsletter_update_user_setting'
- newsletterMute/Unmute variables: flat {newsletter_id} → {input: {newsletter_id, type, value}}

Fixes #2346
2026-03-19 11:49:42 -03:00
Renato Alcara 71fe69fd21 chore: update WhatsApp Web version to v2.3000.1035504971 (#301)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-19 06:33:42 -03:00
github-actions[bot] eecf6df2a2 chore: update proto/version to v2.3000.1035484955 (#300)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-19 01:48:35 -03:00
14 changed files with 216 additions and 5458 deletions
+3 -265
View File
@@ -1,7 +1,7 @@
syntax = "proto3"; syntax = "proto3";
package proto; package proto;
/// WhatsApp Version: 2.3000.1039067946 /// WhatsApp Version: 2.3000.1035484955
message ADVDeviceIdentity { message ADVDeviceIdentity {
optional uint32 rawId = 1; optional uint32 rawId = 1;
@@ -14,7 +14,6 @@ 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;
@@ -62,7 +61,6 @@ message AIHomeState {
ANIMATE_PHOTO = 2; ANIMATE_PHOTO = 2;
ANALYZE_FILE = 3; ANALYZE_FILE = 3;
COLLABORATE = 4; COLLABORATE = 4;
OPEN_GREETING_CARD = 5;
} }
} }
@@ -249,16 +247,6 @@ 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;
@@ -433,12 +421,6 @@ message BotCapabilityMetadata {
} }
} }
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 {
@@ -670,9 +652,6 @@ 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;
} }
@@ -724,7 +703,6 @@ enum BotMetricsEntryPoint {
WEB_NAVIGATION_BAR = 47; WEB_NAVIGATION_BAR = 47;
GROUP_MEMBER = 54; GROUP_MEMBER = 54;
CHATLIST_SEARCH = 55; CHATLIST_SEARCH = 55;
NEW_CHAT_LIST = 56;
} }
message BotMetricsMetadata { message BotMetricsMetadata {
optional string destinationId = 1; optional string destinationId = 1;
@@ -911,11 +889,6 @@ 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;
@@ -1151,7 +1124,6 @@ message ClientPayload {
optional int32 preacksCount = 45; optional int32 preacksCount = 45;
optional int32 processingQueueSize = 46; optional int32 processingQueueSize = 46;
repeated string pairedPeripherals = 47; repeated string pairedPeripherals = 47;
optional bytes testIsolationId = 48;
enum AccountType { enum AccountType {
DEFAULT = 0; DEFAULT = 0;
GUEST = 1; GUEST = 1;
@@ -1247,7 +1219,6 @@ 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;
@@ -1263,12 +1234,6 @@ 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;
@@ -1438,9 +1403,6 @@ 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;
@@ -1453,47 +1415,10 @@ message ContextInfo {
} }
} }
message BusinessInteractionPills {
optional string businessJid = 1;
repeated Pill pills = 2;
optional EntryPoint entryPoint = 3;
enum EntryPoint {
ENTRY_POINT_UNKNOWN = 0;
P2P_LINK_SHARE = 1;
CONTACT_CARD_SHARING = 2;
PHONE_NUMBER = 3;
STATUS = 4;
IN_THREAD_CONTEXT_CARD = 5;
}
message Pill {
optional ContextInfo.BusinessInteractionPills.PillType pillType = 1;
optional string actionUrl = 2;
}
enum PillType {
UNKNOWN = 0;
VIEW_BUSINESS = 1;
CHAT = 2;
CALL = 3;
CATALOG = 4;
CHANNEL = 5;
BOOK_APPOINTMENT = 6;
OFFERS = 7;
BESTSELLERS = 8;
MENU = 9;
ABOUT = 10;
}
}
message BusinessMessageForwardInfo { 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;
@@ -1542,10 +1467,6 @@ message ContextInfo {
optional string wtwaWebsiteUrl = 26; optional string wtwaWebsiteUrl = 26;
optional string adPreviewUrl = 27; optional string adPreviewUrl = 27;
optional bool containsCtwaFlowsAutoReply = 28; 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;
@@ -1702,23 +1623,12 @@ message Conversation {
optional bool isMarketingMessageThread = 55; optional bool isMarketingMessageThread = 55;
optional bool isSenderNewAccount = 56; optional bool isSenderNewAccount = 56;
optional uint32 afterReadDuration = 57; 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 {
@@ -1742,7 +1652,6 @@ message DeviceCapabilities {
optional bool companionSupportEnabled = 2; optional bool companionSupportEnabled = 2;
optional bool campaignSyncEnabled = 3; optional bool campaignSyncEnabled = 3;
optional bool insightsSyncEnabled = 4; optional bool insightsSyncEnabled = 4;
optional int32 recipientLimit = 5;
} }
enum ChatLockSupportLevel { enum ChatLockSupportLevel {
@@ -1818,8 +1727,6 @@ message DeviceProps {
optional uint32 initialSyncMaxMessagesPerChat = 20; optional uint32 initialSyncMaxMessagesPerChat = 20;
optional bool supportManusHistory = 21; optional bool supportManusHistory = 21;
optional bool supportHatchHistory = 22; optional bool supportHatchHistory = 22;
repeated string supportedBotChannelFbids = 23;
optional bool supportInlineContacts = 24;
} }
enum PlatformType { enum PlatformType {
@@ -2006,16 +1913,6 @@ 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;
@@ -2038,7 +1935,6 @@ message HandshakeMessage {
optional bool sendServerHelloPaddedBytes = 7; optional bool sendServerHelloPaddedBytes = 7;
optional bool simulateXxkemFs = 8; optional bool simulateXxkemFs = 8;
optional HandshakeMessage.HandshakePqMode pqMode = 9; optional HandshakeMessage.HandshakePqMode pqMode = 9;
optional bytes extendedEphemeral = 10;
} }
enum HandshakePqMode { enum HandshakePqMode {
@@ -2058,7 +1954,6 @@ message HandshakeMessage {
optional bytes payload = 3; optional bytes payload = 3;
optional bytes extendedStatic = 4; optional bytes extendedStatic = 4;
optional bytes paddingBytes = 5; optional bytes paddingBytes = 5;
optional bytes extendedCiphertext = 6;
} }
} }
@@ -2082,8 +1977,6 @@ 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;
@@ -2178,14 +2071,6 @@ 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;
@@ -2264,6 +2149,7 @@ 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;
} }
} }
@@ -2425,9 +2311,6 @@ message Message {
optional ConditionalRevealMessage conditionalRevealMessage = 120; optional ConditionalRevealMessage conditionalRevealMessage = 120;
optional PollAddOptionMessage pollAddOptionMessage = 121; optional PollAddOptionMessage pollAddOptionMessage = 121;
optional EventInviteMessage eventInviteMessage = 122; 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;
@@ -2608,41 +2491,6 @@ 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;
@@ -2654,7 +2502,6 @@ 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;
@@ -2752,7 +2599,6 @@ message Message {
optional int64 startTime = 5; optional int64 startTime = 5;
optional string caption = 6; optional string caption = 6;
optional bool isCanceled = 7; optional bool isCanceled = 7;
optional int64 endTime = 8;
} }
message EventMessage { message EventMessage {
@@ -3335,35 +3181,6 @@ message Message {
} }
} }
message P2PPaymentReminderNotification {
optional string reminderId = 1;
optional Money amount = 2;
optional ReminderFrequency frequency = 3;
optional int64 nextReminderTimestamp = 4;
optional int64 expiryTimestamp = 5;
optional ReminderState state = 6;
optional string description = 7;
optional string creatorJid = 8;
optional string receiverJid = 9;
optional string upiId = 10;
optional int64 createdTimestamp = 11;
enum ReminderFrequency {
UNKNOWN_FREQUENCY = 0;
WEEKLY = 1;
BIWEEKLY = 2;
MONTHLY = 3;
CUSTOM = 4;
}
enum ReminderState {
UNKNOWN_STATE = 0;
ACTIVE = 1;
PAUSED = 2;
STOPPED = 3;
EXPIRED = 4;
CANCELLED = 5;
}
}
message PaymentExtendedMetadata { message PaymentExtendedMetadata {
optional uint32 type = 1; optional uint32 type = 1;
optional string platform = 2; optional string platform = 2;
@@ -3466,7 +3283,6 @@ 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 {
@@ -3553,7 +3369,6 @@ 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;
@@ -3773,7 +3588,6 @@ 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;
@@ -3803,7 +3617,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; BOT_UNLINK_MESSAGE = 33;
} }
} }
@@ -3895,26 +3709,6 @@ 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;
@@ -3974,7 +3768,6 @@ 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 {
@@ -4195,7 +3988,6 @@ 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;
@@ -4388,8 +4180,6 @@ enum MutationProps {
CUSTOMER_DATA_ACTION = 83; CUSTOMER_DATA_ACTION = 83;
SUBSCRIPTIONS_SYNC_V2_ACTION = 84; SUBSCRIPTIONS_SYNC_V2_ACTION = 84;
THREAD_PIN_ACTION = 85; 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;
@@ -4703,12 +4493,6 @@ 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;
@@ -4865,7 +4649,6 @@ message StatusAttribution {
SHARECHAT = 9; SHARECHAT = 9;
GOOGLE_PHOTOS = 10; GOOGLE_PHOTOS = 10;
SOUNDCLOUD = 11; SOUNDCLOUD = 11;
SHAZAM = 12;
} }
} }
@@ -4923,7 +4706,6 @@ message StatusAttribution {
LAYOUTS = 8; LAYOUTS = 8;
NEWSLETTER_STATUS = 9; NEWSLETTER_STATUS = 9;
STATUS_CLOSE_SHARING = 10; STATUS_CLOSE_SHARING = 10;
PAID_PARTNERSHIP = 11;
} }
} }
@@ -5037,8 +4819,6 @@ message SyncActionValue {
optional CustomerDataAction customerDataAction = 83; optional CustomerDataAction customerDataAction = 83;
optional SubscriptionsSyncV2Action subscriptionsSyncV2Action = 84; optional SubscriptionsSyncV2Action subscriptionsSyncV2Action = 84;
optional ThreadPinAction threadPinAction = 85; 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;
@@ -5058,10 +4838,6 @@ 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;
@@ -5072,20 +4848,6 @@ 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;
} }
@@ -5131,7 +4893,6 @@ 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 {
@@ -5261,7 +5022,6 @@ message SyncActionValue {
DRAFTED = 8; DRAFTED = 8;
AI_HANDOFF = 9; AI_HANDOFF = 9;
CHANNELS = 10; CHANNELS = 10;
AI_RESPONDING = 11;
} }
} }
@@ -5435,7 +5195,6 @@ 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 {
@@ -5478,8 +5237,6 @@ 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;
@@ -5524,8 +5281,6 @@ 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;
@@ -5549,22 +5304,11 @@ 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;
} }
} }
@@ -5951,10 +5695,6 @@ 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;
@@ -6194,8 +5934,6 @@ 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;
} }
} }
+14 -632
View File
File diff suppressed because it is too large Load Diff
+13 -4474
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
{"version": [2, 3000, 1039067946]} {"version":[2,3000,1035504971]}
+5 -3
View File
@@ -188,10 +188,12 @@ export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[]
/** 120s timeout for history sync stall detection, same as WA Web's handleChunkProgress / restartPausedTimer (g = 120) */ /** 120s timeout for history sync stall detection, same as WA Web's handleChunkProgress / restartPausedTimer (g = 120) */
export const HISTORY_SYNC_PAUSED_TIMEOUT_MS = 120_000 export const HISTORY_SYNC_PAUSED_TIMEOUT_MS = 120_000
export const MIN_PREKEY_COUNT = 5 // Replenishment threshold: when server count drops below this, top-up back to INITIAL_PREKEY_COUNT
export const MIN_PREKEY_COUNT = 200
// Moderate prekey count (upstream uses 812, reduced to balance rate limiting and availability) // Initial pool size matching WA Business (CDP IDB capture: prekey-store = 812 on registration)
export const INITIAL_PREKEY_COUNT = 200 // Rounded to 800 for cleanliness; replenishment always tops up to this value
export const INITIAL_PREKEY_COUNT = 800
export const UPLOAD_TIMEOUT = 30000 // 30 seconds export const UPLOAD_TIMEOUT = 30000 // 30 seconds
// Moderate upload interval to balance rate limiting and responsiveness (was 5000) // Moderate upload interval to balance rate limiting and responsiveness (was 5000)
+29 -40
View File
@@ -7,6 +7,7 @@ import { proto } from '../../WAProto/index.js'
import { import {
DEFAULT_CACHE_TTLS, DEFAULT_CACHE_TTLS,
DEFAULT_SESSION_CLEANUP_CONFIG, DEFAULT_SESSION_CLEANUP_CONFIG,
INITIAL_PREKEY_COUNT,
KEY_BUNDLE_TYPE, KEY_BUNDLE_TYPE,
MIN_PREKEY_COUNT, MIN_PREKEY_COUNT,
PLACEHOLDER_MAX_AGE_SECONDS, PLACEHOLDER_MAX_AGE_SECONDS,
@@ -1212,7 +1213,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
return await query(stanza) return await query(stanza)
} }
const sendRetryRequest = async (node: BinaryNode, forceIncludeKeys = false) => { const sendRetryRequest = async (node: BinaryNode, forceIncludeKeys = false, decryptionError?: string) => {
const { fullMessage } = decodeMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '') const { fullMessage } = decodeMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '')
const { key: msgKey } = fullMessage const { key: msgKey } = fullMessage
const msgId = msgKey.id! const msgId = msgKey.id!
@@ -1310,39 +1311,27 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
const fromJid = node.attrs.from! const fromJid = node.attrs.from!
// Check if we should recreate the session // Derive the Signal error code from the actual decryption failure message.
let shouldRecreateSession = false // This is sent in the retry receipt so the peer (even another InfiniteAPI instance)
let recreateReason = '' // knows the exact reason and can recreate the session immediately instead of waiting
// for the 1-hour timeout fallback.
if (enableAutoSessionRecreation && messageRetryManager && retryCount >= 1) { //
try { // Codes mirror RetryReason enum in message-retry-manager.ts:
// Check if we have a session with this JID // 0 = UnknownError | 1 = NoSession | 2 = InvalidKey
const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid) // 3 = InvalidKeyId | 7 = BadMac (= SignalErrorInvalidMessage/InvalidCipherKey)
const hasSession = await signalRepository.validateSession(fromJid) //
// NOTE: We do NOT delete the session here (receiver side). The Signal Protocol
// Extract error code from retry node if present (for MAC error detection) // recovers automatically when the sender's pkmsg arrives — it overwrites the
const retryNode = getBinaryNodeChild(node, 'retry') // corrupted session. Deleting prematurely creates a race window where no session
const errorAttr = retryNode?.attrs?.error // exists, which can cause "No Session" errors on concurrent messages.
const errorCode = messageRetryManager.parseRetryErrorCode(errorAttr) const retryErrorCode = (() => {
if (!decryptionError) return 0
const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists, errorCode) if (/bad\s*mac/i.test(decryptionError)) return 7 // SignalErrorBadMac
shouldRecreateSession = result.recreate if (/no\s*session/i.test(decryptionError)) return 1 // SignalErrorNoSession
recreateReason = result.reason if (/pre\s*key/i.test(decryptionError)) return 3 // SignalErrorInvalidKeyId
if (/invalid\s*key/i.test(decryptionError)) return 2 // SignalErrorInvalidKey
if (shouldRecreateSession) { return 0
logger.debug({ fromJid, retryCount, reason: recreateReason, errorCode }, 'recreating session for retry') })()
// Delete existing session to force recreation
// CRITICAL: Use same transaction key as encrypt/decrypt operations to prevent race
// Using meId ensures this delete serializes with sendMessage() and other session operations
await authState.keys.transaction(async () => {
await authState.keys.set({ session: { [sessionId]: null } })
}, authState.creds.me?.id || 'session-operation')
forceIncludeKeys = true
}
} catch (error) {
logger.warn({ error, fromJid }, 'failed to check session recreation')
}
}
if (retryCount <= 2) { if (retryCount <= 2) {
// Use new retry manager for phone requests if available // Use new retry manager for phone requests if available
@@ -1383,8 +1372,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
id: node.attrs.id!, id: node.attrs.id!,
t: node.attrs.t!, t: node.attrs.t!,
v: '1', v: '1',
// ADD ERROR FIELD error: retryErrorCode.toString()
error: '0'
} }
}, },
{ {
@@ -1403,7 +1391,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
receipt.attrs.participant = node.attrs.participant receipt.attrs.participant = node.attrs.participant
} }
if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) { if (retryCount > 1 || forceIncludeKeys) {
const { update, preKeys } = await getNextPreKeys(authState, 1) const { update, preKeys } = await getNextPreKeys(authState, 1)
const [keyId] = Object.keys(preKeys) const [keyId] = Object.keys(preKeys)
@@ -1440,7 +1428,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
logger.debug({ count, shouldUploadMorePreKeys }, 'recv pre-key count') logger.debug({ count, shouldUploadMorePreKeys }, 'recv pre-key count')
if (shouldUploadMorePreKeys) { if (shouldUploadMorePreKeys) {
await uploadPreKeys() // Top-up back to INITIAL_PREKEY_COUNT so the pool is always restored to full size
await uploadPreKeys(Math.max(1, INITIAL_PREKEY_COUNT - count))
} }
} else { } else {
const result = await handleIdentityChange(node, { const result = await handleIdentityChange(node, {
@@ -2505,7 +2494,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
} }
const encNode = getBinaryNodeChild(node, 'enc') const encNode = getBinaryNodeChild(node, 'enc')
await sendRetryRequest(node, !encNode) await sendRetryRequest(node, !encNode, errorMessage)
if (retryRequestDelayMs) { if (retryRequestDelayMs) {
await delay(retryRequestDelayMs) await delay(retryRequestDelayMs)
} }
@@ -2514,7 +2503,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// Still attempt retry even if pre-key upload failed // Still attempt retry even if pre-key upload failed
try { try {
const encNode = getBinaryNodeChild(node, 'enc') const encNode = getBinaryNodeChild(node, 'enc')
await sendRetryRequest(node, !encNode) await sendRetryRequest(node, !encNode, errorMessage)
} catch (retryErr) { } catch (retryErr) {
logger.error({ retryErr }, 'Failed to send retry after error handling') logger.error({ retryErr }, 'Failed to send retry after error handling')
} }
+28
View File
@@ -9,6 +9,7 @@ import type {
AlbumMessageOptions, AlbumMessageOptions,
AlbumSendResult, AlbumSendResult,
AnyMessageContent, AnyMessageContent,
LIDMapping,
MediaConnInfo, MediaConnInfo,
MessageReceiptType, MessageReceiptType,
MessageRelayOptions, MessageRelayOptions,
@@ -338,6 +339,33 @@ export const makeMessagesSocket = (config: SocketConfig) => {
} }
} }
// 4th LID→PN source: device-list entries sharing the same raw_id
// WA Business uses this for accounts where HistorySync sends zero phoneNumberToLidMappings
const allEntries = [...result.list, ...result.sideList]
const rawIdMap = new Map<number, { pn?: string; lid?: string }>()
for (const item of allEntries) {
if (typeof item.rawId !== 'number' || isNaN(item.rawId)) continue
const decoded = jidDecode(item.id)
if (!decoded) continue
const entry = rawIdMap.get(item.rawId) || {}
if (decoded.server === 'lid' || decoded.server === 'hosted.lid') {
entry.lid = item.id
} else if (decoded.server === 's.whatsapp.net' || decoded.server === 'c.us') {
entry.pn = item.id
}
rawIdMap.set(item.rawId, entry)
}
const rawIdMappings: LIDMapping[] = []
for (const { pn, lid } of rawIdMap.values()) {
if (pn && lid) {
rawIdMappings.push({ lid: jidNormalizedUser(lid), pn: jidNormalizedUser(pn) })
}
}
if (rawIdMappings.length > 0) {
await signalRepository.lidMapping.storeLIDPNMappings(rawIdMappings)
logger.debug({ count: rawIdMappings.length }, 'stored LID-PN mappings from raw_id pairing')
}
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 || ''
+2 -2
View File
@@ -123,11 +123,11 @@ export const makeNewsletterSocket = (config: SocketConfig) => {
}, },
newsletterMute: (jid: string) => { newsletterMute: (jid: string) => {
return executeWMexQuery({ newsletter_id: jid }, QueryIds.MUTE, XWAPaths.xwa2_newsletter_mute_v2) return executeWMexQuery({ input: { newsletter_id: jid, type: 'MUTE_ADMIN_ACTIVITY', value: 'OFF' } }, QueryIds.MUTE, XWAPaths.xwa2_newsletter_mute_v2)
}, },
newsletterUnmute: (jid: string) => { newsletterUnmute: (jid: string) => {
return executeWMexQuery({ newsletter_id: jid }, QueryIds.UNMUTE, XWAPaths.xwa2_newsletter_unmute_v2) return executeWMexQuery({ input: { newsletter_id: jid, type: 'MUTE_ADMIN_ACTIVITY', value: 'ON' } }, QueryIds.UNMUTE, XWAPaths.xwa2_newsletter_unmute_v2)
}, },
newsletterUpdateName: async (jid: string, name: string) => { newsletterUpdateName: async (jid: string, name: string) => {
+32 -23
View File
@@ -107,31 +107,20 @@ 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 && !envDisableCB) { if (enableCircuitBreaker) {
// 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: Math.max(defaultQueryTimeoutMs || 0, 120_000), timeout: defaultQueryTimeoutMs || 60000,
onStateChange: (from, to) => { onStateChange: (from, to) => {
logger.info({ from, to }, 'Query circuit breaker state changed') logger.info({ from, to }, 'Query circuit breaker state changed')
}, },
@@ -712,10 +701,12 @@ export const makeSocket = (config: SocketConfig) => {
} }
} }
// Prevent multiple concurrent uploads // Prevent multiple concurrent uploads — if one is already running, wait for it and return:
// the concurrent upload already replenished the pool, so there is nothing left to do.
if (uploadPreKeysPromise) { if (uploadPreKeysPromise) {
logger.debug('Pre-key upload already in progress, waiting for completion') logger.debug('Pre-key upload already in progress, waiting for completion')
await uploadPreKeysPromise await uploadPreKeysPromise
return
} }
const uploadLogic = async () => { const uploadLogic = async () => {
@@ -795,14 +786,15 @@ export const makeSocket = (config: SocketConfig) => {
try { try {
let count = 0 let count = 0
const preKeyCount = await getAvailablePreKeysOnServer() const preKeyCount = await getAvailablePreKeysOnServer()
if (preKeyCount === 0) count = INITIAL_PREKEY_COUNT // How many to upload: top-up to INITIAL_PREKEY_COUNT from whatever remains on server
else count = MIN_PREKEY_COUNT count = Math.max(0, INITIAL_PREKEY_COUNT - preKeyCount)
const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists() const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists()
logger.info(`${preKeyCount} pre-keys found on server`) logger.info(`${preKeyCount} pre-keys found on server`)
logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`) logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`)
const lowServerCount = preKeyCount <= count // Trigger upload when below the replenishment threshold, not when count < topUp amount
const lowServerCount = preKeyCount < MIN_PREKEY_COUNT
const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0 const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0
const shouldUpload = lowServerCount || missingCurrentPreKey const shouldUpload = lowServerCount || missingCurrentPreKey
@@ -1162,7 +1154,7 @@ export const makeSocket = (config: SocketConfig) => {
// Decrement active connections // Decrement active connections
decrementActiveConnections() decrementActiveConnections()
clearInterval(keepAliveReq) clearTimeout(keepAliveReq)
clearTimeout(qrTimer) clearTimeout(qrTimer)
// Clear offline-buffer safety timer so its callback cannot call ev.flush() // Clear offline-buffer safety timer so its callback cannot call ev.flush()
@@ -1273,8 +1265,16 @@ export const makeSocket = (config: SocketConfig) => {
}) })
} }
const startKeepAliveRequest = () => const startKeepAliveRequest = () => {
(keepAliveReq = setInterval(() => { // Use recursive setTimeout with ±15% jitter to match WA Desktop behaviour
// (WA Business Desktop: ~25-30s intervals with natural variance)
const scheduleNextKeepAlive = () => {
const jitter = keepAliveIntervalMs * 0.15
const delay = keepAliveIntervalMs + Math.floor((Math.random() * 2 - 1) * jitter)
keepAliveReq = setTimeout(onKeepAliveTick, delay)
}
const onKeepAliveTick = () => {
if (!lastDateRecv) { if (!lastDateRecv) {
lastDateRecv = new Date() lastDateRecv = new Date()
} }
@@ -1286,6 +1286,7 @@ export const makeSocket = (config: SocketConfig) => {
*/ */
if (diff > keepAliveIntervalMs + 5000) { if (diff > keepAliveIntervalMs + 5000) {
void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost })) void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
return // connection closing — do not reschedule
} else if (ws.isOpen) { } else if (ws.isOpen) {
// Send keep-alive ping via sendNode() (fire-and-forget) instead of query(). // Send keep-alive ping via sendNode() (fire-and-forget) instead of query().
// query() wraps the ping in the query circuit breaker — when that breaker is // query() wraps the ping in the query circuit breaker — when that breaker is
@@ -1308,7 +1309,15 @@ export const makeSocket = (config: SocketConfig) => {
} else { } else {
logger.warn('keep alive called when WS not open') logger.warn('keep alive called when WS not open')
} }
}, keepAliveIntervalMs))
// Do not reschedule once shutdown has started (closed set by end() on any concurrent path)
if (!closed) {
scheduleNextKeepAlive()
}
}
scheduleNextKeepAlive()
}
/** i have no idea why this exists. pls enlighten me */ /** i have no idea why this exists. pls enlighten me */
const sendPassiveIq = (tag: 'passive' | 'active') => const sendPassiveIq = (tag: 'passive' | 'active') =>
query({ query({
+4
View File
@@ -27,6 +27,8 @@ export type BaileysEventMap = {
chats: Chat[] chats: Chat[]
contacts: Contact[] contacts: Contact[]
messages: WAMessage[] messages: WAMessage[]
/** Past participants for group chats (people who left/were removed). userJid is always a phone number (PN), never a LID. */
pastParticipants?: proto.IPastParticipants[] | null
isLatest?: boolean isLatest?: boolean
progress?: number | null progress?: number | null
syncType?: proto.HistorySync.HistorySyncType | null syncType?: proto.HistorySync.HistorySyncType | null
@@ -185,6 +187,8 @@ export type BufferedEventData = {
chats: { [jid: string]: Chat } chats: { [jid: string]: Chat }
contacts: { [jid: string]: Contact } contacts: { [jid: string]: Contact }
messages: { [uqId: string]: WAMessage } messages: { [uqId: string]: WAMessage }
/** Keyed by groupJid for O(1) deduplication across chunks */
pastParticipants: { [groupJid: string]: proto.IPastParticipant[] }
empty: boolean empty: boolean
isLatest: boolean isLatest: boolean
progress?: number | null progress?: number | null
+8 -8
View File
@@ -4,10 +4,10 @@ export enum XWAPaths {
xwa2_newsletter_view = 'xwa2_newsletter_view', xwa2_newsletter_view = 'xwa2_newsletter_view',
xwa2_newsletter_metadata = 'xwa2_newsletter', xwa2_newsletter_metadata = 'xwa2_newsletter',
xwa2_newsletter_admin_count = 'xwa2_newsletter_admin', xwa2_newsletter_admin_count = 'xwa2_newsletter_admin',
xwa2_newsletter_mute_v2 = 'xwa2_newsletter_mute_v2', xwa2_newsletter_mute_v2 = 'xwa2_newsletter_update_user_setting',
xwa2_newsletter_unmute_v2 = 'xwa2_newsletter_unmute_v2', xwa2_newsletter_unmute_v2 = 'xwa2_newsletter_update_user_setting',
xwa2_newsletter_follow = 'xwa2_newsletter_follow', xwa2_newsletter_follow = 'xwa2_newsletter_join_v2',
xwa2_newsletter_unfollow = 'xwa2_newsletter_unfollow', xwa2_newsletter_unfollow = 'xwa2_newsletter_leave_v2',
xwa2_newsletter_change_owner = 'xwa2_newsletter_change_owner', xwa2_newsletter_change_owner = 'xwa2_newsletter_change_owner',
xwa2_newsletter_demote = 'xwa2_newsletter_demote', xwa2_newsletter_demote = 'xwa2_newsletter_demote',
xwa2_newsletter_delete_v2 = 'xwa2_newsletter_delete_v2' xwa2_newsletter_delete_v2 = 'xwa2_newsletter_delete_v2'
@@ -17,10 +17,10 @@ export enum QueryIds {
UPDATE_METADATA = '24250201037901610', UPDATE_METADATA = '24250201037901610',
METADATA = '6563316087068696', METADATA = '6563316087068696',
SUBSCRIBERS = '9783111038412085', SUBSCRIBERS = '9783111038412085',
FOLLOW = '7871414976211147', FOLLOW = '24404358912487870',
UNFOLLOW = '7238632346214362', UNFOLLOW = '9767147403369991',
MUTE = '29766401636284406', MUTE = '31938993655691868',
UNMUTE = '9864994326891137', UNMUTE = '31938993655691868',
ADMIN_COUNT = '7130823597031706', ADMIN_COUNT = '7130823597031706',
CHANGE_OWNER = '7341777602580933', CHANGE_OWNER = '7341777602580933',
DEMOTE = '6551828931592903', DEMOTE = '6551828931592903',
+28
View File
@@ -1,4 +1,5 @@
import EventEmitter from 'events' import EventEmitter from 'events'
import { proto } from '../../WAProto/index.js'
import type { import type {
BaileysEvent, BaileysEvent,
BaileysEventEmitter, BaileysEventEmitter,
@@ -890,6 +891,7 @@ const makeBufferData = (): BufferedEventData => {
chats: {}, chats: {},
messages: {}, messages: {},
contacts: {}, contacts: {},
pastParticipants: {},
isLatest: false, isLatest: false,
empty: true empty: true
}, },
@@ -967,6 +969,28 @@ function append<E extends BufferableEvent>(
} }
} }
// Merge pastParticipants with deduplication by groupJid and by userJid within each group.
// Multiple HistorySync chunks can carry the same group -- we merge rather than concatenate
// to avoid duplicate entries in the final event delivered to the consumer.
for (const group of (eventData.pastParticipants ?? []) as proto.IPastParticipants[]) {
const groupJid = group.groupJid
if (!groupJid) continue
if (!data.historySets.pastParticipants[groupJid]) {
data.historySets.pastParticipants[groupJid] = []
}
const existing = data.historySets.pastParticipants[groupJid]
const seenJids = new Set(existing.map(p => p.userJid).filter(Boolean))
for (const participant of (group.pastParticipants ?? []) as proto.IPastParticipant[]) {
if (participant.userJid && !seenJids.has(participant.userJid)) {
existing.push(participant)
seenJids.add(participant.userJid)
}
}
}
data.historySets.empty = false data.historySets.empty = false
data.historySets.syncType = eventData.syncType data.historySets.syncType = eventData.syncType
data.historySets.progress = eventData.progress data.historySets.progress = eventData.progress
@@ -1292,6 +1316,10 @@ function consolidateEvents(data: BufferedEventData) {
chats: Object.values(data.historySets.chats), chats: Object.values(data.historySets.chats),
messages: Object.values(data.historySets.messages), messages: Object.values(data.historySets.messages),
contacts: Object.values(data.historySets.contacts), contacts: Object.values(data.historySets.contacts),
// Convert dedup map back to array. Each entry has groupJid + participants (all unique userJids).
pastParticipants: Object.entries(data.historySets.pastParticipants).map(
([groupJid, participants]) => ({ groupJid, pastParticipants: participants })
),
syncType: data.historySets.syncType, syncType: data.historySets.syncType,
progress: data.historySets.progress, progress: data.historySets.progress,
isLatest: data.historySets.isLatest, isLatest: data.historySets.isLatest,
+34 -2
View File
@@ -15,7 +15,8 @@ import {
import { toNumber } from './generics' import { toNumber } from './generics'
import type { ILogger } from './logger.js' import type { ILogger } from './logger.js'
import { normalizeMessageContent } from './messages' import { normalizeMessageContent } from './messages'
import { downloadContentFromMessage } from './messages-media' import { DEFAULT_ORIGIN } from '../Defaults'
import { downloadContentFromMessage, getUrlFromDirectPath } from './messages-media'
const inflatePromise = promisify(inflate) const inflatePromise = promisify(inflate)
@@ -374,10 +375,25 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
// Convert Map back to array for return // Convert Map back to array for return
const lidPnMappings = Array.from(lidPnMap.values()) const lidPnMappings = Array.from(lidPnMap.values())
// Normalize pastParticipants: resolve LID userJids → PN so the consumer always
// receives a phone number, never an opaque LID identifier.
// Uses the lidPnMap built above (populated from phoneNumberToLidMappings + conversations).
// If a LID cannot be resolved, the original value is kept rather than dropping the participant.
const pastParticipants: proto.IPastParticipants[] = (item.pastParticipants ?? []).map(group => ({
groupJid: group.groupJid,
pastParticipants: (group.pastParticipants ?? []).map(participant => {
const userJid = participant.userJid
if (!userJid || !isAnyLidUser(userJid)) return participant
const mapping = lidPnMap.get(jidNormalizedUser(userJid))
return mapping?.pn ? { ...participant, userJid: mapping.pn } : participant
})
}))
return { return {
chats, chats,
contacts, contacts,
messages, messages,
pastParticipants,
lidPnMappings, lidPnMappings,
syncType: item.syncType, syncType: item.syncType,
progress: item.progress progress: item.progress
@@ -408,7 +424,23 @@ export const downloadAndProcessHistorySyncNotification = async (
historyMsg = await downloadHistory(msg, options) historyMsg = await downloadHistory(msg, options)
} }
return processHistoryMessage(historyMsg, logger) const result = processHistoryMessage(historyMsg, logger)
// Mirror WA Desktop behaviour: DELETE the CDN blob only after processing succeeds.
// Doing this earlier (e.g. inside downloadHistory) risks permanent history loss if
// processing throws — the server copy would be gone and retry after reconnect would fail.
if (msg.directPath) {
const cdnUrl = getUrlFromDirectPath(msg.directPath)
fetch(cdnUrl, {
...options,
method: 'DELETE',
headers: { ...((options as RequestInit).headers ?? {}), Origin: DEFAULT_ORIGIN }
}).catch(() => {
// non-fatal — server will expire it anyway
})
}
return result
} }
/** /**
+15 -8
View File
@@ -10,7 +10,7 @@ import {
} from './Protocols' } from './Protocols'
import { USyncUser } from './USyncUser' import { USyncUser } from './USyncUser'
export type USyncQueryResultList = { [protocol: string]: unknown; id: string } export type USyncQueryResultList = { [protocol: string]: unknown; id: string; rawId?: number }
export type USyncQueryResult = { export type USyncQueryResult = {
list: USyncQueryResultList[] list: USyncQueryResultList[]
@@ -68,10 +68,8 @@ export class USyncQuery {
//TODO: see if there are any errors in the result node //TODO: see if there are any errors in the result node
//const resultNode = getBinaryNodeChild(usyncNode, 'result') //const resultNode = getBinaryNodeChild(usyncNode, 'result')
const listNode = usyncNode ? getBinaryNodeChild(usyncNode, 'list') : undefined const parseNodeList = (content: BinaryNode[]): USyncQueryResultList[] =>
content.reduce((acc: USyncQueryResultList[], node) => {
if (listNode?.content && Array.isArray(listNode.content)) {
queryResult.list = listNode.content.reduce((acc: USyncQueryResultList[], node) => {
const id = node?.attrs.jid const id = node?.attrs.jid
if (id) { if (id) {
const data = Array.isArray(node?.content) const data = Array.isArray(node?.content)
@@ -89,15 +87,24 @@ export class USyncQuery {
.filter(([, b]) => b !== null) as [string, unknown][] .filter(([, b]) => b !== null) as [string, unknown][]
) )
: {} : {}
acc.push({ ...data, id }) const rawIdAttr = node?.attrs?.['raw_id']
const rawId = rawIdAttr !== undefined ? Number(rawIdAttr) : undefined
acc.push({ ...data, id, ...(rawId !== undefined && !isNaN(rawId) ? { rawId } : {}) })
} }
return acc return acc
}, []) }, [])
const listNode = usyncNode ? getBinaryNodeChild(usyncNode, 'list') : undefined
if (listNode?.content && Array.isArray(listNode.content)) {
queryResult.list = parseNodeList(listNode.content)
}
const sideListNode = usyncNode ? getBinaryNodeChild(usyncNode, 'side_list') : undefined
if (sideListNode?.content && Array.isArray(sideListNode.content)) {
queryResult.sideList = parseNodeList(sideListNode.content)
} }
//TODO: implement side list
//const sideListNode = getBinaryNodeChild(usyncNode, 'side_list')
return queryResult return queryResult
} }