Compare commits

..

5 Commits

Author SHA1 Message Date
Renato Alcara 3f4e83a8af fix: address codex/copilot review on view-once send/recv
P1 (messages-recv.ts): remove early return for <unavailable type="view_once"/>.
decryptMessageNode already handles this gracefully — decode-wa-message.ts sets
fullMessage.key.isViewOnce=true and skips the CIPHERTEXT stub (line 464).
The early return was preventing upsertMessage from emitting an event to consumers,
making view-once sends from the primary phone invisible on linked devices.

P2 (messages-send.ts): restrict isViewOnceMsg detection to actual view-once media.
Previously checked only for the presence of viewOnceMessage* wrappers, which are
also used by buttons/lists/carousels (non-view-once). Now unwraps the inner message
and checks imageMessage.viewOnce / videoMessage.viewOnce / audioMessage.viewOnce,
consistent with the detection in decode-wa-message.ts. Prevents companion DSM
filtering from incorrectly applying to interactive messages.

P3 (messages-send.ts): move assertSessions to use only the recipients actually
encrypted for. For view-once, omitted companions no longer cause assertSessions
to fail or do unnecessary session validation work.

Co-Authored-By: Renato Alcara
2026-03-23 00:35:10 -03:00
Renato Alcara d870ffb2a3 fix: handle view-once send/recv on linked devices
- messages-send.ts: for view-once messages, send DSM only to device=0
  (primary phone). Companion devices (device>0) are omitted from
  participants — the WA server generates <unavailable type="view_once"/>
  for them automatically. Sending explicit <unavailable> from a companion
  is rejected by the server and breaks delivery.
  phash is recalculated to match the actual participants sent.

- messages-recv.ts: add early return for <unavailable type="view_once"/>
  stanzas received by the companion (API). When the primary phone sends
  a view-once, linked companions receive this node with no enc content.
  Previously the code tried to decrypt it and crashed with a 500 error.

Co-Authored-By: Renato Alcara
2026-03-23 00:02:54 -03:00
Renato Alcara f6c72530a2 fix: unwrap viewOnceMessage in getMediaType so enc node gets mediatype attribute
Without this fix, view-once media (image/video/audio) was sent without
mediatype="image/video/audio" on the enc node because getMediaType only
checked the top-level message fields. The viewOnceMessage wrapper hides
the inner imageMessage, causing mediatype to be empty and WA servers to
silently drop the message on the recipient side.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 10:45:14 -03:00
Renato Alcara a4e7422487 fix: remove unnecessary non-null assertion on fullMessage.key
fullMessage.key is always present on WAMessage. Use consistent style
with stanza-2 handling at line 297.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 10:09:09 -03:00
Renato Alcara c6511d3034 fix: detect view-once media (image/video/audio) on stanza 1 for linked devices
When a view-once arrives at a linked device (InfiniteAPI), the server sends:
- Stanza 1: enc payload with full media metadata (mediaKey, directPath) wrapped in
  viewOnceMessage > imageMessage/videoMessage/audioMessage with viewOnce: true
- Stanza 2: unavailable view_once fanout placeholder (already handled)

Previously only stanza 2 set key.isViewOnce = true. Stanza 1 was emitted as
plain media with no view-once indicator, making it indistinguishable from regular
media on the consumer side (messages.upsert).

This fix inspects the decrypted proto for viewOnceMessage (v1/v2/v2Ext) wrappers
containing a media message with viewOnce=true and propagates key.isViewOnce=true.

The viewOnceMessage wrapper is also used for interactive messages (carousel,
buttons, lists) but those carry interactiveMessage/listMessage inside -- never
imageMessage.viewOnce / videoMessage.viewOnce / audioMessage.viewOnce.
The distinction is unambiguous and does not affect interactive message handling.

Verified via WA Desktop CDP capture (2026-03-19) and Android Frida DB capture
of view-once types 42/43/82 in msgstore.db (2026-03-20).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 09:35:26 -03:00
13 changed files with 486 additions and 3155 deletions
+2 -69
View File
@@ -1,7 +1,7 @@
syntax = "proto3"; syntax = "proto3";
package proto; package proto;
/// WhatsApp Version: 2.3000.1037162330 /// WhatsApp Version: 2.3000.1035577069
message ADVDeviceIdentity { message ADVDeviceIdentity {
optional uint32 rawId = 1; optional uint32 rawId = 1;
@@ -421,12 +421,6 @@ message BotCapabilityMetadata {
} }
} }
message BotCommandMetadata {
optional string commandName = 1;
optional string commandDescription = 2;
optional string commandPrompt = 3;
}
message BotDocumentMessageMetadata { message BotDocumentMessageMetadata {
optional DocumentPluginType pluginType = 1; optional DocumentPluginType pluginType = 1;
enum DocumentPluginType { enum DocumentPluginType {
@@ -658,7 +652,6 @@ message BotMetadata {
optional BotRenderingConfigMetadata botRenderingConfigMetadata = 36; optional BotRenderingConfigMetadata botRenderingConfigMetadata = 36;
optional BotInfrastructureDiagnostics botInfrastructureDiagnostics = 37; optional BotInfrastructureDiagnostics botInfrastructureDiagnostics = 37;
optional AIMediaCollectionMetadata aiMediaCollectionMetadata = 38; optional AIMediaCollectionMetadata aiMediaCollectionMetadata = 38;
optional BotCommandMetadata commandMetadata = 39;
optional bytes internalMetadata = 999; optional bytes internalMetadata = 999;
} }
@@ -710,7 +703,6 @@ enum BotMetricsEntryPoint {
WEB_NAVIGATION_BAR = 47; WEB_NAVIGATION_BAR = 47;
GROUP_MEMBER = 54; GROUP_MEMBER = 54;
CHATLIST_SEARCH = 55; CHATLIST_SEARCH = 55;
NEW_CHAT_LIST = 56;
} }
message BotMetricsMetadata { message BotMetricsMetadata {
optional string destinationId = 1; optional string destinationId = 1;
@@ -1475,10 +1467,6 @@ message ContextInfo {
optional string wtwaWebsiteUrl = 26; optional string wtwaWebsiteUrl = 26;
optional string adPreviewUrl = 27; optional string adPreviewUrl = 27;
optional bool containsCtwaFlowsAutoReply = 28; optional bool containsCtwaFlowsAutoReply = 28;
optional int32 agmThumbnailStrategy = 29;
optional int32 agmTitleStrategy = 30;
optional int32 agmSubtitleStrategy = 31;
optional int32 agmHeaderInteractionStrategy = 32;
enum AdType { enum AdType {
CTWA = 0; CTWA = 0;
CAWC = 1; CAWC = 1;
@@ -1664,7 +1652,6 @@ message DeviceCapabilities {
optional bool companionSupportEnabled = 2; optional bool companionSupportEnabled = 2;
optional bool campaignSyncEnabled = 3; optional bool campaignSyncEnabled = 3;
optional bool insightsSyncEnabled = 4; optional bool insightsSyncEnabled = 4;
optional int32 recipientLimit = 5;
} }
enum ChatLockSupportLevel { enum ChatLockSupportLevel {
@@ -1740,8 +1727,6 @@ message DeviceProps {
optional uint32 initialSyncMaxMessagesPerChat = 20; optional uint32 initialSyncMaxMessagesPerChat = 20;
optional bool supportManusHistory = 21; optional bool supportManusHistory = 21;
optional bool supportHatchHistory = 22; optional bool supportHatchHistory = 22;
repeated string supportedBotChannelFbids = 23;
optional bool supportInlineContacts = 24;
} }
enum PlatformType { enum PlatformType {
@@ -1928,16 +1913,6 @@ message GroupParticipant {
} }
} }
message GroupRootKeyShare {
repeated GroupRootKeyShareEntry keys = 1;
}
message GroupRootKeyShareEntry {
optional bytes groupRootKey = 1;
optional string keyId = 2;
optional int64 expiryTimestampMs = 3;
}
message HandshakeMessage { message HandshakeMessage {
optional ClientHello clientHello = 2; optional ClientHello clientHello = 2;
optional ServerHello serverHello = 3; optional ServerHello serverHello = 3;
@@ -1960,7 +1935,6 @@ message HandshakeMessage {
optional bool sendServerHelloPaddedBytes = 7; optional bool sendServerHelloPaddedBytes = 7;
optional bool simulateXxkemFs = 8; optional bool simulateXxkemFs = 8;
optional HandshakeMessage.HandshakePqMode pqMode = 9; optional HandshakeMessage.HandshakePqMode pqMode = 9;
optional bytes extendedEphemeral = 10;
} }
enum HandshakePqMode { enum HandshakePqMode {
@@ -1980,7 +1954,6 @@ message HandshakeMessage {
optional bytes payload = 3; optional bytes payload = 3;
optional bytes extendedStatic = 4; optional bytes extendedStatic = 4;
optional bytes paddingBytes = 5; optional bytes paddingBytes = 5;
optional bytes extendedCiphertext = 6;
} }
} }
@@ -2004,8 +1977,6 @@ message HistorySync {
optional bytes shareableChatIdentifierEncryptionKey = 17; optional bytes shareableChatIdentifierEncryptionKey = 17;
repeated Account accounts = 18; repeated Account accounts = 18;
optional bytes nctSalt = 19; optional bytes nctSalt = 19;
repeated InlineContact inlineContacts = 20;
optional bool inlineContactsProvided = 21;
enum BotAIWaitListState { enum BotAIWaitListState {
IN_WAITLIST = 0; IN_WAITLIST = 0;
AI_AVAILABLE = 1; AI_AVAILABLE = 1;
@@ -2100,14 +2071,6 @@ message InThreadSurveyMetadata {
} }
message InlineContact {
optional string pnJid = 1;
optional string lidJid = 2;
optional string fullName = 3;
optional string firstName = 4;
optional string username = 5;
}
message InteractiveAnnotation { message InteractiveAnnotation {
repeated Point polygonVertices = 1; repeated Point polygonVertices = 1;
optional bool shouldSkipConfirmation = 4; optional bool shouldSkipConfirmation = 4;
@@ -2347,7 +2310,6 @@ message Message {
optional ConditionalRevealMessage conditionalRevealMessage = 120; optional ConditionalRevealMessage conditionalRevealMessage = 120;
optional PollAddOptionMessage pollAddOptionMessage = 121; optional PollAddOptionMessage pollAddOptionMessage = 121;
optional EventInviteMessage eventInviteMessage = 122; optional EventInviteMessage eventInviteMessage = 122;
optional GroupRootKeyShare groupRootKeyShare = 123;
message AlbumMessage { message AlbumMessage {
optional uint32 expectedImageCount = 2; optional uint32 expectedImageCount = 2;
optional uint32 expectedVideoCount = 3; optional uint32 expectedVideoCount = 3;
@@ -2539,7 +2501,6 @@ message Message {
UNKNOWN = 0; UNKNOWN = 0;
CONTROL_PASSED = 1; CONTROL_PASSED = 1;
CONTROL_TAKEN = 2; CONTROL_TAKEN = 2;
INFO = 3;
} }
message CloudAPIThreadControlNotificationContent { message CloudAPIThreadControlNotificationContent {
optional string handoffNotificationText = 1; optional string handoffNotificationText = 1;
@@ -3321,7 +3282,6 @@ message Message {
optional int32 onDemandMsgCount = 4; optional int32 onDemandMsgCount = 4;
optional int64 oldestMsgTimestampMs = 5; optional int64 oldestMsgTimestampMs = 5;
optional string accountLid = 6; optional string accountLid = 6;
optional bool supportInlineResponse = 7;
} }
message PlaceholderMessageResendRequest { message PlaceholderMessageResendRequest {
@@ -3408,7 +3368,6 @@ message Message {
ERROR_REQUEST_ON_NON_SMB_PRIMARY = 4; ERROR_REQUEST_ON_NON_SMB_PRIMARY = 4;
ERROR_HOSTED_DEVICE_NOT_CONNECTED = 5; ERROR_HOSTED_DEVICE_NOT_CONNECTED = 5;
ERROR_HOSTED_DEVICE_LOGIN_TIME_NOT_SET = 6; ERROR_HOSTED_DEVICE_LOGIN_TIME_NOT_SET = 6;
ERROR_MULTI_PROVIDER_NOT_CONFIGURED = 7;
} }
message HistorySyncChunkRetryResponse { message HistorySyncChunkRetryResponse {
optional Message.HistorySyncType syncType = 1; optional Message.HistorySyncType syncType = 1;
@@ -3657,6 +3616,7 @@ message Message {
GROUP_MEMBER_LABEL_CHANGE = 30; GROUP_MEMBER_LABEL_CHANGE = 30;
AI_MEDIA_COLLECTION_MESSAGE = 31; AI_MEDIA_COLLECTION_MESSAGE = 31;
MESSAGE_UNSCHEDULE = 32; MESSAGE_UNSCHEDULE = 32;
BOT_UNLINK_MESSAGE = 33;
} }
} }
@@ -3807,7 +3767,6 @@ message Message {
optional bool isLottie = 21; optional bool isLottie = 21;
optional string accessibilityLabel = 22; optional string accessibilityLabel = 22;
optional int32 premium = 24; optional int32 premium = 24;
optional string emojis = 25;
} }
message StickerPackMessage { message StickerPackMessage {
@@ -4028,7 +3987,6 @@ message MessageContextInfo {
optional LimitSharing limitSharingV2 = 14; optional LimitSharing limitSharingV2 = 14;
repeated ThreadID threadId = 15; repeated ThreadID threadId = 15;
optional WebLinkRenderConfig weblinkRenderConfig = 16; optional WebLinkRenderConfig weblinkRenderConfig = 16;
optional bytes teeBotMetadata = 17;
enum MessageAddonExpiryType { enum MessageAddonExpiryType {
STATIC = 1; STATIC = 1;
DEPENDENT_ON_PARENT = 2; DEPENDENT_ON_PARENT = 2;
@@ -4534,12 +4492,6 @@ message ReportingTokenInfo {
optional bytes reportingTag = 1; optional bytes reportingTag = 1;
} }
message ScheduledMessageMetadata {
optional string revealKeyId = 1;
optional bytes revealKey = 2;
optional uint64 scheduledTime = 3;
}
message SenderKeyDistributionMessage { message SenderKeyDistributionMessage {
optional uint32 id = 1; optional uint32 id = 1;
optional uint32 iteration = 2; optional uint32 iteration = 2;
@@ -5070,7 +5022,6 @@ message SyncActionValue {
DRAFTED = 8; DRAFTED = 8;
AI_HANDOFF = 9; AI_HANDOFF = 9;
CHANNELS = 10; CHANNELS = 10;
AI_RESPONDING = 11;
} }
} }
@@ -5286,8 +5237,6 @@ message SyncActionValue {
optional bool isStatusNotificationEnabled = 29; optional bool isStatusNotificationEnabled = 29;
optional int32 statusNotificationToneId = 30; optional int32 statusNotificationToneId = 30;
optional bool shouldPlaySoundForCallNotification = 31; optional bool shouldPlaySoundForCallNotification = 31;
optional string chatThemeId = 32;
optional string colorSchemeId = 33;
enum DisplayMode { enum DisplayMode {
DISPLAY_MODE_UNKNOWN = 0; DISPLAY_MODE_UNKNOWN = 0;
ALWAYS = 1; ALWAYS = 1;
@@ -5332,8 +5281,6 @@ message SyncActionValue {
IS_STATUS_NOTIFICATION_ENABLED = 29; IS_STATUS_NOTIFICATION_ENABLED = 29;
STATUS_NOTIFICATION_TONE_ID = 30; STATUS_NOTIFICATION_TONE_ID = 30;
SHOULD_PLAY_SOUND_FOR_CALL_NOTIFICATION = 31; SHOULD_PLAY_SOUND_FOR_CALL_NOTIFICATION = 31;
CHAT_THEME_ID = 32;
COLOR_SCHEME_ID = 33;
} }
enum SettingPlatform { enum SettingPlatform {
PLATFORM_UNKNOWN = 0; PLATFORM_UNKNOWN = 0;
@@ -5357,21 +5304,11 @@ message SyncActionValue {
repeated string userJid = 2; repeated string userJid = 2;
optional bool shareToFB = 3; optional bool shareToFB = 3;
optional bool shareToIG = 4; optional bool shareToIG = 4;
repeated CustomList customLists = 5;
message CustomList {
optional string listId = 1;
optional string name = 2;
optional string emoji = 3;
optional bool isSelected = 4;
repeated string userJid = 5;
}
enum StatusDistributionMode { enum StatusDistributionMode {
ALLOW_LIST = 0; ALLOW_LIST = 0;
DENY_LIST = 1; DENY_LIST = 1;
CONTACTS = 2; CONTACTS = 2;
CLOSE_FRIENDS = 3; CLOSE_FRIENDS = 3;
CUSTOM_LIST = 4;
} }
} }
@@ -5758,8 +5695,6 @@ message WebMessageInfo {
optional QuarantinedMessage quarantinedMessage = 77; optional QuarantinedMessage quarantinedMessage = 77;
optional uint32 nonJidMentions = 78; optional uint32 nonJidMentions = 78;
optional string hsmTag = 79; optional string hsmTag = 79;
optional uint64 ephemeralExpirationTimestamp = 80;
optional ScheduledMessageMetadata scheduledMessageMetadata = 81;
enum BizPrivacyStatus { enum BizPrivacyStatus {
E2EE = 0; E2EE = 0;
FB = 2; FB = 2;
@@ -5999,8 +5934,6 @@ message WebMessageInfo {
GROUP_MEMBER_SHARE_GROUP_HISTORY_MODE = 221; GROUP_MEMBER_SHARE_GROUP_HISTORY_MODE = 221;
GROUP_OPEN_BOT_ADDED = 222; GROUP_OPEN_BOT_ADDED = 222;
GROUP_TEE_BOT_ADDED = 223; GROUP_TEE_BOT_ADDED = 223;
CONTACT_INFO = 224;
SCHEDULED_MESSAGE_CREATED = 225;
} }
} }
+9 -183
View File
@@ -1097,26 +1097,6 @@ export namespace proto {
} }
} }
interface IBotCommandMetadata {
commandName?: (string|null);
commandDescription?: (string|null);
commandPrompt?: (string|null);
}
class BotCommandMetadata implements IBotCommandMetadata {
constructor(p?: proto.IBotCommandMetadata);
public commandName?: (string|null);
public commandDescription?: (string|null);
public commandPrompt?: (string|null);
public static create(properties?: proto.IBotCommandMetadata): proto.BotCommandMetadata;
public static encode(m: proto.IBotCommandMetadata, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.BotCommandMetadata;
public static fromObject(d: { [k: string]: any }): proto.BotCommandMetadata;
public static toObject(m: proto.BotCommandMetadata, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IBotDocumentMessageMetadata { interface IBotDocumentMessageMetadata {
pluginType?: (proto.BotDocumentMessageMetadata.DocumentPluginType|null); pluginType?: (proto.BotDocumentMessageMetadata.DocumentPluginType|null);
} }
@@ -1701,7 +1681,6 @@ export namespace proto {
botRenderingConfigMetadata?: (proto.IBotRenderingConfigMetadata|null); botRenderingConfigMetadata?: (proto.IBotRenderingConfigMetadata|null);
botInfrastructureDiagnostics?: (proto.IBotInfrastructureDiagnostics|null); botInfrastructureDiagnostics?: (proto.IBotInfrastructureDiagnostics|null);
aiMediaCollectionMetadata?: (proto.IAIMediaCollectionMetadata|null); aiMediaCollectionMetadata?: (proto.IAIMediaCollectionMetadata|null);
commandMetadata?: (proto.IBotCommandMetadata|null);
internalMetadata?: (Uint8Array|null); internalMetadata?: (Uint8Array|null);
} }
@@ -1744,7 +1723,6 @@ export namespace proto {
public botRenderingConfigMetadata?: (proto.IBotRenderingConfigMetadata|null); public botRenderingConfigMetadata?: (proto.IBotRenderingConfigMetadata|null);
public botInfrastructureDiagnostics?: (proto.IBotInfrastructureDiagnostics|null); public botInfrastructureDiagnostics?: (proto.IBotInfrastructureDiagnostics|null);
public aiMediaCollectionMetadata?: (proto.IAIMediaCollectionMetadata|null); public aiMediaCollectionMetadata?: (proto.IAIMediaCollectionMetadata|null);
public commandMetadata?: (proto.IBotCommandMetadata|null);
public internalMetadata?: (Uint8Array|null); public internalMetadata?: (Uint8Array|null);
public static create(properties?: proto.IBotMetadata): proto.BotMetadata; public static create(properties?: proto.IBotMetadata): proto.BotMetadata;
public static encode(m: proto.IBotMetadata, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.IBotMetadata, w?: $protobuf.Writer): $protobuf.Writer;
@@ -1802,8 +1780,7 @@ export namespace proto {
WEB_INTRO_PANEL = 46, WEB_INTRO_PANEL = 46,
WEB_NAVIGATION_BAR = 47, WEB_NAVIGATION_BAR = 47,
GROUP_MEMBER = 54, GROUP_MEMBER = 54,
CHATLIST_SEARCH = 55, CHATLIST_SEARCH = 55
NEW_CHAT_LIST = 56
} }
interface IBotMetricsMetadata { interface IBotMetricsMetadata {
@@ -3615,10 +3592,6 @@ export namespace proto {
wtwaWebsiteUrl?: (string|null); wtwaWebsiteUrl?: (string|null);
adPreviewUrl?: (string|null); adPreviewUrl?: (string|null);
containsCtwaFlowsAutoReply?: (boolean|null); containsCtwaFlowsAutoReply?: (boolean|null);
agmThumbnailStrategy?: (number|null);
agmTitleStrategy?: (number|null);
agmSubtitleStrategy?: (number|null);
agmHeaderInteractionStrategy?: (number|null);
} }
class ExternalAdReplyInfo implements IExternalAdReplyInfo { class ExternalAdReplyInfo implements IExternalAdReplyInfo {
@@ -3651,10 +3624,6 @@ export namespace proto {
public wtwaWebsiteUrl?: (string|null); public wtwaWebsiteUrl?: (string|null);
public adPreviewUrl?: (string|null); public adPreviewUrl?: (string|null);
public containsCtwaFlowsAutoReply?: (boolean|null); public containsCtwaFlowsAutoReply?: (boolean|null);
public agmThumbnailStrategy?: (number|null);
public agmTitleStrategy?: (number|null);
public agmSubtitleStrategy?: (number|null);
public agmHeaderInteractionStrategy?: (number|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;
@@ -4059,7 +4028,6 @@ export namespace proto {
companionSupportEnabled?: (boolean|null); companionSupportEnabled?: (boolean|null);
campaignSyncEnabled?: (boolean|null); campaignSyncEnabled?: (boolean|null);
insightsSyncEnabled?: (boolean|null); insightsSyncEnabled?: (boolean|null);
recipientLimit?: (number|null);
} }
class BusinessBroadcast implements IBusinessBroadcast { class BusinessBroadcast implements IBusinessBroadcast {
@@ -4068,7 +4036,6 @@ export namespace proto {
public companionSupportEnabled?: (boolean|null); public companionSupportEnabled?: (boolean|null);
public campaignSyncEnabled?: (boolean|null); public campaignSyncEnabled?: (boolean|null);
public insightsSyncEnabled?: (boolean|null); public insightsSyncEnabled?: (boolean|null);
public recipientLimit?: (number|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;
@@ -4244,8 +4211,6 @@ export namespace proto {
initialSyncMaxMessagesPerChat?: (number|null); initialSyncMaxMessagesPerChat?: (number|null);
supportManusHistory?: (boolean|null); supportManusHistory?: (boolean|null);
supportHatchHistory?: (boolean|null); supportHatchHistory?: (boolean|null);
supportedBotChannelFbids?: (string[]|null);
supportInlineContacts?: (boolean|null);
} }
class HistorySyncConfig implements IHistorySyncConfig { class HistorySyncConfig implements IHistorySyncConfig {
@@ -4272,8 +4237,6 @@ export namespace proto {
public initialSyncMaxMessagesPerChat?: (number|null); public initialSyncMaxMessagesPerChat?: (number|null);
public supportManusHistory?: (boolean|null); public supportManusHistory?: (boolean|null);
public supportHatchHistory?: (boolean|null); public supportHatchHistory?: (boolean|null);
public supportedBotChannelFbids: string[];
public supportInlineContacts?: (boolean|null);
public static create(properties?: proto.DeviceProps.IHistorySyncConfig): proto.DeviceProps.HistorySyncConfig; public static create(properties?: proto.DeviceProps.IHistorySyncConfig): proto.DeviceProps.HistorySyncConfig;
public static encode(m: proto.DeviceProps.IHistorySyncConfig, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.DeviceProps.IHistorySyncConfig, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.DeviceProps.HistorySyncConfig; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.DeviceProps.HistorySyncConfig;
@@ -4742,42 +4705,6 @@ export namespace proto {
} }
} }
interface IGroupRootKeyShare {
keys?: (proto.IGroupRootKeyShareEntry[]|null);
}
class GroupRootKeyShare implements IGroupRootKeyShare {
constructor(p?: proto.IGroupRootKeyShare);
public keys: proto.IGroupRootKeyShareEntry[];
public static create(properties?: proto.IGroupRootKeyShare): proto.GroupRootKeyShare;
public static encode(m: proto.IGroupRootKeyShare, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.GroupRootKeyShare;
public static fromObject(d: { [k: string]: any }): proto.GroupRootKeyShare;
public static toObject(m: proto.GroupRootKeyShare, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IGroupRootKeyShareEntry {
groupRootKey?: (Uint8Array|null);
keyId?: (string|null);
expiryTimestampMs?: (number|Long|null);
}
class GroupRootKeyShareEntry implements IGroupRootKeyShareEntry {
constructor(p?: proto.IGroupRootKeyShareEntry);
public groupRootKey?: (Uint8Array|null);
public keyId?: (string|null);
public expiryTimestampMs?: (number|Long|null);
public static create(properties?: proto.IGroupRootKeyShareEntry): proto.GroupRootKeyShareEntry;
public static encode(m: proto.IGroupRootKeyShareEntry, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.GroupRootKeyShareEntry;
public static fromObject(d: { [k: string]: any }): proto.GroupRootKeyShareEntry;
public static toObject(m: proto.GroupRootKeyShareEntry, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IHandshakeMessage { interface IHandshakeMessage {
clientHello?: (proto.HandshakeMessage.IClientHello|null); clientHello?: (proto.HandshakeMessage.IClientHello|null);
serverHello?: (proto.HandshakeMessage.IServerHello|null); serverHello?: (proto.HandshakeMessage.IServerHello|null);
@@ -4834,7 +4761,6 @@ export namespace proto {
sendServerHelloPaddedBytes?: (boolean|null); sendServerHelloPaddedBytes?: (boolean|null);
simulateXxkemFs?: (boolean|null); simulateXxkemFs?: (boolean|null);
pqMode?: (proto.HandshakeMessage.HandshakePqMode|null); pqMode?: (proto.HandshakeMessage.HandshakePqMode|null);
extendedEphemeral?: (Uint8Array|null);
} }
class ClientHello implements IClientHello { class ClientHello implements IClientHello {
@@ -4848,7 +4774,6 @@ export namespace proto {
public sendServerHelloPaddedBytes?: (boolean|null); public sendServerHelloPaddedBytes?: (boolean|null);
public simulateXxkemFs?: (boolean|null); public simulateXxkemFs?: (boolean|null);
public pqMode?: (proto.HandshakeMessage.HandshakePqMode|null); public pqMode?: (proto.HandshakeMessage.HandshakePqMode|null);
public extendedEphemeral?: (Uint8Array|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;
@@ -4876,7 +4801,6 @@ export namespace proto {
payload?: (Uint8Array|null); payload?: (Uint8Array|null);
extendedStatic?: (Uint8Array|null); extendedStatic?: (Uint8Array|null);
paddingBytes?: (Uint8Array|null); paddingBytes?: (Uint8Array|null);
extendedCiphertext?: (Uint8Array|null);
} }
class ServerHello implements IServerHello { class ServerHello implements IServerHello {
@@ -4886,7 +4810,6 @@ export namespace proto {
public payload?: (Uint8Array|null); public payload?: (Uint8Array|null);
public extendedStatic?: (Uint8Array|null); public extendedStatic?: (Uint8Array|null);
public paddingBytes?: (Uint8Array|null); public paddingBytes?: (Uint8Array|null);
public extendedCiphertext?: (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;
@@ -4916,8 +4839,6 @@ export namespace proto {
shareableChatIdentifierEncryptionKey?: (Uint8Array|null); shareableChatIdentifierEncryptionKey?: (Uint8Array|null);
accounts?: (proto.IAccount[]|null); accounts?: (proto.IAccount[]|null);
nctSalt?: (Uint8Array|null); nctSalt?: (Uint8Array|null);
inlineContacts?: (proto.IInlineContact[]|null);
inlineContactsProvided?: (boolean|null);
} }
class HistorySync implements IHistorySync { class HistorySync implements IHistorySync {
@@ -4940,8 +4861,6 @@ export namespace proto {
public shareableChatIdentifierEncryptionKey?: (Uint8Array|null); public shareableChatIdentifierEncryptionKey?: (Uint8Array|null);
public accounts: proto.IAccount[]; public accounts: proto.IAccount[];
public nctSalt?: (Uint8Array|null); public nctSalt?: (Uint8Array|null);
public inlineContacts: proto.IInlineContact[];
public inlineContactsProvided?: (boolean|null);
public static create(properties?: proto.IHistorySync): proto.HistorySync; public static create(properties?: proto.IHistorySync): proto.HistorySync;
public static encode(m: proto.IHistorySync, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.IHistorySync, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.HistorySync; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.HistorySync;
@@ -5209,30 +5128,6 @@ export namespace proto {
} }
} }
interface IInlineContact {
pnJid?: (string|null);
lidJid?: (string|null);
fullName?: (string|null);
firstName?: (string|null);
username?: (string|null);
}
class InlineContact implements IInlineContact {
constructor(p?: proto.IInlineContact);
public pnJid?: (string|null);
public lidJid?: (string|null);
public fullName?: (string|null);
public firstName?: (string|null);
public username?: (string|null);
public static create(properties?: proto.IInlineContact): proto.InlineContact;
public static encode(m: proto.IInlineContact, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.InlineContact;
public static fromObject(d: { [k: string]: any }): proto.InlineContact;
public static toObject(m: proto.InlineContact, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface IInteractiveAnnotation { interface IInteractiveAnnotation {
polygonVertices?: (proto.IPoint[]|null); polygonVertices?: (proto.IPoint[]|null);
shouldSkipConfirmation?: (boolean|null); shouldSkipConfirmation?: (boolean|null);
@@ -5724,7 +5619,6 @@ export namespace proto {
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); eventInviteMessage?: (proto.Message.IEventInviteMessage|null);
groupRootKeyShare?: (proto.IGroupRootKeyShare|null);
} }
class Message implements IMessage { class Message implements IMessage {
@@ -5831,7 +5725,6 @@ export namespace proto {
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 eventInviteMessage?: (proto.Message.IEventInviteMessage|null);
public groupRootKeyShare?: (proto.IGroupRootKeyShare|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;
@@ -6371,8 +6264,7 @@ export namespace proto {
enum CloudAPIThreadControl { enum CloudAPIThreadControl {
UNKNOWN = 0, UNKNOWN = 0,
CONTROL_PASSED = 1, CONTROL_PASSED = 1,
CONTROL_TAKEN = 2, CONTROL_TAKEN = 2
INFO = 3
} }
interface ICloudAPIThreadControlNotificationContent { interface ICloudAPIThreadControlNotificationContent {
@@ -8480,7 +8372,6 @@ export namespace proto {
onDemandMsgCount?: (number|null); onDemandMsgCount?: (number|null);
oldestMsgTimestampMs?: (number|Long|null); oldestMsgTimestampMs?: (number|Long|null);
accountLid?: (string|null); accountLid?: (string|null);
supportInlineResponse?: (boolean|null);
} }
class HistorySyncOnDemandRequest implements IHistorySyncOnDemandRequest { class HistorySyncOnDemandRequest implements IHistorySyncOnDemandRequest {
@@ -8491,7 +8382,6 @@ export namespace proto {
public onDemandMsgCount?: (number|null); public onDemandMsgCount?: (number|null);
public oldestMsgTimestampMs?: (number|Long|null); public oldestMsgTimestampMs?: (number|Long|null);
public accountLid?: (string|null); public accountLid?: (string|null);
public supportInlineResponse?: (boolean|null);
public static create(properties?: proto.Message.PeerDataOperationRequestMessage.IHistorySyncOnDemandRequest): proto.Message.PeerDataOperationRequestMessage.HistorySyncOnDemandRequest; public static create(properties?: proto.Message.PeerDataOperationRequestMessage.IHistorySyncOnDemandRequest): proto.Message.PeerDataOperationRequestMessage.HistorySyncOnDemandRequest;
public static encode(m: proto.Message.PeerDataOperationRequestMessage.IHistorySyncOnDemandRequest, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.Message.PeerDataOperationRequestMessage.IHistorySyncOnDemandRequest, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.PeerDataOperationRequestMessage.HistorySyncOnDemandRequest; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.PeerDataOperationRequestMessage.HistorySyncOnDemandRequest;
@@ -8765,8 +8655,7 @@ export namespace proto {
GENERIC_ERROR = 3, GENERIC_ERROR = 3,
ERROR_REQUEST_ON_NON_SMB_PRIMARY = 4, ERROR_REQUEST_ON_NON_SMB_PRIMARY = 4,
ERROR_HOSTED_DEVICE_NOT_CONNECTED = 5, ERROR_HOSTED_DEVICE_NOT_CONNECTED = 5,
ERROR_HOSTED_DEVICE_LOGIN_TIME_NOT_SET = 6, ERROR_HOSTED_DEVICE_LOGIN_TIME_NOT_SET = 6
ERROR_MULTI_PROVIDER_NOT_CONFIGURED = 7
} }
interface IHistorySyncChunkRetryResponse { interface IHistorySyncChunkRetryResponse {
@@ -9392,7 +9281,8 @@ export namespace proto {
AI_QUERY_FANOUT = 29, AI_QUERY_FANOUT = 29,
GROUP_MEMBER_LABEL_CHANGE = 30, GROUP_MEMBER_LABEL_CHANGE = 30,
AI_MEDIA_COLLECTION_MESSAGE = 31, AI_MEDIA_COLLECTION_MESSAGE = 31,
MESSAGE_UNSCHEDULE = 32 MESSAGE_UNSCHEDULE = 32,
BOT_UNLINK_MESSAGE = 33
} }
} }
@@ -9771,7 +9661,6 @@ export namespace proto {
isLottie?: (boolean|null); isLottie?: (boolean|null);
accessibilityLabel?: (string|null); accessibilityLabel?: (string|null);
premium?: (number|null); premium?: (number|null);
emojis?: (string|null);
} }
class StickerMessage implements IStickerMessage { class StickerMessage implements IStickerMessage {
@@ -9797,7 +9686,6 @@ export namespace proto {
public isLottie?: (boolean|null); public isLottie?: (boolean|null);
public accessibilityLabel?: (string|null); public accessibilityLabel?: (string|null);
public premium?: (number|null); public premium?: (number|null);
public emojis?: (string|null);
public static create(properties?: proto.Message.IStickerMessage): proto.Message.StickerMessage; public static create(properties?: proto.Message.IStickerMessage): proto.Message.StickerMessage;
public static encode(m: proto.Message.IStickerMessage, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.Message.IStickerMessage, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.StickerMessage; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.Message.StickerMessage;
@@ -10293,7 +10181,6 @@ export namespace proto {
limitSharingV2?: (proto.ILimitSharing|null); limitSharingV2?: (proto.ILimitSharing|null);
threadId?: (proto.IThreadID[]|null); threadId?: (proto.IThreadID[]|null);
weblinkRenderConfig?: (proto.WebLinkRenderConfig|null); weblinkRenderConfig?: (proto.WebLinkRenderConfig|null);
teeBotMetadata?: (Uint8Array|null);
} }
class MessageContextInfo implements IMessageContextInfo { class MessageContextInfo implements IMessageContextInfo {
@@ -10314,7 +10201,6 @@ export namespace proto {
public limitSharingV2?: (proto.ILimitSharing|null); public limitSharingV2?: (proto.ILimitSharing|null);
public threadId: proto.IThreadID[]; public threadId: proto.IThreadID[];
public weblinkRenderConfig?: (proto.WebLinkRenderConfig|null); public weblinkRenderConfig?: (proto.WebLinkRenderConfig|null);
public teeBotMetadata?: (Uint8Array|null);
public static create(properties?: proto.IMessageContextInfo): proto.MessageContextInfo; public static create(properties?: proto.IMessageContextInfo): proto.MessageContextInfo;
public static encode(m: proto.IMessageContextInfo, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.IMessageContextInfo, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.MessageContextInfo; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.MessageContextInfo;
@@ -11503,26 +11389,6 @@ export namespace proto {
public static getTypeUrl(typeUrlPrefix?: string): string; public static getTypeUrl(typeUrlPrefix?: string): string;
} }
interface IScheduledMessageMetadata {
revealKeyId?: (string|null);
revealKey?: (Uint8Array|null);
scheduledTime?: (number|Long|null);
}
class ScheduledMessageMetadata implements IScheduledMessageMetadata {
constructor(p?: proto.IScheduledMessageMetadata);
public revealKeyId?: (string|null);
public revealKey?: (Uint8Array|null);
public scheduledTime?: (number|Long|null);
public static create(properties?: proto.IScheduledMessageMetadata): proto.ScheduledMessageMetadata;
public static encode(m: proto.IScheduledMessageMetadata, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.ScheduledMessageMetadata;
public static fromObject(d: { [k: string]: any }): proto.ScheduledMessageMetadata;
public static toObject(m: proto.ScheduledMessageMetadata, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
interface ISenderKeyDistributionMessage { interface ISenderKeyDistributionMessage {
id?: (number|null); id?: (number|null);
iteration?: (number|null); iteration?: (number|null);
@@ -13054,8 +12920,7 @@ export namespace proto {
SERVER_ASSIGNED = 7, SERVER_ASSIGNED = 7,
DRAFTED = 8, DRAFTED = 8,
AI_HANDOFF = 9, AI_HANDOFF = 9,
CHANNELS = 10, CHANNELS = 10
AI_RESPONDING = 11
} }
} }
@@ -13697,8 +13562,6 @@ export namespace proto {
isStatusNotificationEnabled?: (boolean|null); isStatusNotificationEnabled?: (boolean|null);
statusNotificationToneId?: (number|null); statusNotificationToneId?: (number|null);
shouldPlaySoundForCallNotification?: (boolean|null); shouldPlaySoundForCallNotification?: (boolean|null);
chatThemeId?: (string|null);
colorSchemeId?: (string|null);
} }
class SettingsSyncAction implements ISettingsSyncAction { class SettingsSyncAction implements ISettingsSyncAction {
@@ -13734,8 +13597,6 @@ export namespace proto {
public isStatusNotificationEnabled?: (boolean|null); public isStatusNotificationEnabled?: (boolean|null);
public statusNotificationToneId?: (number|null); public statusNotificationToneId?: (number|null);
public shouldPlaySoundForCallNotification?: (boolean|null); public shouldPlaySoundForCallNotification?: (boolean|null);
public chatThemeId?: (string|null);
public colorSchemeId?: (string|null);
public static create(properties?: proto.SyncActionValue.ISettingsSyncAction): proto.SyncActionValue.SettingsSyncAction; public static create(properties?: proto.SyncActionValue.ISettingsSyncAction): proto.SyncActionValue.SettingsSyncAction;
public static encode(m: proto.SyncActionValue.ISettingsSyncAction, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.SyncActionValue.ISettingsSyncAction, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.SettingsSyncAction; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.SettingsSyncAction;
@@ -13792,9 +13653,7 @@ export namespace proto {
IS_GROUP_REACTIONS_NOTIFICATION_ENABLED = 28, IS_GROUP_REACTIONS_NOTIFICATION_ENABLED = 28,
IS_STATUS_NOTIFICATION_ENABLED = 29, IS_STATUS_NOTIFICATION_ENABLED = 29,
STATUS_NOTIFICATION_TONE_ID = 30, STATUS_NOTIFICATION_TONE_ID = 30,
SHOULD_PLAY_SOUND_FOR_CALL_NOTIFICATION = 31, SHOULD_PLAY_SOUND_FOR_CALL_NOTIFICATION = 31
CHAT_THEME_ID = 32,
COLOR_SCHEME_ID = 33
} }
enum SettingPlatform { enum SettingPlatform {
@@ -13843,7 +13702,6 @@ export namespace proto {
userJid?: (string[]|null); userJid?: (string[]|null);
shareToFB?: (boolean|null); shareToFB?: (boolean|null);
shareToIG?: (boolean|null); shareToIG?: (boolean|null);
customLists?: (proto.SyncActionValue.StatusPrivacyAction.ICustomList[]|null);
} }
class StatusPrivacyAction implements IStatusPrivacyAction { class StatusPrivacyAction implements IStatusPrivacyAction {
@@ -13852,7 +13710,6 @@ export namespace proto {
public userJid: string[]; public userJid: string[];
public shareToFB?: (boolean|null); public shareToFB?: (boolean|null);
public shareToIG?: (boolean|null); public shareToIG?: (boolean|null);
public customLists: proto.SyncActionValue.StatusPrivacyAction.ICustomList[];
public static create(properties?: proto.SyncActionValue.IStatusPrivacyAction): proto.SyncActionValue.StatusPrivacyAction; public static create(properties?: proto.SyncActionValue.IStatusPrivacyAction): proto.SyncActionValue.StatusPrivacyAction;
public static encode(m: proto.SyncActionValue.IStatusPrivacyAction, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.SyncActionValue.IStatusPrivacyAction, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.StatusPrivacyAction; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.StatusPrivacyAction;
@@ -13864,36 +13721,11 @@ export namespace proto {
namespace StatusPrivacyAction { namespace StatusPrivacyAction {
interface ICustomList {
listId?: (string|null);
name?: (string|null);
emoji?: (string|null);
isSelected?: (boolean|null);
userJid?: (string[]|null);
}
class CustomList implements ICustomList {
constructor(p?: proto.SyncActionValue.StatusPrivacyAction.ICustomList);
public listId?: (string|null);
public name?: (string|null);
public emoji?: (string|null);
public isSelected?: (boolean|null);
public userJid: string[];
public static create(properties?: proto.SyncActionValue.StatusPrivacyAction.ICustomList): proto.SyncActionValue.StatusPrivacyAction.CustomList;
public static encode(m: proto.SyncActionValue.StatusPrivacyAction.ICustomList, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.StatusPrivacyAction.CustomList;
public static fromObject(d: { [k: string]: any }): proto.SyncActionValue.StatusPrivacyAction.CustomList;
public static toObject(m: proto.SyncActionValue.StatusPrivacyAction.CustomList, o?: $protobuf.IConversionOptions): { [k: string]: any };
public toJSON(): { [k: string]: any };
public static getTypeUrl(typeUrlPrefix?: string): string;
}
enum StatusDistributionMode { enum StatusDistributionMode {
ALLOW_LIST = 0, ALLOW_LIST = 0,
DENY_LIST = 1, DENY_LIST = 1,
CONTACTS = 2, CONTACTS = 2,
CLOSE_FRIENDS = 3, CLOSE_FRIENDS = 3
CUSTOM_LIST = 4
} }
} }
@@ -14901,8 +14733,6 @@ export namespace proto {
quarantinedMessage?: (proto.IQuarantinedMessage|null); quarantinedMessage?: (proto.IQuarantinedMessage|null);
nonJidMentions?: (number|null); nonJidMentions?: (number|null);
hsmTag?: (string|null); hsmTag?: (string|null);
ephemeralExpirationTimestamp?: (number|Long|null);
scheduledMessageMetadata?: (proto.IScheduledMessageMetadata|null);
} }
class WebMessageInfo implements IWebMessageInfo { class WebMessageInfo implements IWebMessageInfo {
@@ -14975,8 +14805,6 @@ export namespace proto {
public quarantinedMessage?: (proto.IQuarantinedMessage|null); public quarantinedMessage?: (proto.IQuarantinedMessage|null);
public nonJidMentions?: (number|null); public nonJidMentions?: (number|null);
public hsmTag?: (string|null); public hsmTag?: (string|null);
public ephemeralExpirationTimestamp?: (number|Long|null);
public scheduledMessageMetadata?: (proto.IScheduledMessageMetadata|null);
public static create(properties?: proto.IWebMessageInfo): proto.WebMessageInfo; public static create(properties?: proto.IWebMessageInfo): proto.WebMessageInfo;
public static encode(m: proto.IWebMessageInfo, w?: $protobuf.Writer): $protobuf.Writer; public static encode(m: proto.IWebMessageInfo, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.WebMessageInfo; public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.WebMessageInfo;
@@ -15228,9 +15056,7 @@ export namespace proto {
QUARANTINED_MESSAGE = 220, QUARANTINED_MESSAGE = 220,
GROUP_MEMBER_SHARE_GROUP_HISTORY_MODE = 221, GROUP_MEMBER_SHARE_GROUP_HISTORY_MODE = 221,
GROUP_OPEN_BOT_ADDED = 222, GROUP_OPEN_BOT_ADDED = 222,
GROUP_TEE_BOT_ADDED = 223, GROUP_TEE_BOT_ADDED = 223
CONTACT_INFO = 224,
SCHEDULED_MESSAGE_CREATED = 225
} }
} }
+5 -1395
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-531
View File
@@ -1,531 +0,0 @@
# Mensagens View-Once (Visualização Única) — Guia de Implementação
**Data:** 2026-03-23
**Versão:** 1.0
---
## 1. O que é View-Once
Mensagem de visualização única é um tipo especial de mídia (imagem, vídeo ou áudio) que o destinatário pode abrir apenas uma vez. Após abrir, o conteúdo some permanentemente.
---
> ## ⚠️ RESTRIÇÃO CRÍTICA — Contas Hosted (Meta Business Cloud API)
>
> **Contas registradas como "hosted" na Meta Business Cloud API têm view-once bloqueado pelo servidor do WhatsApp.**
>
> Essa restrição se aplica a **todos os clientes** (Android, iOS, WA Web) e **não há como contornar por código** — é uma política imposta diretamente pelo servidor WA para contas hosted.
>
> **Como identificar se sua conta é hosted:**
> - A conta foi criada via Meta Business Cloud API (WABA)
> - Aparece como "Meta" ou "Business Cloud" nas configurações do WhatsApp Business Manager
>
> **Contas não-hosted** (registradas diretamente via QR code ou pair code) **funcionam normalmente** com view-once.
---
**Comportamento em cada dispositivo após envio via API (contas não-hosted):**
| Dispositivo | O que aparece |
|---|---|
| Destinatário (Android/iOS) | Recebe a mídia com ícone de olhinho — pode abrir uma vez |
| Android principal do remetente | Mostra na conversa que a mensagem foi enviada (ícone de olhinho) |
| WA Web do remetente | "Aguardando mensagem. Abra o WhatsApp no seu celular." |
> **Nota:** A mensagem "Aguardando mensagem..." no WA Web é o comportamento correto e esperado quando o envio vem de um companion (API). É uma limitação do protocolo WhatsApp — o servidor só permite que o celular principal notifique companions com o conteúdo da view-once.
---
## 2. Como Enviar via API
### 2.1 Imagem
```bash
curl -X POST https://sua-api.com/v1/messages/send_image \
-H "Content-Type: application/json" \
-H "x-api-key: SEU_API_KEY" \
-d '{
"instance": "nome-da-instancia",
"to": "5511999999999",
"caption": "Olha só isso!",
"imageUrl": "https://exemplo.com/foto.jpg",
"viewOnce": true
}'
```
**Resposta de sucesso:**
```json
{ "ok": true }
```
### 2.2 Vídeo
```bash
curl -X POST https://sua-api.com/v1/messages/send_video \
-H "Content-Type: application/json" \
-H "x-api-key: SEU_API_KEY" \
-d '{
"instance": "nome-da-instancia",
"to": "5511999999999",
"caption": "Assista uma vez",
"videoUrl": "https://exemplo.com/video.mp4",
"viewOnce": true
}'
```
### 2.3 Áudio
```bash
curl -X POST https://sua-api.com/v1/messages/send_audio \
-H "Content-Type: application/json" \
-H "x-api-key: SEU_API_KEY" \
-d '{
"instance": "nome-da-instancia",
"to": "5511999999999",
"audioUrl": "https://exemplo.com/audio.ogg",
"ptt": false,
"viewOnce": true
}'
```
### 2.4 Usando Base64 (sem URL pública)
```bash
curl -X POST https://sua-api.com/v1/messages/send_image \
-H "Content-Type: application/json" \
-H "x-api-key: SEU_API_KEY" \
-d '{
"instance": "nome-da-instancia",
"to": "5511999999999",
"imageBase64": "data:image/jpeg;base64,/9j/4AAQSkZJRgAB...",
"viewOnce": true
}'
```
---
## 3. Como Receber via Webhook
Quando um contato envia uma mensagem de visualização única para o número conectado na API, o webhook recebe o evento com `key.isViewOnce = true`.
### 3.1 Estrutura do evento recebido
```json
{
"event": "messages.upsert",
"instance": "nome-da-instancia",
"data": {
"messages": [
{
"key": {
"remoteJid": "5511999999999@s.whatsapp.net",
"fromMe": false,
"id": "3EB0ABCDEF123456",
"isViewOnce": true
},
"message": {
"viewOnceMessageV2": {
"message": {
"imageMessage": {
"url": "https://mmg.whatsapp.net/...",
"mimetype": "image/jpeg",
"mediaKey": "base64...",
"fileEncSha256": "base64...",
"directPath": "/v/...",
"viewOnce": true
}
}
}
},
"messageTimestamp": 1742700000,
"pushName": "João Silva"
}
],
"type": "notify"
}
}
```
### 3.2 Como identificar que é view-once
O campo principal para identificar é `key.isViewOnce = true`. Existem dois caminhos para confirmar:
```javascript
function isViewOnce(msg) {
// Caminho 1: flag direta (mais simples)
if (msg.key?.isViewOnce) return true
// Caminho 2: verificar o wrapper da mensagem
const inner =
msg.message?.viewOnceMessageV2?.message ||
msg.message?.viewOnceMessage?.message ||
msg.message?.viewOnceMessageV2Extension?.message
return !!(
inner?.imageMessage?.viewOnce ||
inner?.videoMessage?.viewOnce ||
inner?.audioMessage?.viewOnce
)
}
```
### 3.3 Como acessar a mídia
```javascript
function getViewOnceMedia(msg) {
const inner =
msg.message?.viewOnceMessageV2?.message ||
msg.message?.viewOnceMessage?.message ||
msg.message?.viewOnceMessageV2Extension?.message
if (!inner) return null
if (inner.imageMessage) return { type: 'image', media: inner.imageMessage }
if (inner.videoMessage) return { type: 'video', media: inner.videoMessage }
if (inner.audioMessage) return { type: 'audio', media: inner.audioMessage }
return null
}
// Uso:
const media = getViewOnceMedia(msg)
if (media) {
console.log(`View-once ${media.type}`)
console.log('URL:', media.media.url)
console.log('DirectPath:', media.media.directPath)
console.log('MediaKey:', media.media.mediaKey) // necessário para download
}
```
---
## 4. Como Exibir numa Ferramenta (Frontend/CRM)
### 4.1 Lógica de detecção e exibição
```javascript
function renderMessage(msg) {
if (!isViewOnce(msg)) {
// Renderizar mensagem normal
return renderNormalMessage(msg)
}
const media = getViewOnceMedia(msg)
if (!media) return
// Verificar se já foi aberta (controle local da ferramenta)
const alreadyOpened = localStorage.getItem(`vo_${msg.key.id}`)
if (alreadyOpened) {
// Mostrar placeholder de "já visualizado"
return renderViewOncePlaceholder(media.type, msg.key.fromMe)
}
if (msg.key.fromMe) {
// Mensagem enviada por mim — nunca mostrar conteúdo
return renderSentViewOnce(media.type)
}
// Mensagem recebida — mostrar botão para "abrir uma vez"
return renderViewOnceButton(media.type, msg)
}
```
### 4.2 Componente de exibição — mensagem recebida
```javascript
function renderViewOnceButton(type, msg) {
const icons = { image: '📷', video: '🎥', audio: '🎵' }
const labels = { image: 'Foto', video: 'Vídeo', áudio: 'Áudio' }
return `
<div class="view-once-bubble received">
<span class="view-once-icon">${icons[type]}</span>
<span class="view-once-label">${labels[type]} de visualização única</span>
<button onclick="openViewOnce('${msg.key.id}', '${type}')">
Abrir
</button>
</div>
`
}
async function openViewOnce(msgId, type) {
// 1. Marcar como aberta ANTES de mostrar (só uma chance)
localStorage.setItem(`vo_${msgId}`, '1')
// 2. Baixar e mostrar a mídia
const mediaUrl = await downloadViewOnceMedia(msgId)
showMedia(type, mediaUrl)
// 3. Após fechar/visualizar, substituir pelo placeholder
onMediaClosed(() => {
replaceWithPlaceholder(msgId, type)
})
}
```
### 4.3 Componente de exibição — mensagem enviada
```javascript
function renderSentViewOnce(type) {
const labels = { image: 'Foto', video: 'Vídeo', audio: 'Áudio' }
return `
<div class="view-once-bubble sent">
<span class="view-once-icon">👁️</span>
<span>${labels[type]} de visualização única</span>
<span class="view-once-status">Enviada</span>
</div>
`
}
```
### 4.4 Placeholder após visualização
```javascript
function renderViewOncePlaceholder(type, fromMe) {
const labels = { image: 'foto', video: 'vídeo', audio: 'áudio' }
const text = fromMe
? `Você enviou uma ${labels[type]} de visualização única`
: `${labels[type]} de visualização única aberta`
return `
<div class="view-once-bubble placeholder">
<span class="view-once-icon">🚫</span>
<span>${text}</span>
</div>
`
}
```
### 4.5 CSS sugerido
```css
.view-once-bubble {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-radius: 12px;
max-width: 280px;
}
.view-once-bubble.received {
background: #f0f0f0;
color: #333;
}
.view-once-bubble.sent {
background: #dcf8c6;
color: #333;
align-self: flex-end;
}
.view-once-bubble.placeholder {
background: #e8e8e8;
color: #999;
font-style: italic;
}
.view-once-bubble button {
background: #25d366;
color: white;
border: none;
border-radius: 8px;
padding: 6px 12px;
cursor: pointer;
font-size: 13px;
}
```
---
## 5. Código Implementado na API
### 5.1 Geração da mensagem — `src/Utils/messages.ts`
Quando `viewOnce: true` é passado, a mídia é encapsulada no wrapper moderno `viewOnceMessageV2` (campo 55 do proto) e o flag `viewOnce=true` é setado na mídia interna:
```typescript
if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
// Seta viewOnce=true na mensagem de mídia interna
if (m.imageMessage) m.imageMessage.viewOnce = true
else if (m.videoMessage) m.videoMessage.viewOnce = true
else if (m.audioMessage) m.audioMessage.viewOnce = true
// Usa viewOnceMessageV2 (campo 55) — formato atual do WA
m = { viewOnceMessageV2: { message: m } }
}
```
### 5.2 Envio — `src/Socket/messages-send.ts`
**Detecção de view-once real** (não confunde com botões/listas que também usam o wrapper `viewOnceMessage`):
```typescript
// Detecta view-once real pela flag viewOnce na mídia interna.
// viewOnceMessage* wrappers são também usados por botões/listas/carrosséis
// (que carregam interactiveMessage dentro) — esses NÃO têm viewOnce=true na mídia.
const viewOnceInner =
message.viewOnceMessageV2?.message ||
message.viewOnceMessage?.message ||
message.viewOnceMessageV2Extension?.message
const isViewOnceMsg = !!(
viewOnceInner?.imageMessage?.viewOnce ||
viewOnceInner?.videoMessage?.viewOnce ||
viewOnceInner?.audioMessage?.viewOnce
)
// Para view-once: DSM enviado apenas para o celular principal (device=0).
// Companions (device>0) são omitidos — o servidor WA gera
// <unavailable type="view_once"/> automaticamente para eles.
const viewOnceMeRecipients = isViewOnceMsg
? meRecipients.filter(jid => !jidDecode(jid)?.device)
: meRecipients
// assertSessions apenas para quem vai realmente receber
await assertSessions([...viewOnceMeRecipients, ...otherRecipients])
const [meNodes, otherNodes] = await Promise.all([
createParticipantNodes(viewOnceMeRecipients, meMsg || message, extraAttrs),
createParticipantNodes(otherRecipients, message, extraAttrs)
])
participants.push(...meNodes)
participants.push(...otherNodes)
// phash recalculado com os participantes reais
const phashRecipients = isViewOnceMsg
? [...viewOnceMeRecipients, ...otherRecipients]
: [...meRecipients, ...otherRecipients]
if (phashRecipients.length > 0) {
extraAttrs['phash'] = generateParticipantHashV2(phashRecipients)
}
```
**`getMediaType()` — garante que vídeo e áudio view-once sejam entregues:**
```typescript
const getMediaType = (message: proto.IMessage) => {
// Desencapsula wrapper view-once antes de verificar o tipo de mídia.
// Sem isso, message.videoMessage e message.audioMessage são undefined
// (estão dentro do wrapper) e o atributo mediatype ficaria ausente no
// enc node — o servidor WA descarta silenciosamente vídeo/áudio view-once.
const inner =
message.viewOnceMessage?.message ||
message.viewOnceMessageV2?.message ||
message.viewOnceMessageV2Extension?.message
if (inner) {
return getMediaType(inner)
}
if (message.imageMessage) return 'image'
else if (message.videoMessage) return 'video'
// ...
}
```
### 5.3 Recepção — `src/Utils/decode-wa-message.ts`
Quando a API recebe uma view-once como companion (linked device), o servidor envia dois stanzas:
- **Stanza 1** (`enc`): conteúdo completo com metadados da mídia
- **Stanza 2** (`unavailable type="view_once"`): sinal de sync
O stanza 1 é detectado e marcado corretamente:
```typescript
// Detecta view-once na stanza 1 recebida por linked device.
// O wrapper viewOnceMessage também é usado por mensagens interativas
// (interactiveMessage, listMessage) — esses NÃO têm viewOnce=true na mídia.
const viewOnceInner =
msg.viewOnceMessage?.message ||
msg.viewOnceMessageV2?.message ||
msg.viewOnceMessageV2Extension?.message
if (
viewOnceInner?.imageMessage?.viewOnce ||
viewOnceInner?.videoMessage?.viewOnce ||
viewOnceInner?.audioMessage?.viewOnce
) {
fullMessage.key.isViewOnce = true
}
```
---
## 6. Fluxo Completo
```
API envia view-once para destinatário
├── Protobuf gerado:
│ viewOnceMessageV2 {
│ message {
│ imageMessage { viewOnce: true, url, mediaKey, ... }
│ }
│ }
├── Stanza enviado ao servidor WA:
│ <message to="destinatario@s.whatsapp.net" type="media">
│ <enc type="pkmsg">...viewOnceMessageV2 cifrado...</enc> ← para o destinatário
│ <participants>
│ <to jid="meu-numero:0@lid"> ← celular principal
│ <enc type="msg">...DSM{viewOnceMessageV2}...</enc>
│ </to>
│ <!-- device>0 omitidos — servidor gera unavailable -->
│ </participants>
│ </message>
└── Distribuição pelo servidor:
├── Destinatário → recebe enc → abre uma vez ✅
├── Celular principal → recebe DSM → mostra "você enviou" ✅
└── WA Web → recebe <unavailable> → "Aguardando..." ✅
Destinatário envia view-once de volta para a API
├── Stanza recebido pela API (stanza 1 — enc com conteúdo):
│ <message from="destinatario@s.whatsapp.net" type="media">
│ <enc type="msg">...viewOnceMessageV2 cifrado...</enc>
│ </message>
├── decode-wa-message.ts:
│ └── Descriptografa → detecta viewOnce=true → seta key.isViewOnce=true
├── Stanza recebido (stanza 2 — unavailable, ignorado):
│ <message from="...">
│ <unavailable type="view_once"/>
│ </message>
└── Webhook emitido:
messages.upsert → msg.key.isViewOnce = true ✅
```
---
## 7. Limitações
| Situação | Comportamento | Motivo |
|---|---|---|
| **Conta hosted (Meta Business Cloud API)** | **View-once bloqueado para todos os clientes** | **Política do servidor WA para contas hosted — sem workaround** |
| WA Web do remetente | "Aguardando mensagem. Abra no celular." | Servidor WA só aceita `<unavailable>` vindo do celular principal, não de companions |
| View-once de documento/sticker | Não suportado | Limitação do protocolo WA — só imagem, vídeo e áudio |
| `error_479` nos logs | Normal, não é erro | TcToken é atualizado após o envio de view-once |
| Companion não consegue abrir | Por design | O conteúdo não é entregue para linked devices |
---
## 8. Checklist de Validação
**Envio:**
- [ ] Destinatário recebe com ícone de olhinho
- [ ] Destinatário consegue abrir a mídia
- [ ] Após abrir, a mídia some
- [ ] Celular principal do remetente mostra "você enviou" com ícone de olhinho
- [ ] WA Web do remetente mostra "Aguardando mensagem..."
- [ ] Funciona para imagem, vídeo e áudio
- [ ] Logs mostram `📤 Message sent` (error_479 é normal)
**Recepção:**
- [ ] Webhook recebe `key.isViewOnce = true`
- [ ] `viewOnceMessageV2.message.imageMessage` (ou video/audio) está presente
- [ ] API não crasha ao receber view-once de outro dispositivo
- [ ] Mensagem interativa (botão/lista) enviada após view-once não é afetada
+1 -1
View File
@@ -1 +1 @@
{"version":[2,3000,1037166441]} {"version":[2,3000,1035595667]}
+4 -38
View File
@@ -31,26 +31,9 @@ export const STATUS_EXPIRY_SECONDS = 24 * 60 * 60
export const PLACEHOLDER_MAX_AGE_SECONDS = 7 * 24 * 60 * 60 export const PLACEHOLDER_MAX_AGE_SECONDS = 7 * 24 * 60 * 60
export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0' export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0'
export const DICT_VERSION = 3
/**
* Protocol mode: 'web' (default) or 'native' (experimental)
* - web: WA\x06\x03 — standard WhatsApp Web protocol
* - native: WAM\x05 — native Android protocol (may enable view-once media on companions)
*
* Set via BAILEYS_PROTOCOL env var:
* BAILEYS_PROTOCOL=native
*/
const PROTOCOL_MODE = process.env.BAILEYS_PROTOCOL?.trim().toLowerCase() === 'native' ? 'native' : 'web'
export const DICT_VERSION = PROTOCOL_MODE === 'native' ? 5 : 3
export const KEY_BUNDLE_TYPE = Buffer.from([5]) export const KEY_BUNDLE_TYPE = Buffer.from([5])
export const NOISE_WA_HEADER = Buffer.from([87, 65, 6, DICT_VERSION]) // last is "DICT_VERSION"
// WA\x06\x03 = web protocol, WAM\x05 = native Android protocol
export const NOISE_WA_HEADER = PROTOCOL_MODE === 'native'
? Buffer.from([87, 65, 77, DICT_VERSION]) // WAM\x05
: Buffer.from([87, 65, 6, DICT_VERSION]) // WA\x06\x03
export { PROTOCOL_MODE }
/** from: https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url */ /** from: https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url */
export const URL_REGEX = /https:\/\/(?![^:@\/\s]+:[^:@\/\s]+@)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(:\d+)?(\/[^\s]*)?/g export const URL_REGEX = /https:\/\/(?![^:@\/\s]+:[^:@\/\s]+@)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(:\d+)?(\/[^\s]*)?/g
@@ -171,20 +154,7 @@ export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = {
'md-msg-hist': '/mms/md-app-state', 'md-msg-hist': '/mms/md-app-state',
'biz-cover-photo': '/pps/biz-cover-photo', 'biz-cover-photo': '/pps/biz-cover-photo',
'sticker-pack': '/mms/sticker-pack', 'sticker-pack': '/mms/sticker-pack',
'thumbnail-sticker-pack': '/mms/thumbnail-sticker-pack', 'thumbnail-sticker-pack': '/mms/thumbnail-sticker-pack'
// View-once types — same upload path as regular media (server CDN routing TBD)
'viewonce-image': '/mms/image',
'viewonce-video': '/mms/video',
'viewonce-audio': '/mms/audio'
}
export const NEWSLETTER_MEDIA_PATH_MAP: { [T in MediaType]?: string } = {
image: '/newsletter/newsletter-image',
video: '/newsletter/newsletter-video',
document: '/newsletter/newsletter-document',
audio: '/newsletter/newsletter-audio',
sticker: '/newsletter/newsletter-image',
'thumbnail-link': '/newsletter/newsletter-image'
} }
export const MEDIA_HKDF_KEY_MAPPING = { export const MEDIA_HKDF_KEY_MAPPING = {
@@ -208,11 +178,7 @@ export const MEDIA_HKDF_KEY_MAPPING = {
ptv: 'Video', ptv: 'Video',
'biz-cover-photo': 'Image', 'biz-cover-photo': 'Image',
'sticker-pack': 'Sticker Pack', 'sticker-pack': 'Sticker Pack',
'thumbnail-sticker-pack': 'Sticker Pack Thumbnail', 'thumbnail-sticker-pack': 'Sticker Pack Thumbnail'
// View-once types — same HKDF keys as regular types (recipients decrypt with normal keys)
'viewonce-image': 'Image',
'viewonce-video': 'Video',
'viewonce-audio': 'Audio'
} }
export type MediaType = keyof typeof MEDIA_HKDF_KEY_MAPPING export type MediaType = keyof typeof MEDIA_HKDF_KEY_MAPPING
+7 -88
View File
@@ -2241,79 +2241,20 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
} }
const handleMessage = async (node: BinaryNode) => { const handleMessage = async (node: BinaryNode) => {
const _encNode = getBinaryNodeChild(node, 'enc')
const _unavNode = getBinaryNodeChild(node, 'unavailable')
// VO_RAW: log BEFORE any filtering — captures every stanza that enters handleMessage
console.log(`[VO_RAW] id=${node.attrs.id} from=${node.attrs.from} type=${node.attrs.type} enc=${_encNode ? _encNode.attrs?.type : 'NO'} unavailable=${_unavNode ? _unavNode.attrs?.type : 'NO'} children=${Array.isArray(node.content) ? (node.content as BinaryNode[]).map(c => c.tag).join(',') : 'none'}`)
if (shouldIgnoreJid(node.attrs.from!) && node.attrs.from !== S_WHATSAPP_NET) { if (shouldIgnoreJid(node.attrs.from!) && node.attrs.from !== S_WHATSAPP_NET) {
console.log(`[VO_RAW] IGNORED by shouldIgnoreJid: ${node.attrs.from}`)
logger.trace({ from: node.attrs.from }, 'ignored message') logger.trace({ from: node.attrs.from }, 'ignored message')
// Send a clean ACK (no error code) so the server considers the
// message delivered. Using error 500 (UnhandledError) previously
// caused the server to retry delivery, generating duplicate traffic.
await sendMessageAck(node) await sendMessageAck(node)
return return
} }
const encNode = getBinaryNodeChild(node, 'enc') const encNode = getBinaryNodeChild(node, 'enc')
const unavailableNode = getBinaryNodeChild(node, 'unavailable') // TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
if (encNode?.attrs.type === 'msmsg') {
// DEBUG: log every incoming message stanza to trace view-once delivery logger.debug({ key: node.attrs.key }, 'ignored msmsg')
logger.info({ await sendMessageAck(node, NACK_REASONS.MissingMessageSecret)
id: node.attrs.id,
from: node.attrs.from,
type: node.attrs.type,
hasEnc: !!encNode,
encType: encNode?.attrs?.type,
hasUnavailable: !!unavailableNode,
unavailableType: unavailableNode?.attrs?.type,
}, 'VO_TRACE: incoming message stanza')
// msmsg block removed — allow msmsg decryption (required for view-once on linked devices)
if (unavailableNode?.attrs?.type === 'view_once') {
const { fullMessage: voMsg } = decryptMessageNode(
node,
authState.creds.me!.id,
authState.creds.me!.lid || '',
signalRepository,
logger,
)
voMsg.key.isViewOnce = true
// Try to request the actual view-once content from the primary phone via PDO
// The phone may resend the full media (url, mediaKey, directPath, etc.)
console.log(`[VO_PDO] Requesting view-once content via PDO for ${voMsg.key.id} from ${voMsg.key.remoteJid}`)
try {
const requestId = await requestPlaceholderResend(voMsg.key, {
key: { ...voMsg.key },
pushName: voMsg.pushName,
messageTimestamp: voMsg.messageTimestamp,
participant: voMsg.key.participant,
participantAlt: voMsg.key.participantAlt
})
if (requestId && requestId !== 'RESOLVED') {
console.log(`[VO_PDO] PDO request sent! requestId=${requestId} — waiting for phone to resend`)
} else if (requestId === 'RESOLVED') {
console.log(`[VO_PDO] Message arrived during PDO delay!`)
} else {
console.log(`[VO_PDO] Already requested or no response`)
}
} catch (err: any) {
console.log(`[VO_PDO] PDO request failed: ${err.message} — falling back to placeholder`)
}
// Emit placeholder immediately so consumers see the notification
// If PDO succeeds, the real message will arrive via messages.upsert later
voMsg.message = {
viewOnceMessage: {
message: {
imageMessage: {
viewOnce: true,
}
}
}
}
const upsertType = node.attrs.offline ? 'append' : 'notify'
await upsertMessage(voMsg, upsertType)
await sendMessageAck(node)
return return
} }
@@ -2430,23 +2371,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
{ msgId: msg.key?.id, messageType }, { msgId: msg.key?.id, messageType },
'CTWA: Skipping placeholder resend for unavailable fanout type' 'CTWA: Skipping placeholder resend for unavailable fanout type'
) )
// For view-once: emit the message with a viewOnceMessage placeholder
// so consumers (zpro, etc.) can detect and display it.
// Include a message wrapper so isValidMsg accepts it.
if (messageType === 'view_once_unavailable_fanout') {
msg.key.isViewOnce = true
msg.message = {
viewOnceMessage: {
message: {
imageMessage: {
viewOnce: true,
}
}
}
}
const upsertType = node.attrs.offline ? 'append' : 'notify'
await upsertMessage(msg, upsertType)
}
metrics.ctwaRecoveryFailures.inc({ reason: 'unavailable_fanout' }) metrics.ctwaRecoveryFailures.inc({ reason: 'unavailable_fanout' })
return sendMessageAck(node) return sendMessageAck(node)
} }
@@ -2628,11 +2552,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
await sendReceipt(msg.key.remoteJid!, participant!, [msg.key.id!], type) await sendReceipt(msg.key.remoteJid!, participant!, [msg.key.id!], type)
// NOTE: do NOT send view_once_read from a linked device (API).
// WA shows view-once as view_once_unavailable_fanout on linked devices — only the
// primary phone sends view_once_read when the user actually opens it.
// Sending it from a linked device causes WA to flag the account and disables view-once.
// send ack for history message // send ack for history message
const isAnyHistoryMsg = getHistoryMsg(msg.message!) const isAnyHistoryMsg = getHistoryMsg(msg.message!)
if (isAnyHistoryMsg) { if (isAnyHistoryMsg) {
+3 -6
View File
@@ -1041,8 +1041,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
} }
await authState.keys.transaction(async () => { await authState.keys.transaction(async () => {
// normalizeMessageContent unwraps viewOnceMessage/viewOnceMessageV2 so getMediaType works correctly const mediaType = getMediaType(message)
const mediaType = getMediaType(normalizeMessageContent(message) || message)
if (mediaType) { if (mediaType) {
extraAttrs['mediatype'] = mediaType extraAttrs['mediatype'] = mediaType
} }
@@ -1052,7 +1051,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const bytes = encodeNewsletterMessage(patched as proto.IMessage) const bytes = encodeNewsletterMessage(patched as proto.IMessage)
binaryNodeContent.push({ binaryNodeContent.push({
tag: 'plaintext', tag: 'plaintext',
attrs: mediaType ? { mediatype: mediaType } : {}, attrs: {},
content: bytes content: bytes
}) })
const stanza: BinaryNode = { const stanza: BinaryNode = {
@@ -1074,8 +1073,6 @@ export const makeMessagesSocket = (config: SocketConfig) => {
extraAttrs['decrypt-fail'] = 'hide' // todo: expand for reactions and other types extraAttrs['decrypt-fail'] = 'hide' // todo: expand for reactions and other types
} }
// view_once_write disabled — causes server to drop message (validation fails for linked device sessions)
if (isGroupOrStatus && !isRetryResend) { if (isGroupOrStatus && !isRetryResend) {
const [groupData, senderKeyMap] = await Promise.all([ const [groupData, senderKeyMap] = await Promise.all([
(async () => { (async () => {
@@ -1310,13 +1307,13 @@ export const makeMessagesSocket = (config: SocketConfig) => {
{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: meNodes, shouldIncludeDeviceIdentity: s1 },
{ nodes: otherNodes, shouldIncludeDeviceIdentity: s2 } { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }
] = await Promise.all([ ] = await Promise.all([
// For own devices: use DSM (deviceSentMessage) wrapper
createParticipantNodes(viewOnceMeRecipients, meMsg || message, extraAttrs), createParticipantNodes(viewOnceMeRecipients, meMsg || message, extraAttrs),
createParticipantNodes(effectiveOtherRecipients, message, extraAttrs) createParticipantNodes(effectiveOtherRecipients, message, extraAttrs)
]) ])
participants.push(...meNodes) participants.push(...meNodes)
participants.push(...otherNodes) participants.push(...otherNodes)
// phash: para view-once inclui só celular principal + destinatários
const phashRecipients = isViewOnceMsg const phashRecipients = isViewOnceMsg
? [...viewOnceMeRecipients, ...effectiveOtherRecipients] ? [...viewOnceMeRecipients, ...effectiveOtherRecipients]
: effectiveAllRecipients : effectiveAllRecipients
+2 -13
View File
@@ -155,7 +155,6 @@ export type MessageReceiptType =
| 'sender' | 'sender'
| 'inactive' | 'inactive'
| 'played' | 'played'
| 'view_once_read'
| undefined | undefined
export type MediaConnInfo = { export type MediaConnInfo = {
@@ -179,8 +178,6 @@ export interface WAUrlInfo {
type Mentionable = { type Mentionable = {
/** list of jids that are mentioned in the accompanying text */ /** list of jids that are mentioned in the accompanying text */
mentions?: string[] mentions?: string[]
/** mention all participants in the group */
mentionAll?: boolean
} }
type Contextable = { type Contextable = {
/** add contextInfo to the message */ /** add contextInfo to the message */
@@ -993,16 +990,8 @@ export type MessageGenerationOptionsFromContent = MiscMessageGenerationOptions &
export type WAMediaUploadFunction = ( export type WAMediaUploadFunction = (
encFilePath: string, encFilePath: string,
opts: { fileEncSha256B64: string; mediaType: MediaType; timeoutMs?: number; newsletter?: boolean } opts: { fileEncSha256B64: string; mediaType: MediaType; timeoutMs?: number }
) => Promise<{ ) => Promise<{ mediaUrl: string; directPath: string; meta_hmac?: string; ts?: number; fbid?: number }>
mediaUrl: string | undefined
directPath: string
meta_hmac?: string
ts?: number
fbid?: number
thumbnailDirectPath?: string
thumbnailSha256?: string
}>
export type MediaGenerationOptions = { export type MediaGenerationOptions = {
logger?: ILogger logger?: ILogger
+6 -32
View File
@@ -10,13 +10,7 @@ import { join } from 'path'
import { Readable, Transform } from 'stream' import { Readable, Transform } from 'stream'
import { URL } from 'url' import { URL } from 'url'
import { proto } from '../../WAProto/index.js' import { proto } from '../../WAProto/index.js'
import { import { DEFAULT_ORIGIN, MEDIA_HKDF_KEY_MAPPING, MEDIA_PATH_MAP, type MediaType } from '../Defaults'
DEFAULT_ORIGIN,
MEDIA_HKDF_KEY_MAPPING,
MEDIA_PATH_MAP,
type MediaType,
NEWSLETTER_MEDIA_PATH_MAP
} from '../Defaults'
import type { import type {
BaileysEventMap, BaileysEventMap,
DownloadableMessage, DownloadableMessage,
@@ -687,10 +681,6 @@ type MediaUploadResult = {
meta_hmac?: string meta_hmac?: string
ts?: number ts?: number
fbid?: number fbid?: number
thumbnail_info?: {
thumbnail_sha256?: string
thumbnail_direct_path?: string
}
} }
export type UploadParams = { export type UploadParams = {
@@ -835,21 +825,11 @@ export const getWAUploadToServer = (
{ customUploadHosts, fetchAgent, logger, options }: SocketConfig, { customUploadHosts, fetchAgent, logger, options }: SocketConfig,
refreshMediaConn: (force: boolean) => Promise<MediaConnInfo> refreshMediaConn: (force: boolean) => Promise<MediaConnInfo>
): WAMediaUploadFunction => { ): WAMediaUploadFunction => {
return async (filePath, { mediaType, fileEncSha256B64, timeoutMs, newsletter }) => { return async (filePath, { mediaType, fileEncSha256B64, timeoutMs }) => {
// send a query JSON to obtain the url & auth token to upload our media // send a query JSON to obtain the url & auth token to upload our media
let uploadInfo = await refreshMediaConn(false) let uploadInfo = await refreshMediaConn(false)
let urls: let urls: { mediaUrl: string; directPath: string; meta_hmac?: string; ts?: number; fbid?: number } | undefined
| {
mediaUrl: string
directPath: string
meta_hmac?: string
ts?: number
fbid?: number
thumbnailDirectPath?: string
thumbnailSha256?: string
}
| undefined
const hosts = [...customUploadHosts, ...uploadInfo.hosts] const hosts = [...customUploadHosts, ...uploadInfo.hosts]
fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64) fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64)
@@ -871,11 +851,7 @@ export const getWAUploadToServer = (
logger.debug(`uploading to "${hostname}"`) logger.debug(`uploading to "${hostname}"`)
const auth = encodeURIComponent(uploadInfo.auth) const auth = encodeURIComponent(uploadInfo.auth)
const mediaPath = (newsletter ? NEWSLETTER_MEDIA_PATH_MAP[mediaType] : undefined) || MEDIA_PATH_MAP[mediaType] const url = `https://${hostname}${MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
let url = `https://${hostname}${mediaPath}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
if (newsletter) {
url += '&server_thumb_gen=1'
}
let result: MediaUploadResult | undefined let result: MediaUploadResult | undefined
try { try {
@@ -892,13 +868,11 @@ export const getWAUploadToServer = (
if (result?.url || result?.direct_path) { if (result?.url || result?.direct_path) {
urls = { urls = {
mediaUrl: result.url || result.direct_path!, mediaUrl: result.url!,
directPath: result.direct_path!, directPath: result.direct_path!,
meta_hmac: result.meta_hmac, meta_hmac: result.meta_hmac,
fbid: result.fbid, fbid: result.fbid,
ts: result.ts, ts: result.ts
thumbnailDirectPath: result.thumbnail_info?.thumbnail_direct_path,
thumbnailSha256: result.thumbnail_info?.thumbnail_sha256
} }
break break
} else { } else {
+14 -46
View File
@@ -196,11 +196,10 @@ export const prepareWAMessageMedia = async (
) )
const fileSha256B64 = fileSha256.toString('base64') const fileSha256B64 = fileSha256.toString('base64')
const { directPath, thumbnailDirectPath, thumbnailSha256 } = await options.upload(filePath, { const { mediaUrl, directPath } = await options.upload(filePath, {
fileEncSha256B64: fileSha256B64, fileEncSha256B64: fileSha256B64,
mediaType: mediaType, mediaType: mediaType,
timeoutMs: options.mediaUploadTimeoutMs, timeoutMs: options.mediaUploadTimeoutMs
newsletter: true
}) })
await fs.unlink(filePath) await fs.unlink(filePath)
@@ -208,12 +207,10 @@ export const prepareWAMessageMedia = async (
const obj = WAProto.Message.fromObject({ const obj = WAProto.Message.fromObject({
// todo: add more support here // todo: add more support here
[`${mediaType}Message`]: (MessageTypeProto as any)[mediaType].fromObject({ [`${mediaType}Message`]: (MessageTypeProto as any)[mediaType].fromObject({
// url intentionally omitted — newsletters use directPath only url: mediaUrl,
directPath, directPath,
fileSha256, fileSha256,
fileLength, fileLength,
thumbnailDirectPath,
thumbnailSha256: thumbnailSha256 ? Buffer.from(thumbnailSha256, 'base64') : undefined,
...uploadData, ...uploadData,
media: undefined media: undefined
}) })
@@ -239,7 +236,7 @@ export const prepareWAMessageMedia = async (
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined' const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
const requiresThumbnailComputation = const requiresThumbnailComputation =
(mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined' (mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined'
const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true && typeof uploadData.waveform === 'undefined' const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true
const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation
const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream( const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream(
@@ -257,9 +254,7 @@ export const prepareWAMessageMedia = async (
(async () => { (async () => {
const result = await options.upload(encFilePath, { const result = await options.upload(encFilePath, {
fileEncSha256B64, fileEncSha256B64,
// Use mediaTypeOverride for upload path (e.g. 'viewonce-image' → /o1/mms/image) mediaType,
// Encryption already used the override via encryptedStream above
mediaType: options.mediaTypeOverride || mediaType,
timeoutMs: options.mediaUploadTimeoutMs timeoutMs: options.mediaUploadTimeoutMs
}) })
logger?.debug({ mediaType, cacheableKey }, 'uploaded media') logger?.debug({ mediaType, cacheableKey }, 'uploaded media')
@@ -297,7 +292,7 @@ export const prepareWAMessageMedia = async (
logger?.debug('computed audio duration') logger?.debug('computed audio duration')
} }
if (requiresWaveformProcessing && !uploadData.waveform) { if (requiresWaveformProcessing) {
uploadData.waveform = await getAudioWaveform(filePath, logger) uploadData.waveform = await getAudioWaveform(filePath, logger)
logger?.debug('processed waveform') logger?.debug('processed waveform')
} }
@@ -1752,49 +1747,22 @@ export const generateWAMessageContent = async (
} }
} }
} else { } else {
// For view-once media, upload to the one-time CDN (/o1/mms/*) instead of regular CDN m = await prepareWAMessageMedia(message, options)
// WA Web view-once messages always have directPath: /o1/v/t24/... (not /v/t62.7118-24/...)
// Using the wrong CDN causes WA server to flag the account and breaks view-once capability
const isViewOnce = hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce
const viewOnceTypeMap: Record<string, string> = {
image: 'viewonce-image',
video: 'viewonce-video',
audio: 'viewonce-audio'
}
const viewOnceOverride = isViewOnce
? viewOnceTypeMap[Object.keys(message).find(k => k in viewOnceTypeMap) || '']
: undefined
m = await prepareWAMessageMedia(message, {
...options,
mediaTypeOverride: (viewOnceOverride as any) || options.mediaTypeOverride
})
} }
if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) { if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
// Also set viewOnce=true on the inner media message (required by WA app to render as view-once) m = { viewOnceMessage: { message: m } }
if (m.imageMessage) m.imageMessage.viewOnce = true
else if (m.videoMessage) m.videoMessage.viewOnce = true
else if (m.audioMessage) m.audioMessage.viewOnce = true
// Use viewOnceMessageV2 (field 55) — modern WA clients use this wrapper
// viewOnceMessage (field 37) is the old format which WA server no longer accepts with view_once_write
m = { viewOnceMessageV2: { message: m } }
} }
if ( if (hasOptionalProperty(message, 'mentions') && message.mentions?.length) {
(hasOptionalProperty(message, 'mentions') && message.mentions?.length) ||
(hasOptionalProperty(message, 'mentionAll') && message.mentionAll)
) {
const messageType = Object.keys(m)[0] as Extract<keyof proto.IMessage, MessageWithContextInfo> const messageType = Object.keys(m)[0] as Extract<keyof proto.IMessage, MessageWithContextInfo>
if (messageType) { if (messageType) {
const key = m[messageType] const key = m[messageType]
if (key && 'contextInfo' in key) { if (key && 'contextInfo' in key && !!key.contextInfo) {
key.contextInfo = key.contextInfo || {} key.contextInfo.mentionedJid = message.mentions
if (message.mentions?.length) { } else if (key) {
key.contextInfo.mentionedJid = message.mentions key.contextInfo = {
} mentionedJid: message.mentions
if (message.mentionAll) {
key.contextInfo.nonJidMentions = 1
} }
} }
} }
+1 -5
View File
@@ -3,7 +3,6 @@ import { createHash } from 'crypto'
import { proto } from '../../WAProto/index.js' import { proto } from '../../WAProto/index.js'
import { import {
KEY_BUNDLE_TYPE, KEY_BUNDLE_TYPE,
PROTOCOL_MODE,
WA_ADV_ACCOUNT_SIG_PREFIX, WA_ADV_ACCOUNT_SIG_PREFIX,
WA_ADV_DEVICE_SIG_PREFIX, WA_ADV_DEVICE_SIG_PREFIX,
WA_ADV_HOSTED_ACCOUNT_SIG_PREFIX WA_ADV_HOSTED_ACCOUNT_SIG_PREFIX
@@ -59,10 +58,7 @@ const getClientPayload = (config: SocketConfig) => {
userAgent: getUserAgent(config) userAgent: getUserAgent(config)
} }
// Native protocol (WAM\x05) does not use WebInfo — only web protocol does payload.webInfo = getWebInfo(config)
if (PROTOCOL_MODE !== 'native') {
payload.webInfo = getWebInfo(config)
}
return payload return payload
} }