Compare commits

..

2 Commits

Author SHA1 Message Date
Renato Alcara 5854a5098d fix: carousel WhatsApp Web real-time rendering (no F5 needed)
Root cause: WhatsApp Web stores pkmsg as ciphertext first, then decrypts
to interactive. React does not re-render the ciphertext→interactive
transition, requiring F5. When enc type is msg (established session),
Web receives as interactive directly and renders in real-time.

Changes:
- messages-send.ts: re-enable biz node for carousel (CDP evidence from
  Pastorini confirms native_flow v=9 name=mixed + quality_control),
  LID-based addressing for session reuse, blocking tctoken fetch,
  force device-identity, skip bot node for carousel
- messages.ts: interactiveMessage direct (no viewOnceMessage wrapper),
  skip Message.create() for carousel (oneOf corruption), skip ephemeral
  contextInfo for carousel
- messages-media.ts: fix jimp type check (function not object)
- tc-token-utils.ts: dual storage under LID and PN for reliable lookup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 20:13:19 -03:00
Renato Alcara 20b5a4703d fix: skip biz node for carousel messages (fixes WhatsApp Web rendering)
Carousel messages must NOT include the biz XML node in the stanza.
The biz node causes WhatsApp Web to render only the header instead
of the full carousel with cards. Android and iOS are unaffected.

Confirmed via Frida capture of working implementation (Pastorini):
carousel stanza has NO biz node, NO bot node.

Change: add `&& !isCarousel` to biz node injection condition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:25:52 -03:00
18 changed files with 370 additions and 2144 deletions
+2 -75
View File
@@ -1,7 +1,7 @@
syntax = "proto3"; syntax = "proto3";
package proto; package proto;
/// WhatsApp Version: 2.3000.1035302375 /// WhatsApp Version: 2.3000.1034621774
message ADVDeviceIdentity { message ADVDeviceIdentity {
optional uint32 rawId = 1; optional uint32 rawId = 1;
@@ -345,14 +345,6 @@ 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 {
@@ -415,8 +407,6 @@ 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;
} }
} }
@@ -1084,7 +1074,6 @@ 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 {
@@ -1465,7 +1454,6 @@ 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;
@@ -1620,8 +1608,6 @@ 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;
@@ -1649,8 +1635,6 @@ 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 {
@@ -1920,8 +1904,6 @@ 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 {
@@ -1930,9 +1912,6 @@ 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 {
@@ -1940,7 +1919,6 @@ 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;
} }
} }
@@ -2297,7 +2275,6 @@ 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;
@@ -2578,16 +2555,6 @@ 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;
@@ -3114,10 +3081,9 @@ message Message {
message MessageHistoryMetadata { message MessageHistoryMetadata {
repeated string historyReceivers = 1; repeated string historyReceivers = 1;
optional int64 oldestMessageTimestampInWindow = 2; optional int64 oldestMessageTimestamp = 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 {
@@ -3178,11 +3144,6 @@ 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;
@@ -3604,7 +3565,6 @@ 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;
} }
} }
@@ -3637,7 +3597,6 @@ 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;
@@ -4165,8 +4124,6 @@ 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;
@@ -4635,7 +4592,6 @@ message StatusAttribution {
APPLE_MUSIC = 8; APPLE_MUSIC = 8;
SHARECHAT = 9; SHARECHAT = 9;
GOOGLE_PHOTOS = 10; GOOGLE_PHOTOS = 10;
SOUNDCLOUD = 11;
} }
} }
@@ -4804,8 +4760,6 @@ 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;
@@ -5321,29 +5275,6 @@ 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;
@@ -5355,10 +5286,6 @@ 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;
} }
+5 -201
View File
@@ -981,38 +981,6 @@ 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);
} }
@@ -1090,9 +1058,7 @@ 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
} }
} }
@@ -2803,7 +2769,6 @@ 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 {
@@ -2812,7 +2777,6 @@ 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;
@@ -3590,7 +3554,6 @@ 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 {
@@ -3622,7 +3585,6 @@ 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;
@@ -3887,8 +3849,6 @@ 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 {
@@ -3948,8 +3908,6 @@ 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;
@@ -4025,16 +3983,12 @@ 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;
@@ -4730,8 +4684,6 @@ 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 {
@@ -4739,8 +4691,6 @@ 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;
@@ -4756,9 +4706,6 @@ 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 {
@@ -4768,9 +4715,6 @@ 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;
@@ -4785,7 +4729,6 @@ 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 {
@@ -4794,7 +4737,6 @@ 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;
@@ -5604,7 +5546,6 @@ 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 {
@@ -5710,7 +5651,6 @@ 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;
@@ -6514,34 +6454,6 @@ 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);
@@ -7906,19 +7818,17 @@ export namespace proto {
interface IMessageHistoryMetadata { interface IMessageHistoryMetadata {
historyReceivers?: (string[]|null); historyReceivers?: (string[]|null);
oldestMessageTimestampInWindow?: (number|Long|null); oldestMessageTimestamp?: (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 oldestMessageTimestampInWindow?: (number|Long|null); public oldestMessageTimestamp?: (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;
@@ -8076,7 +7986,6 @@ 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 {
@@ -8085,7 +7994,6 @@ 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;
@@ -8097,11 +8005,6 @@ 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,
@@ -9267,8 +9170,7 @@ 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
} }
} }
@@ -9359,14 +9261,12 @@ 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;
@@ -10562,8 +10462,6 @@ 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
@@ -11847,8 +11745,7 @@ 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
} }
} }
@@ -12159,8 +12056,6 @@ 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 {
@@ -12239,8 +12134,6 @@ 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;
@@ -13773,79 +13666,6 @@ 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);
@@ -13884,22 +13704,6 @@ 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);
} }
+19 -1515
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.3", "music-metadata": "^11.12.0",
"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,1035317910]} {"version":[2,3000,1034641024]}
+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
} }
+18 -22
View File
@@ -25,30 +25,28 @@ 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( await Promise.all(metadata.participants.map(async (p) => {
metadata.participants.map(async p => { if (isLidUser(p.id)) {
if (isLidUser(p.id)) { if (p.phoneNumber) {
if (p.phoneNumber) { p.lid = p.id
p.id = p.phoneNumber
} else {
const resolved = await resolveLidToPn(p.id, lidMapping, logger)
if (resolved && resolved !== p.id) {
p.lid = p.id p.lid = p.id
p.id = p.phoneNumber p.id = resolved
} else {
const resolved = await resolveLidToPn(p.id, lidMapping, logger)
if (resolved && resolved !== p.id) {
p.lid = p.id
p.id = resolved
}
} }
} }
}) }
) }))
// 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
@@ -97,13 +95,11 @@ 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( const meta = await normalizeGroupMetadata(extractGroupMetadata({
extractGroupMetadata({ tag: 'result',
tag: 'result', attrs: {},
attrs: {}, content: [groupNode]
content: [groupNode] }))
})
)
data[meta.id] = meta data[meta.id] = meta
} }
} }
+233 -237
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,8 +430,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
break break
case 'participant': case 'participant':
const resolvedParticipantUser = const resolvedParticipantUser = await resolveLidToPn(child.attrs.jid!, signalRepository.lidMapping, logger) || child.attrs.jid!
(await resolveLidToPn(child.attrs.jid, signalRepository.lidMapping, logger)) || child.attrs.jid!
const participantUpdate = { const participantUpdate = {
id: from, id: from,
author, author,
@@ -574,7 +573,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) {
@@ -595,31 +594,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({ tag: 'device-identity', attrs: {}, content: undefined }) offerContent.push(
{ 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)
@@ -648,7 +647,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) {
@@ -664,15 +663,13 @@ 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)
@@ -686,18 +683,24 @@ 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 (callId: string, callFrom: string, isVideo?: boolean) => { const acceptCall = async (
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[] = [{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined }] const acceptContent: BinaryNode[] = [
{ 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
}) })
@@ -705,7 +708,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 = {
@@ -713,18 +716,16 @@ 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)
@@ -739,8 +740,14 @@ 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 (callId: string, callCreator: string, isVideo?: boolean) => { const preacceptCall = async (
const preacceptContent: BinaryNode[] = [{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined }] callId: string,
callCreator: string,
isVideo?: boolean,
) => {
const preacceptContent: BinaryNode[] = [
{ tag: 'audio', attrs: { rate: '16000', enc: 'opus' }, content: undefined },
]
if (isVideo) { if (isVideo) {
preacceptContent.push({ preacceptContent.push({
@@ -749,7 +756,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
}) })
@@ -757,25 +764,23 @@ 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)
@@ -801,11 +806,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) {
@@ -838,15 +843,13 @@ 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)
@@ -867,12 +870,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) {
@@ -882,22 +885,20 @@ 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)
@@ -918,27 +919,25 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
callCreator: string, callCreator: string,
peer: string, peer: string,
audioDuration: number, audioDuration: number,
callType = '1x1' callType: string = '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)
@@ -953,24 +952,27 @@ 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 (callId: string, callCreator: string, to: string, muted: boolean) => { const muteCall = async (
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)
@@ -983,23 +985,24 @@ 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 (callId: string, callCreator: string) => { const sendHeartbeat = async (
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)
@@ -1014,27 +1017,30 @@ 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 (callId: string, callCreator: string, to: string, transactionId: string) => { const sendEncRekey = async (
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)
@@ -1055,26 +1061,24 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
callCreator: string, callCreator: string,
to: string, to: string,
enabled: boolean, enabled: boolean,
orientation = '1' orientation: string = '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)
@@ -1096,17 +1100,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_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)
@@ -1134,7 +1136,9 @@ 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 ? `https://call.whatsapp.com/${token}` : undefined const url = token
? `https://call.whatsapp.com/${token}`
: undefined
return { token, url, response } return { token, url, response }
} }
@@ -1152,15 +1156,13 @@ 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)
@@ -1178,7 +1180,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') {
@@ -1188,7 +1190,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
}) })
@@ -1198,15 +1200,13 @@ 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,9 +1246,7 @@ 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 const senderJid = msgKey.participant ? jidNormalizedUser(msgKey.participant) : jidNormalizedUser(node.attrs.from!)
? 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)
@@ -1284,9 +1282,7 @@ 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 const senderJid = msgKey.participant ? jidNormalizedUser(msgKey.participant) : jidNormalizedUser(node.attrs.from!)
? 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)
@@ -1468,13 +1464,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()
} }
@@ -1520,20 +1516,16 @@ 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 || (await resolveLidToPn(actingParticipantLid, lidMapping, logger)) const actingParticipant = actingParticipantPn
|| await resolveLidToPn(actingParticipantLid, lidMapping, logger)
// Resolve affected participant to PN // Resolve affected participant to PN
const affectedParticipant = const affectedParticipant = affectedParticipantPn
affectedParticipantPn || (await resolveLidToPn(affectedParticipantLid, lidMapping, logger)) || 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 ( if (actingParticipantLid && actingParticipantPn && isLidUser(actingParticipantLid) && isPnUser(actingParticipantPn)) {
actingParticipantLid &&
actingParticipantPn &&
isLidUser(actingParticipantLid) &&
isPnUser(actingParticipantPn)
) {
mappingsToStore.push({ lid: actingParticipantLid, pn: actingParticipantPn }) mappingsToStore.push({ lid: actingParticipantLid, pn: actingParticipantPn })
} }
@@ -1562,7 +1554,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
} }
@@ -1596,7 +1588,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!
}) })
) )
@@ -1778,7 +1770,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,
@@ -1823,8 +1815,7 @@ 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 = const resolvedBlockJid = await resolveLidToPn(attrs.jid!, signalRepository.lidMapping, logger) || attrs.jid!
(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 })
@@ -2155,8 +2146,7 @@ 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 = const resolvedReceiptUserJid = await resolveLidToPn(attrs.participant, lidMapping, logger) || jidNormalizedUser(attrs.participant)
(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 => ({
@@ -2269,7 +2259,13 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
category, category,
author, author,
decrypt decrypt
} = decryptMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '', signalRepository, logger) } = decryptMessageNode(
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:
@@ -2684,7 +2680,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,
})) }))
} }
@@ -2772,7 +2768,9 @@ 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 ? Number(callSummary.attrs.call_duration) : undefined call.duration = callSummary.attrs.call_duration
? Number(callSummary.attrs.call_duration)
: undefined
call.participants = extractParticipants(callSummary) call.participants = extractParticipants(callSummary)
} }
} }
@@ -2816,14 +2814,12 @@ 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( await Promise.all(call.participants.map(async (p) => {
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])
@@ -2861,22 +2857,20 @@ 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(() => { .catch(() => { /* fire-and-forget */ })
/* 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.
@@ -2917,22 +2911,20 @@ 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(() => { .catch(() => { /* fire-and-forget */ })
/* fire-and-forget */
})
} else { } else {
logger.warn({ attrs }, 'received error in ack') logger.warn({ attrs }, 'received error in ack')
} }
@@ -3066,20 +3058,24 @@ 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({ callId, callCreator, uuid: node.attrs.uuid }, 'received relay info') logger.debug(
ev.emit('call', [ { callId, callCreator, uuid: node.attrs.uuid },
{ 'received relay info'
chatId: callCreator, )
from: callCreator, ev.emit('call', [{
id: callId, chatId: callCreator,
date: new Date(), from: callCreator,
offline: false, id: callId,
status: 'relay' as WACallUpdateType date: new Date(),
} offline: false,
]) status: 'relay' as WACallUpdateType,
}])
} else { } else {
logger.debug({ attrs: node.attrs }, 'received relay stanza without call-id/call-creator') logger.debug(
{ attrs: node.attrs },
'received relay stanza without call-id/call-creator'
)
} }
}) })
+18 -28
View File
@@ -1,6 +1,6 @@
import { randomBytes } from 'crypto'
import NodeCache from '@cacheable/node-cache' import NodeCache from '@cacheable/node-cache'
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import { 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 {
@@ -574,31 +574,6 @@ 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,
@@ -618,14 +593,29 @@ 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) : null const meLidUser = meLid ? jidDecode(meLid)?.user : 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
const msgToEncrypt = resolveDsmMessageForRecipient(jid, patchedMessage, dsmMessage, meId, meLid, meLidUser) let msgToEncrypt = patchedMessage
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
+8 -13
View File
@@ -1361,16 +1361,13 @@ 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({
@@ -1541,9 +1538,7 @@ 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( 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'}`)
`${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,7 +246,6 @@ 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 -4
View File
@@ -45,8 +45,8 @@ import {
generateThumbnail, generateThumbnail,
getAudioDuration, getAudioDuration,
getAudioWaveform, getAudioWaveform,
getRawMediaUploadData,
getStream, getStream,
getRawMediaUploadData,
type MediaDownloadOptions type MediaDownloadOptions
} from './messages-media' } from './messages-media'
import { shouldIncludeReportingToken } from './reporting-utils' import { shouldIncludeReportingToken } from './reporting-utils'
@@ -658,17 +658,14 @@ export const generateCarouselMessage = async (
const { stream } = await getStream(card.image, mediaOptions.options) const { stream } = await getStream(card.image, mediaOptions.options)
const { buffer, original } = await extractImageThumb(stream) const { buffer, original } = await extractImageThumb(stream)
// eslint-disable-next-line max-depth
if (!imageMessage.jpegThumbnail) { if (!imageMessage.jpegThumbnail) {
imageMessage.jpegThumbnail = buffer.toString('base64') imageMessage.jpegThumbnail = buffer.toString('base64')
} }
// eslint-disable-next-line max-depth
if (!imageMessage.width && original.width) { if (!imageMessage.width && original.width) {
imageMessage.width = original.width imageMessage.width = original.width
} }
// eslint-disable-next-line max-depth
if (!imageMessage.height && original.height) { if (!imageMessage.height && original.height) {
imageMessage.height = original.height imageMessage.height = original.height
} }
+2 -3
View File
@@ -1,7 +1,6 @@
/* 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,
@@ -20,6 +19,7 @@ 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,8 +784,7 @@ const processMessage = async (
}) })
} }
const participantsIncludesMe = () => const participantsIncludesMe = () => participants.find(p => areJidsSameUser(meId, p.id) || areJidsSameUser(meId, p.phoneNumber))
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:
+5 -4
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 { gunzipSync, gzipSync } from 'zlib' import { gzipSync, gunzipSync } from 'zlib'
import { proto } from '../../WAProto/index.js' import { proto } from '../../WAProto/index.js'
import type { MediaType } from '../Defaults/index.js' import type { MediaType } from '../Defaults/index.js'
import type { StickerPack, WAMediaUpload, WAMediaUploadFunction } from '../Types/Message.js' import type { StickerPack, WAMediaUpload, WAMediaUploadFunction } from '../Types/Message.js'
@@ -728,9 +728,10 @@ 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('Sharp library is required for cover/tray icon processing. Install with: yarn add sharp', { throw new Boom(
statusCode: 400 'Sharp library is required for cover/tray icon processing. Install with: yarn add sharp',
}) { statusCode: 400 }
)
} }
coverWebP = await lib.sharp coverWebP = await lib.sharp
+42 -17
View File
@@ -14,19 +14,24 @@ 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.WEB, platform: proto.ClientPayload.UserAgent.Platform.MACOS,
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
@@ -55,16 +60,20 @@ 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 { user, device } = jidDecode(userJid)! const decoded = 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,
@@ -79,6 +88,10 @@ 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
@@ -153,6 +166,9 @@ 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')
@@ -168,42 +184,51 @@ 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 = {
@@ -211,7 +236,7 @@ export const configureSuccessfulPairing = (
attrs: { attrs: {
to: S_WHATSAPP_NET, to: S_WHATSAPP_NET,
type: 'result', type: 'result',
id: msgId! id: msgId
}, },
content: [ content: [
{ {
@@ -230,7 +255,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
} }
+11 -18
View File
@@ -388,13 +388,6 @@ __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"
@@ -3112,7 +3105,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.3" music-metadata: "npm:^11.12.0"
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"
@@ -4563,15 +4556,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"file-type@npm:^21.3.1": "file-type@npm:^21.3.0":
version: 21.3.1 version: 21.3.0
resolution: "file-type@npm:21.3.1" resolution: "file-type@npm:21.3.0"
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/66a8eda781c803c6fc372464abba88cee564cfe30f029716f06909da1564bc51d0a4e8d34654b881dc751cdb0513338b3c0747e06a608cee0dd5c5499b018d47 checksum: 10c0/1b1fa909e6063044a6da1d2ea348ee4d747ed9286382d3f0d4d6532c11fb2ea9f2e7e67b2bc7d745d1bc937e05dee1aa8cb912c64250933bcb393a3744f4e284
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6511,21 +6504,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"music-metadata@npm:^11.12.3": "music-metadata@npm:^11.12.0":
version: 11.12.3 version: 11.12.0
resolution: "music-metadata@npm:11.12.3" resolution: "music-metadata@npm:11.12.0"
dependencies: dependencies:
"@borewit/text-codec": "npm:^0.2.2" "@borewit/text-codec": "npm:^0.2.1"
"@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.1" file-type: "npm:^21.3.0"
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/57df905f51ec792e5b4c994645eab64d47d5608b9de11151f54b26fbd5f563598ec04d8aed7f244ef1479030d78ddab8af1ee84dfcee40a93b3241941b50999d checksum: 10c0/014a120b8941ab80452171def0ec3cba013041ba4b94f8061ad493a037d3423e6d316131a13fb0433404685ebd3c8e3ae2255c1bafaeb20673fcd73af2df0148
languageName: node languageName: node
linkType: hard linkType: hard