Compare commits

...

42 Commits

Author SHA1 Message Date
Claude 37c41931cf fix: resolve all lint errors in PR #297 (prettier formatting + max-depth)
- Apply prettier auto-fix across all modified files (indentation, trailing commas, import order)
- Fix simple-import-sort violations in messages-send.ts, messages.ts, process-message.ts, sticker-pack.ts
- Add eslint-disable-next-line max-depth for 3 deeply nested if blocks in messages.ts
- Remove unused eslint-disable directives flagged as warnings in messages-send.ts

https://claude.ai/code/session_011Z7EFNQqHqn3Eu6xtQ9YZK
2026-03-18 00:55:00 +00: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
20 changed files with 2379 additions and 433 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]}
+1 -1
View File
@@ -306,7 +306,7 @@ export function makeLibSignalRepository(
// This prevents PN/LID race conditions where concurrent operations for the // This prevents PN/LID race conditions where concurrent operations for the
// same logical contact acquire different mutex locks because one uses PN // same logical contact acquire different mutex locks because one uses PN
// and the other uses LID. (Aligned with WABA behavior — all operations use LID internally.) // and the other uses LID. (Aligned with WABA behavior — all operations use LID internally.)
const resolveCanonicalJid = async(jid: string): Promise<string> => { const resolveCanonicalJid = async (jid: string): Promise<string> => {
if (isAnyLidUser(jid)) { if (isAnyLidUser(jid)) {
return jid return jid
} }
+10 -6
View File
@@ -25,7 +25,8 @@ export const makeGroupsSocket = (config: SocketConfig) => {
const lidMapping = signalRepository.lidMapping const lidMapping = signalRepository.lidMapping
// Resolve all participant LIDs in parallel for better performance on large groups // Resolve all participant LIDs in parallel for better performance on large groups
await Promise.all(metadata.participants.map(async (p) => { await Promise.all(
metadata.participants.map(async p => {
if (isLidUser(p.id)) { if (isLidUser(p.id)) {
if (p.phoneNumber) { if (p.phoneNumber) {
p.lid = p.id p.lid = p.id
@@ -38,15 +39,16 @@ export const makeGroupsSocket = (config: SocketConfig) => {
} }
} }
} }
})) })
)
// Normalize owner/subjectOwner if LID (parallel) // Normalize owner/subjectOwner if LID (parallel)
const [resolvedOwner, resolvedSubjectOwner] = await Promise.all([ const [resolvedOwner, resolvedSubjectOwner] = await Promise.all([
metadata.owner && isLidUser(metadata.owner) metadata.owner && isLidUser(metadata.owner)
? (metadata.ownerPn || resolveLidToPn(metadata.owner, lidMapping, logger)) ? metadata.ownerPn || resolveLidToPn(metadata.owner, lidMapping, logger)
: null, : null,
metadata.subjectOwner && isLidUser(metadata.subjectOwner) metadata.subjectOwner && isLidUser(metadata.subjectOwner)
? (metadata.subjectOwnerPn || resolveLidToPn(metadata.subjectOwner, lidMapping, logger)) ? metadata.subjectOwnerPn || resolveLidToPn(metadata.subjectOwner, lidMapping, logger)
: null : null
]) ])
if (resolvedOwner) metadata.owner = resolvedOwner if (resolvedOwner) metadata.owner = resolvedOwner
@@ -95,11 +97,13 @@ export const makeGroupsSocket = (config: SocketConfig) => {
if (groupsChild) { if (groupsChild) {
const groups = getBinaryNodeChildren(groupsChild, 'group') const groups = getBinaryNodeChildren(groupsChild, 'group')
for (const groupNode of groups) { for (const groupNode of groups) {
const meta = await normalizeGroupMetadata(extractGroupMetadata({ const meta = await normalizeGroupMetadata(
extractGroupMetadata({
tag: 'result', tag: 'result',
attrs: {}, attrs: {},
content: [groupNode] content: [groupNode]
})) })
)
data[meta.id] = meta data[meta.id] = meta
} }
} }
+159 -155
View File
@@ -376,7 +376,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
for (const update of updates) { for (const update of updates) {
if (update.jid && update.user) { if (update.jid && update.user) {
const [resolvedAuthor, resolvedUser] = await Promise.all([ const [resolvedAuthor, resolvedUser] = await Promise.all([
resolveLidToPn(node.attrs.from!, signalRepository.lidMapping, logger), resolveLidToPn(node.attrs.from, signalRepository.lidMapping, logger),
resolveLidToPn(update.user, signalRepository.lidMapping, logger) resolveLidToPn(update.user, signalRepository.lidMapping, logger)
]) ])
ev.emit('newsletter-participants.update', { ev.emit('newsletter-participants.update', {
@@ -403,7 +403,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const child = getAllBinaryNodeChildren(node)[0]! const child = getAllBinaryNodeChildren(node)[0]!
const rawAuthor = node.attrs.participant! const rawAuthor = node.attrs.participant!
// Resolve author LID→PN (participant is a user JID that could be LID) // Resolve author LID→PN (participant is a user JID that could be LID)
const author = await resolveLidToPn(rawAuthor, signalRepository.lidMapping, logger) || rawAuthor const author = (await resolveLidToPn(rawAuthor, signalRepository.lidMapping, logger)) || rawAuthor
logger.info({ from, child }, 'got newsletter notification') logger.info({ from, child }, 'got newsletter notification')
@@ -430,7 +430,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
break break
case 'participant': case 'participant':
const resolvedParticipantUser = await resolveLidToPn(child.attrs.jid!, signalRepository.lidMapping, logger) || child.attrs.jid! const resolvedParticipantUser =
(await resolveLidToPn(child.attrs.jid, signalRepository.lidMapping, logger)) || child.attrs.jid!
const participantUpdate = { const participantUpdate = {
id: from, id: from,
author, author,
@@ -573,7 +574,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const offerContent: BinaryNode[] = [ const offerContent: BinaryNode[] = [
{ tag: 'privacy', attrs: {}, content: undefined }, { tag: 'privacy', attrs: {}, content: undefined },
{ tag: 'audio', attrs: { rate: '8000', enc: 'opus' }, content: undefined }, { tag: 'audio', attrs: { rate: '8000', enc: 'opus' }, content: undefined },
{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined }, { tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined }
] ]
if (isVideo) { if (isVideo) {
@@ -594,31 +595,31 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
{ tag: 'net', attrs: { medium: '3' }, content: undefined }, { tag: 'net', attrs: { medium: '3' }, content: undefined },
{ tag: 'capability', attrs: { ver: '1' }, content: undefined }, { tag: 'capability', attrs: { ver: '1' }, content: undefined },
{ tag: 'enc', attrs: { v: '2', type: isVideo ? 'msg' : 'pkmsg' }, content: undefined }, { tag: 'enc', attrs: { v: '2', type: isVideo ? 'msg' : 'pkmsg' }, content: undefined },
{ tag: 'encopt', attrs: { keygen: '2' }, content: undefined }, { tag: 'encopt', attrs: { keygen: '2' }, content: undefined }
) )
// Voice calls include device-identity (verified via Frida capture) // Voice calls include device-identity (verified via Frida capture)
if (!isVideo) { if (!isVideo) {
offerContent.push( offerContent.push({ tag: 'device-identity', attrs: {}, content: undefined })
{ tag: 'device-identity', attrs: {}, content: undefined },
)
} }
const stanza: BinaryNode = { const stanza: BinaryNode = {
tag: 'call', tag: 'call',
attrs: { attrs: {
to: jid, to: jid,
id: stanzaId, id: stanzaId
}, },
content: [{ content: [
{
tag: 'offer', tag: 'offer',
attrs: { attrs: {
'call-creator': meId, 'call-creator': meId,
'call-id': callId, 'call-id': callId,
'device_class': '2013', device_class: '2013'
}, },
content: offerContent content: offerContent
}] }
]
} }
await query(stanza) await query(stanza)
@@ -647,7 +648,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const terminateAttrs: Record<string, string> = { const terminateAttrs: Record<string, string> = {
'call-id': callId, 'call-id': callId,
'call-creator': callCreator || meId, 'call-creator': callCreator || meId
} }
if (reason) { if (reason) {
@@ -663,13 +664,15 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
tag: 'call', tag: 'call',
attrs: { attrs: {
to: callTo, to: callTo,
id: randomBytes(16).toString('hex').toUpperCase(), id: randomBytes(16).toString('hex').toUpperCase()
}, },
content: [{ content: [
{
tag: 'terminate', tag: 'terminate',
attrs: terminateAttrs, attrs: terminateAttrs,
content: undefined content: undefined
}] }
]
} }
await query(stanza) await query(stanza)
@@ -683,24 +686,18 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
* @param callFrom - JID of the caller (call-creator) * @param callFrom - JID of the caller (call-creator)
* @param isVideo - true for video call * @param isVideo - true for video call
*/ */
const acceptCall = async ( const acceptCall = async (callId: string, callFrom: string, isVideo?: boolean) => {
callId: string,
callFrom: string,
isVideo?: boolean,
) => {
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 acceptContent: BinaryNode[] = [ const acceptContent: BinaryNode[] = [{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined }]
{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined },
]
if (isVideo) { if (isVideo) {
acceptContent.push({ acceptContent.push({
tag: 'video', tag: 'video',
attrs: { attrs: {
dec: 'H264,AV1', dec: 'H264,AV1',
device_orientation: '1', device_orientation: '1'
}, },
content: undefined content: undefined
}) })
@@ -708,7 +705,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
acceptContent.push( acceptContent.push(
{ tag: 'net', attrs: { medium: '2' }, content: undefined }, { tag: 'net', attrs: { medium: '2' }, content: undefined },
{ tag: 'encopt', attrs: { keygen: '2' }, content: undefined }, { tag: 'encopt', attrs: { keygen: '2' }, content: undefined }
) )
const stanza: BinaryNode = { const stanza: BinaryNode = {
@@ -716,16 +713,18 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
attrs: { attrs: {
from: meId, from: meId,
to: callFrom, to: callFrom,
id: randomBytes(16).toString('hex').toUpperCase(), id: randomBytes(16).toString('hex').toUpperCase()
}, },
content: [{ content: [
{
tag: 'accept', tag: 'accept',
attrs: { attrs: {
'call-id': callId, 'call-id': callId,
'call-creator': callFrom, 'call-creator': callFrom
}, },
content: acceptContent content: acceptContent
}] }
]
} }
await query(stanza) await query(stanza)
@@ -740,14 +739,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
* @param callCreator - JID of the caller * @param callCreator - JID of the caller
* @param isVideo - true for video call * @param isVideo - true for video call
*/ */
const preacceptCall = async ( const preacceptCall = async (callId: string, callCreator: string, isVideo?: boolean) => {
callId: string, const preacceptContent: BinaryNode[] = [{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined }]
callCreator: string,
isVideo?: boolean,
) => {
const preacceptContent: BinaryNode[] = [
{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined },
]
if (isVideo) { if (isVideo) {
preacceptContent.push({ preacceptContent.push({
@@ -756,7 +749,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
screen_width: '1080', screen_width: '1080',
screen_height: '2400', screen_height: '2400',
dec: 'H264,H265,AV1', dec: 'H264,H265,AV1',
device_orientation: '0', device_orientation: '0'
}, },
content: undefined content: undefined
}) })
@@ -764,23 +757,25 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
preacceptContent.push( preacceptContent.push(
{ tag: 'encopt', attrs: { keygen: '2' }, content: undefined }, { tag: 'encopt', attrs: { keygen: '2' }, content: undefined },
{ tag: 'capability', attrs: { ver: '1' }, content: undefined }, { tag: 'capability', attrs: { ver: '1' }, content: undefined }
) )
const stanza: BinaryNode = { const stanza: BinaryNode = {
tag: 'call', tag: 'call',
attrs: { attrs: {
to: callCreator, to: callCreator,
id: randomBytes(16).toString('hex').toUpperCase(), id: randomBytes(16).toString('hex').toUpperCase()
}, },
content: [{ content: [
{
tag: 'preaccept', tag: 'preaccept',
attrs: { attrs: {
'call-id': callId, 'call-id': callId,
'call-creator': callCreator, 'call-creator': callCreator
}, },
content: preacceptContent content: preacceptContent
}] }
]
} }
await query(stanza) await query(stanza)
@@ -806,11 +801,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
dlBw?: number dlBw?: number
ulBw?: number ulBw?: number
}>, }>,
transactionId?: string, transactionId?: string
) => { ) => {
const relayLatencyAttrs: Record<string, string> = { const relayLatencyAttrs: Record<string, string> = {
'call-id': callId, 'call-id': callId,
'call-creator': callCreator, 'call-creator': callCreator
} }
if (transactionId) { if (transactionId) {
@@ -843,13 +838,15 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
tag: 'call', tag: 'call',
attrs: { attrs: {
to: callCreator, to: callCreator,
id: randomBytes(16).toString('hex').toUpperCase(), id: randomBytes(16).toString('hex').toUpperCase()
}, },
content: [{ content: [
{
tag: 'relaylatency', tag: 'relaylatency',
attrs: relayLatencyAttrs, attrs: relayLatencyAttrs,
content: teChildren content: teChildren
}] }
]
} }
await sendNode(stanza) await sendNode(stanza)
@@ -870,12 +867,12 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
callCreator: string, callCreator: string,
to: string, to: string,
candidates: Array<{ priority: string; data?: Uint8Array }>, candidates: Array<{ priority: string; data?: Uint8Array }>,
round?: number, round?: number
) => { ) => {
const transportAttrs: Record<string, string> = { const transportAttrs: Record<string, string> = {
'call-id': callId, 'call-id': callId,
'call-creator': callCreator, 'call-creator': callCreator,
'transport-message-type': '1', 'transport-message-type': '1'
} }
if (round !== undefined) { if (round !== undefined) {
@@ -885,20 +882,22 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const teChildren: BinaryNode[] = candidates.map(c => ({ const teChildren: BinaryNode[] = candidates.map(c => ({
tag: 'te', tag: 'te',
attrs: { priority: c.priority }, attrs: { priority: c.priority },
content: c.data, content: c.data
})) }))
const stanza: BinaryNode = { const stanza: BinaryNode = {
tag: 'call', tag: 'call',
attrs: { attrs: {
to, to,
id: randomBytes(16).toString('hex').toUpperCase(), id: randomBytes(16).toString('hex').toUpperCase()
}, },
content: [{ content: [
{
tag: 'transport', tag: 'transport',
attrs: transportAttrs, attrs: transportAttrs,
content: teChildren content: teChildren
}] }
]
} }
await sendNode(stanza) await sendNode(stanza)
@@ -919,25 +918,27 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
callCreator: string, callCreator: string,
peer: string, peer: string,
audioDuration: number, audioDuration: number,
callType: string = '1x1', callType = '1x1'
) => { ) => {
const stanza: BinaryNode = { const stanza: BinaryNode = {
tag: 'call', tag: 'call',
attrs: { attrs: {
to: 'call', to: 'call',
id: randomBytes(16).toString('hex').toUpperCase(), id: randomBytes(16).toString('hex').toUpperCase()
}, },
content: [{ content: [
{
tag: 'duration', tag: 'duration',
attrs: { attrs: {
'call-id': callId, 'call-id': callId,
'call-creator': callCreator, 'call-creator': callCreator,
peer, peer,
audio_duration: String(audioDuration), audio_duration: String(audioDuration),
type: callType, type: callType
}, },
content: undefined content: undefined
}] }
]
} }
await sendNode(stanza) await sendNode(stanza)
@@ -952,27 +953,24 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
* @param to - destination JID * @param to - destination JID
* @param muted - true to mute, false to unmute * @param muted - true to mute, false to unmute
*/ */
const muteCall = async ( const muteCall = async (callId: string, callCreator: string, to: string, muted: boolean) => {
callId: string,
callCreator: string,
to: string,
muted: boolean,
) => {
const stanza: BinaryNode = { const stanza: BinaryNode = {
tag: 'call', tag: 'call',
attrs: { attrs: {
to, to,
id: randomBytes(16).toString('hex').toUpperCase(), id: randomBytes(16).toString('hex').toUpperCase()
}, },
content: [{ content: [
{
tag: 'mute_v2', tag: 'mute_v2',
attrs: { attrs: {
'mute-state': muted ? '1' : '0', 'mute-state': muted ? '1' : '0',
'call-id': callId, 'call-id': callId,
'call-creator': callCreator, 'call-creator': callCreator
}, },
content: undefined content: undefined
}] }
]
} }
await sendNode(stanza) await sendNode(stanza)
@@ -985,24 +983,23 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
* @param callId - the call-id (also used as JID with @call) * @param callId - the call-id (also used as JID with @call)
* @param callCreator - JID of the call creator * @param callCreator - JID of the call creator
*/ */
const sendHeartbeat = async ( const sendHeartbeat = async (callId: string, callCreator: string) => {
callId: string,
callCreator: string,
) => {
const stanza: BinaryNode = { const stanza: BinaryNode = {
tag: 'call', tag: 'call',
attrs: { attrs: {
to: `${callId}@call`, to: `${callId}@call`,
id: randomBytes(16).toString('hex').toUpperCase(), id: randomBytes(16).toString('hex').toUpperCase()
}, },
content: [{ content: [
{
tag: 'heartbeat', tag: 'heartbeat',
attrs: { attrs: {
'call-id': callId, 'call-id': callId,
'call-creator': callCreator, 'call-creator': callCreator
}, },
content: undefined content: undefined
}] }
]
} }
await sendNode(stanza) await sendNode(stanza)
@@ -1017,30 +1014,27 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
* @param to - destination JID * @param to - destination JID
* @param transactionId - transaction ID for the rekey * @param transactionId - transaction ID for the rekey
*/ */
const sendEncRekey = async ( const sendEncRekey = async (callId: string, callCreator: string, to: string, transactionId: string) => {
callId: string,
callCreator: string,
to: string,
transactionId: string,
) => {
const stanza: BinaryNode = { const stanza: BinaryNode = {
tag: 'call', tag: 'call',
attrs: { attrs: {
to, to,
id: randomBytes(16).toString('hex').toUpperCase(), id: randomBytes(16).toString('hex').toUpperCase()
}, },
content: [{ content: [
{
tag: 'enc_rekey', tag: 'enc_rekey',
attrs: { attrs: {
'transaction-id': transactionId, 'transaction-id': transactionId,
'call-id': callId, 'call-id': callId,
'call-creator': callCreator, 'call-creator': callCreator
}, },
content: [ content: [
{ tag: 'encopt', attrs: { keygen: '2' }, content: undefined }, { tag: 'encopt', attrs: { keygen: '2' }, content: undefined },
{ tag: 'enc', attrs: { v: '2', type: 'msg' }, content: undefined }, { tag: 'enc', attrs: { v: '2', type: 'msg' }, content: undefined }
]
}
] ]
}]
} }
await sendNode(stanza) await sendNode(stanza)
@@ -1061,24 +1055,26 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
callCreator: string, callCreator: string,
to: string, to: string,
enabled: boolean, enabled: boolean,
orientation: string = '1', orientation = '1'
) => { ) => {
const stanza: BinaryNode = { const stanza: BinaryNode = {
tag: 'call', tag: 'call',
attrs: { attrs: {
to, to,
id: randomBytes(16).toString('hex').toUpperCase(), id: randomBytes(16).toString('hex').toUpperCase()
}, },
content: [{ content: [
{
tag: 'video', tag: 'video',
attrs: { attrs: {
'call-id': callId, 'call-id': callId,
'call-creator': callCreator, 'call-creator': callCreator,
state: enabled ? '1' : '0', state: enabled ? '1' : '0',
device_orientation: orientation, device_orientation: orientation
}, },
content: undefined content: undefined
}] }
]
} }
await sendNode(stanza) await sendNode(stanza)
@@ -1100,15 +1096,17 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
tag: 'call', tag: 'call',
attrs: { attrs: {
to: 'call', to: 'call',
id: randomBytes(16).toString('hex').toUpperCase(), id: randomBytes(16).toString('hex').toUpperCase()
}, },
content: [{ content: [
{
tag: 'link_create', tag: 'link_create',
attrs: { media }, attrs: { media },
content: event content: event
? [{ tag: 'event', attrs: { start_time: String(event.startTime) }, content: undefined }] ? [{ tag: 'event', attrs: { start_time: String(event.startTime) }, content: undefined }]
: undefined : undefined
}] }
]
} }
const response = await query(stanza, timeoutMs) const response = await query(stanza, timeoutMs)
@@ -1136,9 +1134,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
} }
// URL format verified via Frida capture: https://call.whatsapp.com/<token> // URL format verified via Frida capture: https://call.whatsapp.com/<token>
const url = token const url = token ? `https://call.whatsapp.com/${token}` : undefined
? `https://call.whatsapp.com/${token}`
: undefined
return { token, url, response } return { token, url, response }
} }
@@ -1156,13 +1152,15 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
tag: 'call', tag: 'call',
attrs: { attrs: {
to: 'call', to: 'call',
id: randomBytes(16).toString('hex').toUpperCase(), id: randomBytes(16).toString('hex').toUpperCase()
}, },
content: [{ content: [
{
tag: 'link_query', tag: 'link_query',
attrs: { media, token }, attrs: { media, token },
content: undefined content: undefined
}] }
]
} }
return await query(stanza) return await query(stanza)
@@ -1180,7 +1178,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const joinContent: BinaryNode[] = [ const joinContent: BinaryNode[] = [
{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined }, { tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined },
{ tag: 'net', attrs: { medium: '2' }, content: undefined }, { tag: 'net', attrs: { medium: '2' }, content: undefined },
{ tag: 'capability', attrs: { ver: '1' }, content: undefined }, { tag: 'capability', attrs: { ver: '1' }, content: undefined }
] ]
if (media === 'video') { if (media === 'video') {
@@ -1190,7 +1188,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
screen_width: '1080', screen_width: '1080',
screen_height: '2400', screen_height: '2400',
dec: 'H264,H265,AV1', dec: 'H264,H265,AV1',
device_orientation: '0', device_orientation: '0'
}, },
content: undefined content: undefined
}) })
@@ -1200,13 +1198,15 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
tag: 'call', tag: 'call',
attrs: { attrs: {
to: 'call', to: 'call',
id: randomBytes(16).toString('hex').toUpperCase(), id: randomBytes(16).toString('hex').toUpperCase()
}, },
content: [{ content: [
{
tag: 'link_join', tag: 'link_join',
attrs: { media, token }, attrs: { media, token },
content: joinContent content: joinContent
}] }
]
} }
return await query(stanza) return await query(stanza)
@@ -1223,7 +1223,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// For group messages, scope by participant (each participant has its own Signal session). // For group messages, scope by participant (each participant has its own Signal session).
const retryDedupeJid = msgKey.participant const retryDedupeJid = msgKey.participant
? jidNormalizedUser(msgKey.participant) ? jidNormalizedUser(msgKey.participant)
: jidNormalizedUser(node.attrs.from!) : jidNormalizedUser(node.attrs.from)
if (retryRequestActiveJids.has(retryDedupeJid)) { if (retryRequestActiveJids.has(retryDedupeJid)) {
logger.debug( logger.debug(
{ fromJid: retryDedupeJid, msgId }, { fromJid: retryDedupeJid, msgId },
@@ -1246,7 +1246,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// this cleanup only runs as a last resort. // this cleanup only runs as a last resort.
// For group messages, use participant JID (Signal sessions are per-participant, not per-group). // For group messages, use participant JID (Signal sessions are per-participant, not per-group).
if (autoCleanCorrupted) { if (autoCleanCorrupted) {
const senderJid = msgKey.participant ? jidNormalizedUser(msgKey.participant) : jidNormalizedUser(node.attrs.from!) const senderJid = msgKey.participant
? jidNormalizedUser(msgKey.participant)
: jidNormalizedUser(node.attrs.from)
try { try {
const decryptionJid = await getDecryptionJid(senderJid, signalRepository) const decryptionJid = await getDecryptionJid(senderJid, signalRepository)
const deletedCount = await cleanupCorruptedSession(decryptionJid, signalRepository, logger) const deletedCount = await cleanupCorruptedSession(decryptionJid, signalRepository, logger)
@@ -1282,7 +1284,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// Safety net cleanup (same as new system above) // Safety net cleanup (same as new system above)
if (autoCleanCorrupted) { if (autoCleanCorrupted) {
const senderJid = msgKey.participant ? jidNormalizedUser(msgKey.participant) : jidNormalizedUser(node.attrs.from!) const senderJid = msgKey.participant
? jidNormalizedUser(msgKey.participant)
: jidNormalizedUser(node.attrs.from)
try { try {
const decryptionJid = await getDecryptionJid(senderJid, signalRepository) const decryptionJid = await getDecryptionJid(senderJid, signalRepository)
await cleanupCorruptedSession(decryptionJid, signalRepository, logger) await cleanupCorruptedSession(decryptionJid, signalRepository, logger)
@@ -1464,13 +1468,13 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const senderTs = unixTimestampSeconds() const senderTs = unixTimestampSeconds()
logTcToken('reissue', { jid: normalizedJid, reason: 'session_refreshed' }) logTcToken('reissue', { jid: normalizedJid, reason: 'session_refreshed' })
getPrivacyTokens([normalizedJid], senderTs) getPrivacyTokens([normalizedJid], senderTs)
.then(async (iqResult) => { .then(async iqResult => {
await storeTcTokensFromIqResult({ await storeTcTokensFromIqResult({
result: iqResult, result: iqResult,
fallbackJid: normalizedJid, fallbackJid: normalizedJid,
keys: authState.keys, keys: authState.keys,
getLIDForPN, getLIDForPN,
onNewJidStored: (storedJid) => { onNewJidStored: storedJid => {
tcTokenKnownJids.add(storedJid) tcTokenKnownJids.add(storedJid)
scheduleTcTokenIndexSave() scheduleTcTokenIndexSave()
} }
@@ -1516,16 +1520,20 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn! const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn!
// Resolve acting participant to PN — prefer inline PN, fall back to LID→PN resolution // Resolve acting participant to PN — prefer inline PN, fall back to LID→PN resolution
const actingParticipant = actingParticipantPn const actingParticipant = actingParticipantPn || (await resolveLidToPn(actingParticipantLid, lidMapping, logger))
|| await resolveLidToPn(actingParticipantLid, lidMapping, logger)
// Resolve affected participant to PN // Resolve affected participant to PN
const affectedParticipant = affectedParticipantPn const affectedParticipant =
|| await resolveLidToPn(affectedParticipantLid, lidMapping, logger) affectedParticipantPn || (await resolveLidToPn(affectedParticipantLid, lidMapping, logger))
// Store LID↔PN mappings from notification attributes // Store LID↔PN mappings from notification attributes
const mappingsToStore: Array<{ lid: string; pn: string }> = [] const mappingsToStore: Array<{ lid: string; pn: string }> = []
if (actingParticipantLid && actingParticipantPn && isLidUser(actingParticipantLid) && isPnUser(actingParticipantPn)) { if (
actingParticipantLid &&
actingParticipantPn &&
isLidUser(actingParticipantLid) &&
isPnUser(actingParticipantPn)
) {
mappingsToStore.push({ lid: actingParticipantLid, pn: actingParticipantPn }) mappingsToStore.push({ lid: actingParticipantLid, pn: actingParticipantPn })
} }
@@ -1554,7 +1562,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// Resolve metadata owner to PN // Resolve metadata owner to PN
if (metadata.owner && isLidUser(metadata.owner)) { if (metadata.owner && isLidUser(metadata.owner)) {
const resolvedOwner = metadata.ownerPn || await resolveLidToPn(metadata.owner, lidMapping, logger) const resolvedOwner = metadata.ownerPn || (await resolveLidToPn(metadata.owner, lidMapping, logger))
if (resolvedOwner) metadata.owner = resolvedOwner if (resolvedOwner) metadata.owner = resolvedOwner
} }
@@ -1588,7 +1596,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
case 'modify': case 'modify':
const oldNumbers = await Promise.all( const oldNumbers = await Promise.all(
getBinaryNodeChildren(child, 'participant').map(async p => { getBinaryNodeChildren(child, 'participant').map(async p => {
const resolved = await resolveLidToPn(p.attrs.jid!, lidMapping, logger) const resolved = await resolveLidToPn(p.attrs.jid, lidMapping, logger)
return resolved || p.attrs.jid! return resolved || p.attrs.jid!
}) })
) )
@@ -1770,7 +1778,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
{ {
const rawPictureJid = jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '' const rawPictureJid = jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || ''
const pictureJid = await resolveLidToPn(rawPictureJid, signalRepository.lidMapping, logger) || rawPictureJid const pictureJid = (await resolveLidToPn(rawPictureJid, signalRepository.lidMapping, logger)) || rawPictureJid
ev.emit('contacts.update', [ ev.emit('contacts.update', [
{ {
id: pictureJid, id: pictureJid,
@@ -1815,7 +1823,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const blocklists = getBinaryNodeChildren(child, 'item') const blocklists = getBinaryNodeChildren(child, 'item')
for (const { attrs } of blocklists) { for (const { attrs } of blocklists) {
const resolvedBlockJid = await resolveLidToPn(attrs.jid!, signalRepository.lidMapping, logger) || attrs.jid! const resolvedBlockJid =
(await resolveLidToPn(attrs.jid, signalRepository.lidMapping, logger)) || attrs.jid!
const blocklist = [resolvedBlockJid] const blocklist = [resolvedBlockJid]
const type = attrs.action === 'block' ? 'add' : 'remove' const type = attrs.action === 'block' ? 'add' : 'remove'
ev.emit('blocklist.update', { blocklist, type }) ev.emit('blocklist.update', { blocklist, type })
@@ -2146,7 +2155,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
if (attrs.participant) { if (attrs.participant) {
const updateKey: keyof MessageUserReceipt = const updateKey: keyof MessageUserReceipt =
status === proto.WebMessageInfo.Status.DELIVERY_ACK ? 'receiptTimestamp' : 'readTimestamp' status === proto.WebMessageInfo.Status.DELIVERY_ACK ? 'receiptTimestamp' : 'readTimestamp'
const resolvedReceiptUserJid = await resolveLidToPn(attrs.participant, lidMapping, logger) || jidNormalizedUser(attrs.participant) const resolvedReceiptUserJid =
(await resolveLidToPn(attrs.participant, lidMapping, logger)) || jidNormalizedUser(attrs.participant)
ev.emit( ev.emit(
'message-receipt.update', 'message-receipt.update',
ids.map(id => ({ ids.map(id => ({
@@ -2259,13 +2269,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
category, category,
author, author,
decrypt decrypt
} = decryptMessageNode( } = decryptMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '', signalRepository, logger)
node,
authState.creds.me!.id,
authState.creds.me!.lid || '',
signalRepository,
logger,
)
const alt = msg.key.participantAlt || msg.key.remoteJidAlt const alt = msg.key.participantAlt || msg.key.remoteJidAlt
// Handle LID/PN mappings with hybrid approach: // Handle LID/PN mappings with hybrid approach:
@@ -2680,7 +2684,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
jid: u.attrs.jid, jid: u.attrs.jid,
state: u.attrs.state, state: u.attrs.state,
userPn: u.attrs.user_pn, userPn: u.attrs.user_pn,
type: u.attrs.type, type: u.attrs.type
})) }))
} }
@@ -2768,9 +2772,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const callSummary = getBinaryNodeChild(infoChild, 'call_summary') const callSummary = getBinaryNodeChild(infoChild, 'call_summary')
if (callSummary) { if (callSummary) {
call.media = callSummary.attrs.media call.media = callSummary.attrs.media
call.duration = callSummary.attrs.call_duration call.duration = callSummary.attrs.call_duration ? Number(callSummary.attrs.call_duration) : undefined
? Number(callSummary.attrs.call_duration)
: undefined
call.participants = extractParticipants(callSummary) call.participants = extractParticipants(callSummary)
} }
} }
@@ -2814,12 +2816,14 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
if (resolvedLinkCreator) call.linkCreator = resolvedLinkCreator if (resolvedLinkCreator) call.linkCreator = resolvedLinkCreator
// Resolve participant JIDs in parallel // Resolve participant JIDs in parallel
if (call.participants) { if (call.participants) {
await Promise.all(call.participants.map(async (p) => { await Promise.all(
call.participants.map(async p => {
if (p.jid) { if (p.jid) {
const resolved = p.userPn || await resolveLidToPn(p.jid, callLidMapping, logger) const resolved = p.userPn || (await resolveLidToPn(p.jid, callLidMapping, logger))
if (resolved) p.jid = resolved if (resolved) p.jid = resolved
} }
})) })
)
} }
ev.emit('call', [call]) ev.emit('call', [call])
@@ -2857,20 +2861,22 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// WABA Android: error 463 triggers getPrivacyTokens() fire-and-forget // WABA Android: error 463 triggers getPrivacyTokens() fire-and-forget
// to ensure token is available for the retry below // to ensure token is available for the retry below
getPrivacyTokens([jid]) getPrivacyTokens([jid])
.then(async (result) => { .then(async result => {
await storeTcTokensFromIqResult({ await storeTcTokensFromIqResult({
result, result,
fallbackJid: jid, fallbackJid: jid,
keys: authState.keys, keys: authState.keys,
getLIDForPN, getLIDForPN,
onNewJidStored: (storedJid) => { onNewJidStored: storedJid => {
tcTokenKnownJids.add(storedJid) tcTokenKnownJids.add(storedJid)
scheduleTcTokenIndexSave() scheduleTcTokenIndexSave()
} }
}) })
logTcToken('fetched', { jid, reason: 'error_463' }) logTcToken('fetched', { jid, reason: 'error_463' })
}) })
.catch(() => { /* fire-and-forget */ }) .catch(() => {
/* fire-and-forget */
})
// Single-retry: wait 1.5s for the server's tctoken notification to arrive, // Single-retry: wait 1.5s for the server's tctoken notification to arrive,
// then resend. A Set prevents infinite retry loops. // then resend. A Set prevents infinite retry loops.
@@ -2911,20 +2917,22 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
logTcToken('error_479', { jid: jid479, msgId: attrs.id }) logTcToken('error_479', { jid: jid479, msgId: attrs.id })
// WABA Android: error 479 (SmaxInvalid) also triggers token re-fetch // WABA Android: error 479 (SmaxInvalid) also triggers token re-fetch
getPrivacyTokens([jid479]) getPrivacyTokens([jid479])
.then(async (result) => { .then(async result => {
await storeTcTokensFromIqResult({ await storeTcTokensFromIqResult({
result, result,
fallbackJid: jid479, fallbackJid: jid479,
keys: authState.keys, keys: authState.keys,
getLIDForPN, getLIDForPN,
onNewJidStored: (storedJid) => { onNewJidStored: storedJid => {
tcTokenKnownJids.add(storedJid) tcTokenKnownJids.add(storedJid)
scheduleTcTokenIndexSave() scheduleTcTokenIndexSave()
} }
}) })
logTcToken('fetched', { jid: jid479, reason: 'error_479' }) logTcToken('fetched', { jid: jid479, reason: 'error_479' })
}) })
.catch(() => { /* fire-and-forget */ }) .catch(() => {
/* fire-and-forget */
})
} else { } else {
logger.warn({ attrs }, 'received error in ack') logger.warn({ attrs }, 'received error in ack')
} }
@@ -3058,24 +3066,20 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// (call link relays may arrive without these attrs — just log them) // (call link relays may arrive without these attrs — just log them)
if (callId && rawCallCreator) { if (callId && rawCallCreator) {
// Resolve LID→PN for call creator // Resolve LID→PN for call creator
const callCreator = await resolveLidToPn(rawCallCreator, signalRepository.lidMapping, logger) || rawCallCreator const callCreator = (await resolveLidToPn(rawCallCreator, signalRepository.lidMapping, logger)) || rawCallCreator
logger.debug( logger.debug({ callId, callCreator, uuid: node.attrs.uuid }, 'received relay info')
{ callId, callCreator, uuid: node.attrs.uuid }, ev.emit('call', [
'received relay info' {
)
ev.emit('call', [{
chatId: callCreator, chatId: callCreator,
from: callCreator, from: callCreator,
id: callId, id: callId,
date: new Date(), date: new Date(),
offline: false, offline: false,
status: 'relay' as WACallUpdateType, status: 'relay' as WACallUpdateType
}]) }
])
} else { } else {
logger.debug( logger.debug({ attrs: node.attrs }, 'received relay stanza without call-id/call-creator')
{ attrs: node.attrs },
'received relay stanza without call-id/call-creator'
)
} }
}) })
+176 -45
View File
@@ -1,5 +1,6 @@
import NodeCache from '@cacheable/node-cache' import NodeCache from '@cacheable/node-cache'
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import { randomBytes } from 'crypto'
import { proto } from '../../WAProto/index.js' import { proto } from '../../WAProto/index.js'
import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults' import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults'
import type { import type {
@@ -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,31 @@ 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 createParticipantNodes = async ( const createParticipantNodes = async (
recipientJids: string[], recipientJids: string[],
message: proto.IMessage, message: proto.IMessage,
@@ -560,29 +618,14 @@ 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 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 }) => { async ({ recipientJid: jid, message: patchedMessage }: { recipientJid: string; message: proto.IMessage }) => {
try { try {
if (!jid) return null if (!jid) return null
let msgToEncrypt = patchedMessage const msgToEncrypt = resolveDsmMessageForRecipient(jid, patchedMessage, dsmMessage, meId, meLid, meLidUser)
if (dsmMessage) {
const { user: targetUser } = jidDecode(jid)!
const { user: ownPnUser } = jidDecode(meId)!
const ownLidUser = meLidUser
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 bytes = encodeWAMessage(msgToEncrypt)
const mutexKey = jid const mutexKey = jid
@@ -1166,21 +1209,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 +1305,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 +1351,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,10 +1394,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
) )
const interactiveType = 'native_flow' const interactiveType = 'native_flow'
deferredNodes.push({ const bizContent: BinaryNode[] = [
tag: 'biz',
attrs: {},
content: [
{ {
tag: 'interactive', tag: 'interactive',
attrs: { attrs: {
@@ -1361,6 +1412,31 @@ export const makeMessagesSocket = (config: SocketConfig) => {
] ]
} }
] ]
// 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({
tag: 'biz',
attrs: {},
content: bizContent
}) })
} }
@@ -1386,11 +1462,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 +1493,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,11 +1551,63 @@ 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 })
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({
result: fetchResult,
fallbackJid: destinationJid,
keys: authState.keys,
getLIDForPN
}).catch(() => {})
} catch (err: any) {
logger.warn({ jid: destinationJid, err: err?.message }, '[CAROUSEL] Blocking tctoken fetch failed')
} finally {
tcTokenFetchingJids.delete(tcTokenJid)
}
} else {
// Fire-and-forget for non-carousel
getPrivacyTokens([destinationJid]) getPrivacyTokens([destinationJid])
.then(async fetchResult => { .then(async fetchResult => {
await storeTcTokensFromIqResult({ await storeTcTokensFromIqResult({
@@ -1486,7 +1615,6 @@ export const makeMessagesSocket = (config: SocketConfig) => {
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(err => { .catch(err => {
@@ -1496,6 +1624,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
tcTokenFetchingJids.delete(tcTokenJid) tcTokenFetchingJids.delete(tcTokenJid)
}) })
} }
}
if (tcTokenBuffer?.length) { if (tcTokenBuffer?.length) {
;(stanza.content as BinaryNode[]).push({ ;(stanza.content as BinaryNode[]).push({
@@ -1525,8 +1654,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 +1663,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 +1685,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)
+9 -4
View File
@@ -1361,13 +1361,16 @@ export const makeSocket = (config: SocketConfig) => {
const pairPlatformId = isAndroid ? getPlatformId('Chrome') : getPlatformId(browser[1]) const pairPlatformId = isAndroid ? getPlatformId('Chrome') : getPlatformId(browser[1])
const pairPlatformDisplay = isAndroid ? 'Chrome (Mac OS)' : `${browser[1]} (${browser[0]})` const pairPlatformDisplay = isAndroid ? 'Chrome (Mac OS)' : `${browser[1]} (${browser[0]})`
logger.info({ logger.info(
{
pairCode: pairingCode, pairCode: pairingCode,
jid: authState.creds.me.id, jid: authState.creds.me.id,
companionPlatformId: pairPlatformId, companionPlatformId: pairPlatformId,
companionPlatformDisplay: pairPlatformDisplay, companionPlatformDisplay: pairPlatformDisplay,
isAndroid, isAndroid
}, `pair code requested | companion: ${pairPlatformDisplay} | ${isAndroid ? 'android override -> Chrome' : 'native platform'}`) },
`pair code requested | companion: ${pairPlatformDisplay} | ${isAndroid ? 'android override -> Chrome' : 'native platform'}`
)
ev.emit('creds.update', authState.creds) ev.emit('creds.update', authState.creds)
await sendNode({ await sendNode({
@@ -1538,7 +1541,9 @@ export const makeSocket = (config: SocketConfig) => {
ws.on('CB:success', async (node: BinaryNode) => { ws.on('CB:success', async (node: BinaryNode) => {
const isAndroid = isAndroidBrowser(browser) const isAndroid = isAndroidBrowser(browser)
const phoneId = authState.creds.me?.id?.split(':')[0]?.split('@')[0] || 'new session' const phoneId = authState.creds.me?.id?.split(':')[0]?.split('@')[0] || 'new session'
logger.info(`${isAndroid ? '\uD83D\uDCF1' : '\uD83D\uDDA5\uFE0F'} Connected to WA | ${phoneId} | platform: ${isAndroid ? 'SMB_ANDROID' : 'MACOS'} | device: ${isAndroid ? 'Android' : 'Desktop'} | platformType: ${isAndroid ? 'ANDROID_PHONE' : 'CHROME'}`) logger.info(
`${isAndroid ? '\uD83D\uDCF1' : '\uD83D\uDDA5\uFE0F'} Connected to WA | ${phoneId} | platform: ${isAndroid ? 'SMB_ANDROID' : 'MACOS'} | device: ${isAndroid ? 'Android' : 'Desktop'} | platformType: ${isAndroid ? 'ANDROID_PHONE' : 'CHROME'}`
)
clearTimeout(qrTimer) // will never happen in all likelyhood -- but just in case WA sends success on first try clearTimeout(qrTimer) // will never happen in all likelyhood -- but just in case WA sends success on first try
ev.emit('creds.update', { me: { ...authState.creds.me!, lid: node.attrs.lid } }) ev.emit('creds.update', { me: { ...authState.creds.me!, lid: node.attrs.lid } })
+1 -1
View File
@@ -716,7 +716,7 @@ export function createConnectionCircuitBreaker(customOptions?: Partial<CircuitBr
const WS_LIFECYCLE_STATUS_CODES = new Set([ const WS_LIFECYCLE_STATUS_CODES = new Set([
428, // connectionClosed 428, // connectionClosed
408, // connectionLost / timedOut 408, // connectionLost / timedOut
440, // connectionReplaced 440 // connectionReplaced
]) ])
return new CircuitBreaker({ return new CircuitBreaker({
+2 -2
View File
@@ -13,7 +13,7 @@ import {
isJidNewsletter, isJidNewsletter,
isJidStatusBroadcast, isJidStatusBroadcast,
isLidUser, isLidUser,
isPnUser, isPnUser
// transferDevice // transferDevice
} from '../WABinary' } from '../WABinary'
import { unpadRandomMax16 } from './generics' import { unpadRandomMax16 } from './generics'
@@ -274,7 +274,7 @@ export const decryptMessageNode = (
meId: string, meId: string,
meLid: string, meLid: string,
repository: SignalRepositoryWithLIDStore, repository: SignalRepositoryWithLIDStore,
logger: ILogger, logger: ILogger
) => { ) => {
const { fullMessage, author, sender } = decodeMessageNode(stanza, meId, meLid) const { fullMessage, author, sender } = decodeMessageNode(stanza, meId, meLid)
return { return {
+1
View File
@@ -246,6 +246,7 @@ export const fetchLatestBaileysVersion = async (options: RequestInit & { timeout
} finally { } finally {
clearTimeout(timeout) clearTimeout(timeout)
} }
if (!response.ok) { if (!response.ok) {
throw new Boom(`Failed to fetch latest Baileys version: ${response.statusText}`, { statusCode: response.status }) throw new Boom(`Failed to fetch latest Baileys version: ${response.statusText}`, { statusCode: response.status })
} }
+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,
+47 -1
View File
@@ -41,10 +41,12 @@ import type { ILogger } from './logger'
import { import {
downloadContentFromMessage, downloadContentFromMessage,
encryptedStream, encryptedStream,
extractImageThumb,
generateThumbnail, generateThumbnail,
getAudioDuration, getAudioDuration,
getAudioWaveform, getAudioWaveform,
getRawMediaUploadData, getRawMediaUploadData,
getStream,
type MediaDownloadOptions type MediaDownloadOptions
} from './messages-media' } from './messages-media'
import { shouldIncludeReportingToken } from './reporting-utils' import { shouldIncludeReportingToken } from './reporting-utils'
@@ -648,6 +650,49 @@ 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.
if (imageMessage && (!imageMessage.jpegThumbnail || !imageMessage.height || !imageMessage.width)) {
try {
const { stream } = await getStream(card.image, mediaOptions.options)
const { buffer, original } = await extractImageThumb(stream)
// eslint-disable-next-line max-depth
if (!imageMessage.jpegThumbnail) {
imageMessage.jpegThumbnail = buffer.toString('base64')
}
// eslint-disable-next-line max-depth
if (!imageMessage.width && original.width) {
imageMessage.width = original.width
}
// eslint-disable-next-line max-depth
if (!imageMessage.height && original.height) {
imageMessage.height = original.height
}
mediaOptions.logger?.info(
{
cardTitle: card.title,
recoveredThumbnail: !!imageMessage.jpegThumbnail,
width: imageMessage.width,
height: imageMessage.height
},
'[CAROUSEL] Recovered image metadata from source media'
)
} catch (error) {
mediaOptions.logger?.warn(
{
cardTitle: card.title,
trace: error instanceof Error ? error.stack : String(error)
},
'[CAROUSEL] Failed source-media thumbnail fallback'
)
}
}
// 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 +1284,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
} }
+3 -2
View File
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import Long from 'long' import Long from 'long'
import { proto } from '../../WAProto/index.js' import { proto } from '../../WAProto/index.js'
import type { LIDMappingStore } from '../Signal/lid-mapping'
import type { import type {
AuthenticationCreds, AuthenticationCreds,
BaileysEventEmitter, BaileysEventEmitter,
@@ -19,7 +20,6 @@ import type {
WAMessage, WAMessage,
WAMessageKey WAMessageKey
} from '../Types' } from '../Types'
import type { LIDMappingStore } from '../Signal/lid-mapping'
import { WAMessageStubType } from '../Types' import { WAMessageStubType } from '../Types'
import { getContentType, normalizeMessageContent } from '../Utils/messages' import { getContentType, normalizeMessageContent } from '../Utils/messages'
import { import {
@@ -784,7 +784,8 @@ const processMessage = async (
}) })
} }
const participantsIncludesMe = () => participants.find(p => areJidsSameUser(meId, p.id) || areJidsSameUser(meId, p.phoneNumber)) const participantsIncludesMe = () =>
participants.find(p => areJidsSameUser(meId, p.id) || areJidsSameUser(meId, p.phoneNumber))
switch (message.messageStubType) { switch (message.messageStubType) {
case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER: case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER:
+4 -5
View File
@@ -2,7 +2,7 @@ import { Boom } from '@hapi/boom'
import { createHash } from 'crypto' import { createHash } from 'crypto'
import { zipSync } from 'fflate' import { zipSync } from 'fflate'
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import { gzipSync, gunzipSync } from 'zlib' import { gunzipSync, gzipSync } from 'zlib'
import { proto } from '../../WAProto/index.js' import { proto } from '../../WAProto/index.js'
import type { MediaType } from '../Defaults/index.js' import type { MediaType } from '../Defaults/index.js'
import type { StickerPack, WAMediaUpload, WAMediaUploadFunction } from '../Types/Message.js' import type { StickerPack, WAMediaUpload, WAMediaUploadFunction } from '../Types/Message.js'
@@ -728,10 +728,9 @@ export const prepareStickerPackMessage = async (
// Tray icon uses PNG format, 96x96 pixels (official client standard) // Tray icon uses PNG format, 96x96 pixels (official client standard)
const lib = await getImageProcessingLibrary() const lib = await getImageProcessingLibrary()
if (!lib?.sharp) { if (!lib?.sharp) {
throw new Boom( throw new Boom('Sharp library is required for cover/tray icon processing. Install with: yarn add sharp', {
'Sharp library is required for cover/tray icon processing. Install with: yarn add sharp', statusCode: 400
{ statusCode: 400 } })
)
} }
coverWebP = await lib.sharp coverWebP = await lib.sharp
+12 -4
View File
@@ -154,9 +154,7 @@ export async function storeTcTokensFromIqResult({
continue continue
} }
await keys.set({ const tokenEntry = {
tctoken: {
[storageJid]: {
...existingEntry, ...existingEntry,
token: Buffer.from(tokenNode.content), token: Buffer.from(tokenNode.content),
timestamp: tokenNode.attrs.t, timestamp: tokenNode.attrs.t,
@@ -164,8 +162,18 @@ export async function storeTcTokensFromIqResult({
// (UPDATE wa_trusted_contacts_send SET real_issue_timestamp=null) // (UPDATE wa_trusted_contacts_send SET real_issue_timestamp=null)
realIssueTimestamp: 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