Compare commits

...

51 Commits

Author SHA1 Message Date
Renato Alcara 871ad3711e chore: remove revert artifact file 2026-03-18 20:25:04 -03:00
Renato Alcara 3d8778561a fix(types): use proto.Message.IImageMessage in carousel helper 2026-03-18 19:23:10 -03:00
Renato Alcara 9d6ada6514 fix(lint): use project imageMessage type in carousel metadata helper 2026-03-18 19:20:25 -03:00
Renato Alcara 5e7c53eff7 fix(lint): flatten carousel image metadata fallback depth 2026-03-18 18:40:00 -03:00
Renato Alcara 0640de9a38 Reapply "refactor: flatten participant encryption flow for lint"
This reverts commit 7a66f8b3d6.
2026-03-17 23:01:20 -03:00
Renato Alcara 847e213443 fix: restore corrupted messages.ts from known-good carousel baseline 2026-03-17 22:57:34 -03:00
Renato Alcara 7a66f8b3d6 Revert "refactor: flatten participant encryption flow for lint"
This reverts commit a790bf616d.
2026-03-17 22:48:37 -03:00
Renato Alcara 5b42aaa642 Revert "Commit 5c6f38d by reverting its changes" 2026-03-17 22:41:30 -03:00
Renato Alcara 5c6f38d885 fix(lint): reduce max-depth in carousel image metadata recovery
- Refactor nested if blocks to single-line conditionals
- Fixes max-depth errors on lines 661, 665, 669
- No functional changes, only formatting
2026-03-17 22:38:51 -03:00
Renato Alcara a790bf616d refactor: flatten participant encryption flow for lint 2026-03-17 22:31:13 -03:00
Renato Alcara 259f9e3b68 chore: disable max-depth for messages-send 2026-03-17 21:20:05 -03:00
Renato Alcara 2ff8f6edba chore: scope max-depth lint suppression to participant fanout 2026-03-17 20:48:48 -03:00
Renato Alcara 0655d79e8b fix: move DSM helper out of participant loop 2026-03-17 20:32:42 -03:00
Renato Alcara 2363944bb3 fix: normalize meLidUser type for CI 2026-03-17 12:15:23 -03:00
Renato Alcara 7f1e9e67f7 fix: flatten DSM helper to satisfy lint 2026-03-17 12:06:15 -03:00
Renato Alcara acb8457ebc fix: flatten DSM selection in participant encryption 2026-03-17 11:33:20 -03:00
Renato Alcara 39d167168c fix: resolve lint issues in carousel send path 2026-03-17 11:27:51 -03:00
Renato Alcara 8340803377 chore: restore package files to master for CI 2026-03-17 11:14:21 -03:00
Renato Alcara aa37c8c19e Merge master into fix/carousel-send-render-clean 2026-03-17 11:10:33 -03:00
Renato Alcara d075195cda fix: restore working carousel send/render path 2026-03-17 11:03:06 -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
11 changed files with 2174 additions and 196 deletions
+75 -2
View File
@@ -1,7 +1,7 @@
syntax = "proto3"; syntax = "proto3";
package proto; package proto;
/// WhatsApp Version: 2.3000.1034621774 /// WhatsApp Version: 2.3000.1035302375
message ADVDeviceIdentity { message ADVDeviceIdentity {
optional uint32 rawId = 1; optional uint32 rawId = 1;
@@ -345,6 +345,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 {
@@ -407,6 +415,8 @@ message BotCapabilityMetadata {
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; JSON_PATCH_STREAMING = 58;
AI_TAB_FORCE_CLIPPY = 59;
UNIFIED_RESPONSE_EMBEDDED_SCREENS = 60;
} }
} }
@@ -1074,6 +1084,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 {
@@ -1454,6 +1465,7 @@ 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;
enum AdType { enum AdType {
CTWA = 0; CTWA = 0;
CAWC = 1; CAWC = 1;
@@ -1608,6 +1620,8 @@ 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;
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;
@@ -1635,6 +1649,8 @@ message DeviceCapabilities {
message BusinessBroadcast { message BusinessBroadcast {
optional bool importListEnabled = 1; optional bool importListEnabled = 1;
optional bool companionSupportEnabled = 2; optional bool companionSupportEnabled = 2;
optional bool campaignSyncEnabled = 3;
optional bool insightsSyncEnabled = 4;
} }
enum ChatLockSupportLevel { enum ChatLockSupportLevel {
@@ -1904,6 +1920,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 {
@@ -1912,6 +1930,9 @@ 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;
} }
message ServerHello { message ServerHello {
@@ -1919,6 +1940,7 @@ message HandshakeMessage {
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;
} }
} }
@@ -2275,6 +2297,7 @@ 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;
message AlbumMessage { message AlbumMessage {
optional uint32 expectedImageCount = 2; optional uint32 expectedImageCount = 2;
optional uint32 expectedVideoCount = 3; optional uint32 expectedVideoCount = 3;
@@ -2555,6 +2578,16 @@ 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;
}
message EventMessage { message EventMessage {
optional ContextInfo contextInfo = 1; optional ContextInfo contextInfo = 1;
optional bool isCanceled = 2; optional bool isCanceled = 2;
@@ -3081,9 +3114,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 {
@@ -3144,6 +3178,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;
@@ -3565,6 +3604,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;
BOT_UNLINK_MESSAGE = 33;
} }
} }
@@ -3597,6 +3637,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;
@@ -4124,6 +4165,8 @@ enum MutationProps {
BUSINESS_BROADCAST_CAMPAIGN_ACTION = 81; BUSINESS_BROADCAST_CAMPAIGN_ACTION = 81;
BUSINESS_BROADCAST_INSIGHTS_ACTION = 82; BUSINESS_BROADCAST_INSIGHTS_ACTION = 82;
CUSTOMER_DATA_ACTION = 83; CUSTOMER_DATA_ACTION = 83;
SUBSCRIPTIONS_SYNC_V2_ACTION = 84;
THREAD_PIN_ACTION = 85;
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;
@@ -4592,6 +4635,7 @@ message StatusAttribution {
APPLE_MUSIC = 8; APPLE_MUSIC = 8;
SHARECHAT = 9; SHARECHAT = 9;
GOOGLE_PHOTOS = 10; GOOGLE_PHOTOS = 10;
SOUNDCLOUD = 11;
} }
} }
@@ -4760,6 +4804,8 @@ message SyncActionValue {
optional BusinessBroadcastCampaignAction businessBroadcastCampaignAction = 81; optional BusinessBroadcastCampaignAction businessBroadcastCampaignAction = 81;
optional BusinessBroadcastInsightsAction businessBroadcastInsightsAction = 82; optional BusinessBroadcastInsightsAction businessBroadcastInsightsAction = 82;
optional CustomerDataAction customerDataAction = 83; optional CustomerDataAction customerDataAction = 83;
optional SubscriptionsSyncV2Action subscriptionsSyncV2Action = 84;
optional ThreadPinAction threadPinAction = 85;
message AgentAction { message AgentAction {
optional string name = 1; optional string name = 1;
optional int32 deviceID = 2; optional int32 deviceID = 2;
@@ -5275,6 +5321,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;
@@ -5286,6 +5355,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;
} }
+201 -5
View File
@@ -981,6 +981,38 @@ export namespace proto {
} }
} }
interface IBotAgentDeepLinkMetadata {
token?: (string|null);
}
class BotAgentDeepLinkMetadata implements IBotAgentDeepLinkMetadata {
constructor(p?: proto.IBotAgentDeepLinkMetadata);
public token?: (string|null);
public static create(properties?: proto.IBotAgentDeepLinkMetadata): proto.BotAgentDeepLinkMetadata;
public static encode(m: proto.IBotAgentDeepLinkMetadata, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.BotAgentDeepLinkMetadata;
public static fromObject(d: { [k: string]: any }): proto.BotAgentDeepLinkMetadata;
public static toObject(m: proto.BotAgentDeepLinkMetadata, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IBotAgentMetadata {
deepLinkMetadata?: (proto.IBotAgentDeepLinkMetadata|null);
}
class BotAgentMetadata implements IBotAgentMetadata {
constructor(p?: proto.IBotAgentMetadata);
public deepLinkMetadata?: (proto.IBotAgentDeepLinkMetadata|null);
public static create(properties?: proto.IBotAgentMetadata): proto.BotAgentMetadata;
public static encode(m: proto.IBotAgentMetadata, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.BotAgentMetadata;
public static fromObject(d: { [k: string]: any }): proto.BotAgentMetadata;
public static toObject(m: proto.BotAgentMetadata, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IBotCapabilityMetadata { interface IBotCapabilityMetadata {
capabilities?: (proto.BotCapabilityMetadata.BotCapabilityType[]|null); capabilities?: (proto.BotCapabilityMetadata.BotCapabilityType[]|null);
} }
@@ -1058,7 +1090,9 @@ export namespace proto {
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 JSON_PATCH_STREAMING = 58,
AI_TAB_FORCE_CLIPPY = 59,
UNIFIED_RESPONSE_EMBEDDED_SCREENS = 60
} }
} }
@@ -2769,6 +2803,7 @@ export namespace proto {
isSyncdPureLidSession?: (boolean|null); isSyncdPureLidSession?: (boolean|null);
isSyncdSnapshotRecoveryEnabled?: (boolean|null); isSyncdSnapshotRecoveryEnabled?: (boolean|null);
isHsThumbnailSyncEnabled?: (boolean|null); isHsThumbnailSyncEnabled?: (boolean|null);
subscriptionSyncPayload?: (Uint8Array|null);
} }
class ClientPairingProps implements IClientPairingProps { class ClientPairingProps implements IClientPairingProps {
@@ -2777,6 +2812,7 @@ export namespace proto {
public isSyncdPureLidSession?: (boolean|null); public isSyncdPureLidSession?: (boolean|null);
public isSyncdSnapshotRecoveryEnabled?: (boolean|null); public isSyncdSnapshotRecoveryEnabled?: (boolean|null);
public isHsThumbnailSyncEnabled?: (boolean|null); public isHsThumbnailSyncEnabled?: (boolean|null);
public subscriptionSyncPayload?: (Uint8Array|null);
public static create(properties?: proto.IClientPairingProps): proto.ClientPairingProps; public static create(properties?: proto.IClientPairingProps): proto.ClientPairingProps;
public static encode(m: proto.IClientPairingProps, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.IClientPairingProps, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ClientPairingProps; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ClientPairingProps;
@@ -3554,6 +3590,7 @@ export namespace proto {
adType?: (proto.ContextInfo.ExternalAdReplyInfo.AdType|null); adType?: (proto.ContextInfo.ExternalAdReplyInfo.AdType|null);
wtwaWebsiteUrl?: (string|null); wtwaWebsiteUrl?: (string|null);
adPreviewUrl?: (string|null); adPreviewUrl?: (string|null);
containsCtwaFlowsAutoReply?: (boolean|null);
} }
class ExternalAdReplyInfo implements IExternalAdReplyInfo { class ExternalAdReplyInfo implements IExternalAdReplyInfo {
@@ -3585,6 +3622,7 @@ export namespace proto {
public adType?: (proto.ContextInfo.ExternalAdReplyInfo.AdType|null); public adType?: (proto.ContextInfo.ExternalAdReplyInfo.AdType|null);
public wtwaWebsiteUrl?: (string|null); public wtwaWebsiteUrl?: (string|null);
public adPreviewUrl?: (string|null); public adPreviewUrl?: (string|null);
public containsCtwaFlowsAutoReply?: (boolean|null);
public static create(properties?: proto.ContextInfo.IExternalAdReplyInfo): proto.ContextInfo.ExternalAdReplyInfo; public static create(properties?: proto.ContextInfo.IExternalAdReplyInfo): proto.ContextInfo.ExternalAdReplyInfo;
public static encode(m: proto.ContextInfo.IExternalAdReplyInfo, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.ContextInfo.IExternalAdReplyInfo, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ContextInfo.ExternalAdReplyInfo; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ContextInfo.ExternalAdReplyInfo;
@@ -3849,6 +3887,8 @@ export namespace proto {
limitSharingInitiatedByMe?: (boolean|null); limitSharingInitiatedByMe?: (boolean|null);
maibaAiThreadEnabled?: (boolean|null); maibaAiThreadEnabled?: (boolean|null);
isMarketingMessageThread?: (boolean|null); isMarketingMessageThread?: (boolean|null);
isSenderNewAccount?: (boolean|null);
afterReadDuration?: (number|null);
} }
class Conversation implements IConversation { class Conversation implements IConversation {
@@ -3908,6 +3948,8 @@ export namespace proto {
public limitSharingInitiatedByMe?: (boolean|null); public limitSharingInitiatedByMe?: (boolean|null);
public maibaAiThreadEnabled?: (boolean|null); public maibaAiThreadEnabled?: (boolean|null);
public isMarketingMessageThread?: (boolean|null); public isMarketingMessageThread?: (boolean|null);
public isSenderNewAccount?: (boolean|null);
public afterReadDuration?: (number|null);
public static create(properties?: proto.IConversation): proto.Conversation; public static create(properties?: proto.IConversation): proto.Conversation;
public static encode(m: proto.IConversation, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.IConversation, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Conversation; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Conversation;
@@ -3983,12 +4025,16 @@ export namespace proto {
interface IBusinessBroadcast { interface IBusinessBroadcast {
importListEnabled?: (boolean|null); importListEnabled?: (boolean|null);
companionSupportEnabled?: (boolean|null); companionSupportEnabled?: (boolean|null);
campaignSyncEnabled?: (boolean|null);
insightsSyncEnabled?: (boolean|null);
} }
class BusinessBroadcast implements IBusinessBroadcast { class BusinessBroadcast implements IBusinessBroadcast {
constructor(p?: proto.DeviceCapabilities.IBusinessBroadcast); constructor(p?: proto.DeviceCapabilities.IBusinessBroadcast);
public importListEnabled?: (boolean|null); public importListEnabled?: (boolean|null);
public companionSupportEnabled?: (boolean|null); public companionSupportEnabled?: (boolean|null);
public campaignSyncEnabled?: (boolean|null);
public insightsSyncEnabled?: (boolean|null);
public static create(properties?: proto.DeviceCapabilities.IBusinessBroadcast): proto.DeviceCapabilities.BusinessBroadcast; public static create(properties?: proto.DeviceCapabilities.IBusinessBroadcast): proto.DeviceCapabilities.BusinessBroadcast;
public static encode(m: proto.DeviceCapabilities.IBusinessBroadcast, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.DeviceCapabilities.IBusinessBroadcast, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.DeviceCapabilities.BusinessBroadcast; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.DeviceCapabilities.BusinessBroadcast;
@@ -4684,6 +4730,8 @@ export namespace proto {
"static"?: (Uint8Array|null); "static"?: (Uint8Array|null);
payload?: (Uint8Array|null); payload?: (Uint8Array|null);
extendedCiphertext?: (Uint8Array|null); extendedCiphertext?: (Uint8Array|null);
paddedBytes?: (Uint8Array|null);
simulateXxkemFs?: (boolean|null);
} }
class ClientFinish implements IClientFinish { class ClientFinish implements IClientFinish {
@@ -4691,6 +4739,8 @@ export namespace proto {
public static?: (Uint8Array|null); public static?: (Uint8Array|null);
public payload?: (Uint8Array|null); public payload?: (Uint8Array|null);
public extendedCiphertext?: (Uint8Array|null); public extendedCiphertext?: (Uint8Array|null);
public paddedBytes?: (Uint8Array|null);
public simulateXxkemFs?: (boolean|null);
public static create(properties?: proto.HandshakeMessage.IClientFinish): proto.HandshakeMessage.ClientFinish; public static create(properties?: proto.HandshakeMessage.IClientFinish): proto.HandshakeMessage.ClientFinish;
public static encode(m: proto.HandshakeMessage.IClientFinish, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.HandshakeMessage.IClientFinish, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.HandshakeMessage.ClientFinish; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.HandshakeMessage.ClientFinish;
@@ -4706,6 +4756,9 @@ export namespace proto {
payload?: (Uint8Array|null); payload?: (Uint8Array|null);
useExtended?: (boolean|null); useExtended?: (boolean|null);
extendedCiphertext?: (Uint8Array|null); extendedCiphertext?: (Uint8Array|null);
paddedBytes?: (Uint8Array|null);
sendServerHelloPaddedBytes?: (boolean|null);
simulateXxkemFs?: (boolean|null);
} }
class ClientHello implements IClientHello { class ClientHello implements IClientHello {
@@ -4715,6 +4768,9 @@ export namespace proto {
public payload?: (Uint8Array|null); public payload?: (Uint8Array|null);
public useExtended?: (boolean|null); public useExtended?: (boolean|null);
public extendedCiphertext?: (Uint8Array|null); public extendedCiphertext?: (Uint8Array|null);
public paddedBytes?: (Uint8Array|null);
public sendServerHelloPaddedBytes?: (boolean|null);
public simulateXxkemFs?: (boolean|null);
public static create(properties?: proto.HandshakeMessage.IClientHello): proto.HandshakeMessage.ClientHello; public static create(properties?: proto.HandshakeMessage.IClientHello): proto.HandshakeMessage.ClientHello;
public static encode(m: proto.HandshakeMessage.IClientHello, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.HandshakeMessage.IClientHello, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.HandshakeMessage.ClientHello; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.HandshakeMessage.ClientHello;
@@ -4729,6 +4785,7 @@ export namespace proto {
"static"?: (Uint8Array|null); "static"?: (Uint8Array|null);
payload?: (Uint8Array|null); payload?: (Uint8Array|null);
extendedStatic?: (Uint8Array|null); extendedStatic?: (Uint8Array|null);
paddingBytes?: (Uint8Array|null);
} }
class ServerHello implements IServerHello { class ServerHello implements IServerHello {
@@ -4737,6 +4794,7 @@ export namespace proto {
public static?: (Uint8Array|null); public static?: (Uint8Array|null);
public payload?: (Uint8Array|null); public payload?: (Uint8Array|null);
public extendedStatic?: (Uint8Array|null); public extendedStatic?: (Uint8Array|null);
public paddingBytes?: (Uint8Array|null);
public static create(properties?: proto.HandshakeMessage.IServerHello): proto.HandshakeMessage.ServerHello; public static create(properties?: proto.HandshakeMessage.IServerHello): proto.HandshakeMessage.ServerHello;
public static encode(m: proto.HandshakeMessage.IServerHello, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.HandshakeMessage.IServerHello, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.HandshakeMessage.ServerHello; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.HandshakeMessage.ServerHello;
@@ -5546,6 +5604,7 @@ export namespace proto {
pollCreationMessageV6?: (proto.Message.IPollCreationMessage|null); pollCreationMessageV6?: (proto.Message.IPollCreationMessage|null);
conditionalRevealMessage?: (proto.Message.IConditionalRevealMessage|null); conditionalRevealMessage?: (proto.Message.IConditionalRevealMessage|null);
pollAddOptionMessage?: (proto.Message.IPollAddOptionMessage|null); pollAddOptionMessage?: (proto.Message.IPollAddOptionMessage|null);
eventInviteMessage?: (proto.Message.IEventInviteMessage|null);
} }
class Message implements IMessage { class Message implements IMessage {
@@ -5651,6 +5710,7 @@ export namespace proto {
public pollCreationMessageV6?: (proto.Message.IPollCreationMessage|null); public pollCreationMessageV6?: (proto.Message.IPollCreationMessage|null);
public conditionalRevealMessage?: (proto.Message.IConditionalRevealMessage|null); public conditionalRevealMessage?: (proto.Message.IConditionalRevealMessage|null);
public pollAddOptionMessage?: (proto.Message.IPollAddOptionMessage|null); public pollAddOptionMessage?: (proto.Message.IPollAddOptionMessage|null);
public eventInviteMessage?: (proto.Message.IEventInviteMessage|null);
public static create(properties?: proto.IMessage): proto.Message; public static create(properties?: proto.IMessage): proto.Message;
public static encode(m: proto.IMessage, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.IMessage, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message;
@@ -6454,6 +6514,34 @@ export namespace proto {
public static getTypeUrl(typeUrlPrefix?: string): string; public static getTypeUrl(typeUrlPrefix?: string): string;
} }
interface IEventInviteMessage {
contextInfo?: (proto.IContextInfo|null);
eventId?: (string|null);
eventTitle?: (string|null);
jpegThumbnail?: (Uint8Array|null);
startTime?: (number|Long|null);
caption?: (string|null);
isCanceled?: (boolean|null);
}
class EventInviteMessage implements IEventInviteMessage {
constructor(p?: proto.Message.IEventInviteMessage);
public contextInfo?: (proto.IContextInfo|null);
public eventId?: (string|null);
public eventTitle?: (string|null);
public jpegThumbnail?: (Uint8Array|null);
public startTime?: (number|Long|null);
public caption?: (string|null);
public isCanceled?: (boolean|null);
public static create(properties?: proto.Message.IEventInviteMessage): proto.Message.EventInviteMessage;
public static encode(m: proto.Message.IEventInviteMessage, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.EventInviteMessage;
public static fromObject(d: { [k: string]: any }): proto.Message.EventInviteMessage;
public static toObject(m: proto.Message.EventInviteMessage, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IEventMessage { interface IEventMessage {
contextInfo?: (proto.IContextInfo|null); contextInfo?: (proto.IContextInfo|null);
isCanceled?: (boolean|null); isCanceled?: (boolean|null);
@@ -7818,17 +7906,19 @@ export namespace proto {
interface IMessageHistoryMetadata { interface IMessageHistoryMetadata {
historyReceivers?: (string[]|null); historyReceivers?: (string[]|null);
oldestMessageTimestamp?: (number|Long|null); oldestMessageTimestampInWindow?: (number|Long|null);
messageCount?: (number|Long|null); messageCount?: (number|Long|null);
nonHistoryReceivers?: (string[]|null); nonHistoryReceivers?: (string[]|null);
oldestMessageTimestampInBundle?: (number|Long|null);
} }
class MessageHistoryMetadata implements IMessageHistoryMetadata { class MessageHistoryMetadata implements IMessageHistoryMetadata {
constructor(p?: proto.Message.IMessageHistoryMetadata); constructor(p?: proto.Message.IMessageHistoryMetadata);
public historyReceivers: string[]; public historyReceivers: string[];
public oldestMessageTimestamp?: (number|Long|null); public oldestMessageTimestampInWindow?: (number|Long|null);
public messageCount?: (number|Long|null); public messageCount?: (number|Long|null);
public nonHistoryReceivers: string[]; public nonHistoryReceivers: string[];
public oldestMessageTimestampInBundle?: (number|Long|null);
public static create(properties?: proto.Message.IMessageHistoryMetadata): proto.Message.MessageHistoryMetadata; public static create(properties?: proto.Message.IMessageHistoryMetadata): proto.Message.MessageHistoryMetadata;
public static encode(m: proto.Message.IMessageHistoryMetadata, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.Message.IMessageHistoryMetadata, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.MessageHistoryMetadata; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.MessageHistoryMetadata;
@@ -7986,6 +8076,7 @@ export namespace proto {
expiryTimestamp?: (number|Long|null); expiryTimestamp?: (number|Long|null);
incentiveEligible?: (boolean|null); incentiveEligible?: (boolean|null);
referralId?: (string|null); referralId?: (string|null);
inviteType?: (proto.Message.PaymentInviteMessage.InviteType|null);
} }
class PaymentInviteMessage implements IPaymentInviteMessage { class PaymentInviteMessage implements IPaymentInviteMessage {
@@ -7994,6 +8085,7 @@ export namespace proto {
public expiryTimestamp?: (number|Long|null); public expiryTimestamp?: (number|Long|null);
public incentiveEligible?: (boolean|null); public incentiveEligible?: (boolean|null);
public referralId?: (string|null); public referralId?: (string|null);
public inviteType?: (proto.Message.PaymentInviteMessage.InviteType|null);
public static create(properties?: proto.Message.IPaymentInviteMessage): proto.Message.PaymentInviteMessage; public static create(properties?: proto.Message.IPaymentInviteMessage): proto.Message.PaymentInviteMessage;
public static encode(m: proto.Message.IPaymentInviteMessage, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.Message.IPaymentInviteMessage, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.PaymentInviteMessage; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.PaymentInviteMessage;
@@ -8005,6 +8097,11 @@ export namespace proto {
namespace PaymentInviteMessage { namespace PaymentInviteMessage {
enum InviteType {
DEFAULT = 0,
MAPPER = 1
}
enum ServiceType { enum ServiceType {
UNKNOWN = 0, UNKNOWN = 0,
FBPAY = 1, FBPAY = 1,
@@ -9170,7 +9267,8 @@ export namespace proto {
AI_QUERY_FANOUT = 29, AI_QUERY_FANOUT = 29,
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,
BOT_UNLINK_MESSAGE = 33
} }
} }
@@ -9261,12 +9359,14 @@ export namespace proto {
interface IRequestWelcomeMessageMetadata { interface IRequestWelcomeMessageMetadata {
localChatState?: (proto.Message.RequestWelcomeMessageMetadata.LocalChatState|null); localChatState?: (proto.Message.RequestWelcomeMessageMetadata.LocalChatState|null);
welcomeTrigger?: (proto.Message.RequestWelcomeMessageMetadata.WelcomeTrigger|null); welcomeTrigger?: (proto.Message.RequestWelcomeMessageMetadata.WelcomeTrigger|null);
botAgentMetadata?: (proto.IBotAgentMetadata|null);
} }
class RequestWelcomeMessageMetadata implements IRequestWelcomeMessageMetadata { class RequestWelcomeMessageMetadata implements IRequestWelcomeMessageMetadata {
constructor(p?: proto.Message.IRequestWelcomeMessageMetadata); constructor(p?: proto.Message.IRequestWelcomeMessageMetadata);
public localChatState?: (proto.Message.RequestWelcomeMessageMetadata.LocalChatState|null); public localChatState?: (proto.Message.RequestWelcomeMessageMetadata.LocalChatState|null);
public welcomeTrigger?: (proto.Message.RequestWelcomeMessageMetadata.WelcomeTrigger|null); public welcomeTrigger?: (proto.Message.RequestWelcomeMessageMetadata.WelcomeTrigger|null);
public botAgentMetadata?: (proto.IBotAgentMetadata|null);
public static create(properties?: proto.Message.IRequestWelcomeMessageMetadata): proto.Message.RequestWelcomeMessageMetadata; public static create(properties?: proto.Message.IRequestWelcomeMessageMetadata): proto.Message.RequestWelcomeMessageMetadata;
public static encode(m: proto.Message.IRequestWelcomeMessageMetadata, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.Message.IRequestWelcomeMessageMetadata, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.RequestWelcomeMessageMetadata; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.RequestWelcomeMessageMetadata;
@@ -10462,6 +10562,8 @@ export namespace proto {
BUSINESS_BROADCAST_CAMPAIGN_ACTION = 81, BUSINESS_BROADCAST_CAMPAIGN_ACTION = 81,
BUSINESS_BROADCAST_INSIGHTS_ACTION = 82, BUSINESS_BROADCAST_INSIGHTS_ACTION = 82,
CUSTOMER_DATA_ACTION = 83, CUSTOMER_DATA_ACTION = 83,
SUBSCRIPTIONS_SYNC_V2_ACTION = 84,
THREAD_PIN_ACTION = 85,
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
@@ -11745,7 +11847,8 @@ export namespace proto {
THREADS = 7, THREADS = 7,
APPLE_MUSIC = 8, APPLE_MUSIC = 8,
SHARECHAT = 9, SHARECHAT = 9,
GOOGLE_PHOTOS = 10 GOOGLE_PHOTOS = 10,
SOUNDCLOUD = 11
} }
} }
@@ -12056,6 +12159,8 @@ export namespace proto {
businessBroadcastCampaignAction?: (proto.SyncActionValue.IBusinessBroadcastCampaignAction|null); businessBroadcastCampaignAction?: (proto.SyncActionValue.IBusinessBroadcastCampaignAction|null);
businessBroadcastInsightsAction?: (proto.SyncActionValue.IBusinessBroadcastInsightsAction|null); businessBroadcastInsightsAction?: (proto.SyncActionValue.IBusinessBroadcastInsightsAction|null);
customerDataAction?: (proto.SyncActionValue.ICustomerDataAction|null); customerDataAction?: (proto.SyncActionValue.ICustomerDataAction|null);
subscriptionsSyncV2Action?: (proto.SyncActionValue.ISubscriptionsSyncV2Action|null);
threadPinAction?: (proto.SyncActionValue.IThreadPinAction|null);
} }
class SyncActionValue implements ISyncActionValue { class SyncActionValue implements ISyncActionValue {
@@ -12134,6 +12239,8 @@ export namespace proto {
public businessBroadcastCampaignAction?: (proto.SyncActionValue.IBusinessBroadcastCampaignAction|null); public businessBroadcastCampaignAction?: (proto.SyncActionValue.IBusinessBroadcastCampaignAction|null);
public businessBroadcastInsightsAction?: (proto.SyncActionValue.IBusinessBroadcastInsightsAction|null); public businessBroadcastInsightsAction?: (proto.SyncActionValue.IBusinessBroadcastInsightsAction|null);
public customerDataAction?: (proto.SyncActionValue.ICustomerDataAction|null); public customerDataAction?: (proto.SyncActionValue.ICustomerDataAction|null);
public subscriptionsSyncV2Action?: (proto.SyncActionValue.ISubscriptionsSyncV2Action|null);
public threadPinAction?: (proto.SyncActionValue.IThreadPinAction|null);
public static create(properties?: proto.ISyncActionValue): proto.SyncActionValue; public static create(properties?: proto.ISyncActionValue): proto.SyncActionValue;
public static encode(m: proto.ISyncActionValue, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.ISyncActionValue, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue;
@@ -13666,6 +13773,79 @@ export namespace proto {
public static getTypeUrl(typeUrlPrefix?: string): string; public static getTypeUrl(typeUrlPrefix?: string): string;
} }
interface ISubscriptionsSyncV2Action {
subscriptions?: (proto.SyncActionValue.SubscriptionsSyncV2Action.ISubscriptionInfo[]|null);
paidFeature?: (proto.SyncActionValue.SubscriptionsSyncV2Action.IPaidFeature[]|null);
}
class SubscriptionsSyncV2Action implements ISubscriptionsSyncV2Action {
constructor(p?: proto.SyncActionValue.ISubscriptionsSyncV2Action);
public subscriptions: proto.SyncActionValue.SubscriptionsSyncV2Action.ISubscriptionInfo[];
public paidFeature: proto.SyncActionValue.SubscriptionsSyncV2Action.IPaidFeature[];
public static create(properties?: proto.SyncActionValue.ISubscriptionsSyncV2Action): proto.SyncActionValue.SubscriptionsSyncV2Action;
public static encode(m: proto.SyncActionValue.ISubscriptionsSyncV2Action, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.SubscriptionsSyncV2Action;
public static fromObject(d: { [k: string]: any }): proto.SyncActionValue.SubscriptionsSyncV2Action;
public static toObject(m: proto.SyncActionValue.SubscriptionsSyncV2Action, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
namespace SubscriptionsSyncV2Action {
interface IPaidFeature {
name?: (string|null);
enabled?: (boolean|null);
limit?: (number|null);
expirationTime?: (number|Long|null);
}
class PaidFeature implements IPaidFeature {
constructor(p?: proto.SyncActionValue.SubscriptionsSyncV2Action.IPaidFeature);
public name?: (string|null);
public enabled?: (boolean|null);
public limit?: (number|null);
public expirationTime?: (number|Long|null);
public static create(properties?: proto.SyncActionValue.SubscriptionsSyncV2Action.IPaidFeature): proto.SyncActionValue.SubscriptionsSyncV2Action.PaidFeature;
public static encode(m: proto.SyncActionValue.SubscriptionsSyncV2Action.IPaidFeature, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.SubscriptionsSyncV2Action.PaidFeature;
public static fromObject(d: { [k: string]: any }): proto.SyncActionValue.SubscriptionsSyncV2Action.PaidFeature;
public static toObject(m: proto.SyncActionValue.SubscriptionsSyncV2Action.PaidFeature, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface ISubscriptionInfo {
id?: (string|null);
tier?: (number|null);
status?: (string|null);
startTime?: (number|Long|null);
endTime?: (number|Long|null);
isPlatformChanged?: (boolean|null);
source?: (string|null);
creationTime?: (number|Long|null);
}
class SubscriptionInfo implements ISubscriptionInfo {
constructor(p?: proto.SyncActionValue.SubscriptionsSyncV2Action.ISubscriptionInfo);
public id?: (string|null);
public tier?: (number|null);
public status?: (string|null);
public startTime?: (number|Long|null);
public endTime?: (number|Long|null);
public isPlatformChanged?: (boolean|null);
public source?: (string|null);
public creationTime?: (number|Long|null);
public static create(properties?: proto.SyncActionValue.SubscriptionsSyncV2Action.ISubscriptionInfo): proto.SyncActionValue.SubscriptionsSyncV2Action.SubscriptionInfo;
public static encode(m: proto.SyncActionValue.SubscriptionsSyncV2Action.ISubscriptionInfo, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.SubscriptionsSyncV2Action.SubscriptionInfo;
public static fromObject(d: { [k: string]: any }): proto.SyncActionValue.SubscriptionsSyncV2Action.SubscriptionInfo;
public static toObject(m: proto.SyncActionValue.SubscriptionsSyncV2Action.SubscriptionInfo, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
}
interface ISyncActionMessage { interface ISyncActionMessage {
key?: (proto.IMessageKey|null); key?: (proto.IMessageKey|null);
timestamp?: (number|Long|null); timestamp?: (number|Long|null);
@@ -13704,6 +13884,22 @@ export namespace proto {
public static getTypeUrl(typeUrlPrefix?: string): string; public static getTypeUrl(typeUrlPrefix?: string): string;
} }
interface IThreadPinAction {
pinned?: (boolean|null);
}
class ThreadPinAction implements IThreadPinAction {
constructor(p?: proto.SyncActionValue.IThreadPinAction);
public pinned?: (boolean|null);
public static create(properties?: proto.SyncActionValue.IThreadPinAction): proto.SyncActionValue.ThreadPinAction;
public static encode(m: proto.SyncActionValue.IThreadPinAction, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.ThreadPinAction;
public static fromObject(d: { [k: string]: any }): proto.SyncActionValue.ThreadPinAction;
public static toObject(m: proto.SyncActionValue.ThreadPinAction, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface ITimeFormatAction { interface ITimeFormatAction {
isTwentyFourHourFormatEnabled?: (boolean|null); isTwentyFourHourFormatEnabled?: (boolean|null);
} }
+1515 -19
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,1034641024]} {"version":[2,3000,1035317910]}
+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)
+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
} }
+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