Compare commits

...

6 Commits

Author SHA1 Message Date
Renato Alcara f0ee205f96 fix(waproto): normalize high to int32 + tighten roundtrip test
Addresses 2 review comments on PR #385:

1. CodeRabbit Major — longToString/longToNumber checked
   `value.high < 0` on the raw input. When a Long-like object carries
   high as an unsigned 32-bit value (e.g. raw {low, high} from JSON
   deserialization, not constructed via Long.fromBits/fromValue which
   normalize via `| 0`), the sign-bit detection fails and the value is
   interpreted as unsigned. Example: {low: 0, high: 0xFFFFFFFF} signed
   should be -4294967296 but returned 18446744069414584320.

   Fix: `const high = value.high | 0` before the sign check. Applied
   to both helpers in WAProto/index.js (generated artifact) and
   WAProto/fix-imports.js (template). Diverges from upstream Baileys
   PR #2333 which has the same latent bug.

   Added 2 test cases in bigint-validation.test.ts for the raw
   {low, high} scenario (Long.fromBits would mask the bug because it
   normalizes internally).

2. CodeRabbit Nitpick — the encode/decode roundtrip test in
   proto-tojson-long.test.ts used safe integers (1700000000 and
   123456789), which would silently pass even if longToString fell
   back to Number(value). Switched to uint64 values above
   MAX_SAFE_INTEGER (max uint64 and 9999999999999999999) so the
   roundtrip actually exercises the BigInt fast path.

Test suite: 789/789 passing (+2 cases for the unsigned-high scenario).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:06:16 -03:00
Renato Alcara 5d7840a66c perf: optimize history sync memory and CPU usage
Aligns with Baileys upstream PR #2333 (jlucaso1). Surgical port of
4 micro-optimizations + 2 test files. No collision with InfiniteAPI's
LID/PN, carousel, buttons, Bad MAC or app state sync resilience code.

Changes:

1. WAProto longToString/longToNumber: use native BigInt instead of
   Long library division loops. ~4.6x faster per call (237ns -> 52ns).
   BigInt.toString() delegates to V8 native C++. Same change applied
   to fix-imports.js (template) and index.js (generated artifact) so
   future regenerations stay in sync.

2. downloadHistory: stream-pipe createInflate instead of buffering
   the full compressed payload then inflating. Cuts RSS peak ~50%
   on 50MB history payloads.

3. processHistoryMessage: drop the { ...chat } shallow copy before
   pushing to chats[]. The decoded protobuf object is not referenced
   after the push.

4. downloadEncryptedContent transform: skip Buffer.concat when
   remainingBytes is empty (the common case — chunks usually arrive
   AES-aligned). ~19x faster per chunk (78ms -> 4ms over 25k chunks).

Tests:
- bigint-validation.test.ts (NEW): 32 parameterized cases covering
  zero, MAX_SAFE_INTEGER boundary, max int64/uint64, negative values,
  tricky bit patterns and powers of 2; plus 200-iter fuzz comparing
  old Long vs new BigInt implementations for equivalence.
- proto-tojson-long.test.ts: kept the existing fork-only test for
  string-in-Long-field graceful handling, appended 4 upstream tests
  covering Long->string fast path, large unsigned > MAX_SAFE_INTEGER,
  zero/small values and encode/decode roundtrip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:27:10 -03:00
Renato Alcara e117456079 test(binary): cover reconnection sync skip for both signals (#384)
* test(binary): cover reconnection sync skip for both signals

Ports the upstream reconnection-sync-skip test from Baileys PR #2350
and adapts it to cover InfiniteAPI's dual-signal reconnect detection
in chats.ts:

1. accountSyncCounter > 0 (a previous full sync completed)
2. socketSkippedOfflineBuffer (forwarded from socket.ts when
   hadStaleRoutingInfo is true, e.g. routingInfo discarded on start)

Both are required because they cover different reconnect scenarios
and the absence of the second branch causes a buffer mismatch where
socket.ts skips the offline buffer while chats.ts still waits in
AwaitingInitialSync — stalling live messages for up to 4 seconds.

Co-Authored-By: Renato Alcara
2026-04-25 14:16:14 -03:00
Renato Alcara c179c82cca perf(messages-recv): early-ignore JIDs before buffer/queue (#383)
* perf(messages-recv): early-ignore JIDs before buffer/queue

Aligns with Baileys upstream PR #2352. Moves the shouldIgnoreJid check
out of handleMessage/handleReceipt/handleNotification and into
processNode so ignored stanzas are acked and dropped before entering
ev.buffer(), the offline queue, or the keyed mutexes. Skips the LID->PN
resolution work in handleReceipt for ignored receipts as a side effect.

Diverges from upstream by excluding type='call' from the early-ignore
check — InfiniteAPI's handleCall has never used shouldIgnoreJid, so
keeping calls outside this filter preserves existing behavior for
integrators that filter messages but still want call events.

Carousel, buttons, LID/PN normalization, Bad MAC fix and retry manager
untouched.

Co-Authored-By: Renato Alcara
2026-04-25 13:16:22 -03:00
Renato Alcara d08a09c35f feat: add inbound username support and USync username protocol (#382)
* feat: add inbound username support and USync username protocol

Aligns with Baileys upstream PR #2480. WhatsApp is rolling out an
inbound username field that maps to the user's LID. This change is
purely additive — it captures and propagates the new optional field
through types, decoders, USync queries, and group/contact events.

Skipped intentionally to preserve InfiniteAPI's LID/PN customization:
- handleGroupNotification in messages-recv.ts (custom LID->PN flow on
  groups.upsert / participants.map). Username for these specific
  events still flows via process-message's emitParticipantsUpdate
  (reads message.key.participantUsername added in decode-wa-message).

Carousel/buttons code (messages.ts, messages-send.ts) untouched.

Co-Authored-By: Renato Alcara
2026-04-25 12:39:31 -03:00
Renato Alcara cffaad9e5a fix(chats): use abt xmlns and protocol
fix(chats): use abt xmlns and protocol
2026-04-25 11:08:47 -03:00
23 changed files with 659 additions and 75 deletions
+26 -18
View File
@@ -18,14 +18,21 @@ try {
'\tif (typeof value === "number") {\n' + '\tif (typeof value === "number") {\n' +
'\t\treturn String(value);\n' + '\t\treturn String(value);\n' +
'\t}\n' + '\t}\n' +
'\tif (!$util.Long) {\n' + '\t// Fast path: convert Long {low, high} directly via native BigInt\n' +
'\t\treturn String(value);\n' + '\t// BigInt.toString() is a native C++ operation, much faster than Long\'s pure JS division loops\n' +
'\tif (value && typeof value.low === "number" && typeof value.high === "number") {\n' +
'\t\t// Normalize high to signed int32 so the sign-bit check works for inputs\n' +
'\t\t// where high is stored unsigned (e.g. raw {low, high} JSON).\n' +
'\t\tconst high = value.high | 0;\n' +
'\t\tconst lo = BigInt(value.low >>> 0);\n' +
'\t\tconst hi = BigInt(value.high >>> 0);\n' +
'\t\tconst combined = (hi << 32n) | lo;\n' +
'\t\tif (!unsigned && high < 0) {\n' +
'\t\t\treturn (combined - (1n << 64n)).toString();\n' +
'\t\t}\n' +
'\t\treturn combined.toString();\n' +
'\t}\n' + '\t}\n' +
'\tconst normalized = $util.Long.fromValue(value);\n' + '\treturn String(value);\n' +
'\tconst prepared = unsigned && normalized && typeof normalized.toUnsigned === "function"\n' +
'\t\t? normalized.toUnsigned()\n' +
'\t\t: normalized;\n' +
'\treturn prepared.toString();\n' +
'}\n\n' '}\n\n'
const longToNumberHelper = const longToNumberHelper =
'function longToNumber(value, unsigned) {\n' + 'function longToNumber(value, unsigned) {\n' +
@@ -33,19 +40,20 @@ try {
'\t\treturn value;\n' + '\t\treturn value;\n' +
'\t}\n' + '\t}\n' +
'\tif (typeof value === "string") {\n' + '\tif (typeof value === "string") {\n' +
'\t\tconst numeric = Number(value);\n' +
'\t\treturn numeric;\n' +
'\t}\n' +
'\tif (!$util.Long) {\n' +
'\t\treturn Number(value);\n' + '\t\treturn Number(value);\n' +
'\t}\n' + '\t}\n' +
'\tconst normalized = $util.Long.fromValue(value);\n' + '\t// Fast path: convert Long {low, high} directly via native BigInt\n' +
'\tconst prepared = unsigned && normalized && typeof normalized.toUnsigned === "function"\n' + '\tif (value && typeof value.low === "number" && typeof value.high === "number") {\n' +
'\t\t? normalized.toUnsigned()\n' + '\t\tconst high = value.high | 0;\n' +
'\t\t: typeof normalized.toSigned === "function"\n' + '\t\tconst lo = BigInt(value.low >>> 0);\n' +
'\t\t\t? normalized.toSigned()\n' + '\t\tconst hi = BigInt(value.high >>> 0);\n' +
'\t\t\t: normalized;\n' + '\t\tconst combined = (hi << 32n) | lo;\n' +
'\treturn prepared.toNumber();\n' + '\t\tif (!unsigned && high < 0) {\n' +
'\t\t\treturn Number(combined - (1n << 64n));\n' +
'\t\t}\n' +
'\t\treturn Number(combined);\n' +
'\t}\n' +
'\treturn Number(value);\n' +
'}\n\n' '}\n\n'
if (!content.includes('function longToString(')) { if (!content.includes('function longToString(')) {
+26 -18
View File
@@ -12,14 +12,21 @@ function longToString(value, unsigned) {
if (typeof value === "number") { if (typeof value === "number") {
return String(value); return String(value);
} }
if (!$util.Long) { // Fast path: convert Long {low, high} directly via native BigInt
return String(value); // BigInt.toString() is a native C++ operation, much faster than Long's pure JS division loops
if (value && typeof value.low === "number" && typeof value.high === "number") {
// Normalize high to signed int32 so the sign-bit check works for inputs
// where high is stored unsigned (e.g. raw {low, high} JSON).
const high = value.high | 0;
const lo = BigInt(value.low >>> 0);
const hi = BigInt(value.high >>> 0);
const combined = (hi << 32n) | lo;
if (!unsigned && high < 0) {
return (combined - (1n << 64n)).toString();
}
return combined.toString();
} }
const normalized = $util.Long.fromValue(value); return String(value);
const prepared = unsigned && normalized && typeof normalized.toUnsigned === "function"
? normalized.toUnsigned()
: normalized;
return prepared.toString();
} }
function longToNumber(value, unsigned) { function longToNumber(value, unsigned) {
@@ -27,19 +34,20 @@ function longToNumber(value, unsigned) {
return value; return value;
} }
if (typeof value === "string") { if (typeof value === "string") {
const numeric = Number(value);
return numeric;
}
if (!$util.Long) {
return Number(value); return Number(value);
} }
const normalized = $util.Long.fromValue(value); // Fast path: convert Long {low, high} directly via native BigInt
const prepared = unsigned && normalized && typeof normalized.toUnsigned === "function" if (value && typeof value.low === "number" && typeof value.high === "number") {
? normalized.toUnsigned() const high = value.high | 0;
: typeof normalized.toSigned === "function" const lo = BigInt(value.low >>> 0);
? normalized.toSigned() const hi = BigInt(value.high >>> 0);
: normalized; const combined = (hi << 32n) | lo;
return prepared.toNumber(); if (!unsigned && high < 0) {
return Number(combined - (1n << 64n));
}
return Number(combined);
}
return Number(value);
} }
export const proto = $root.proto = (() => { export const proto = $root.proto = (() => {
+4 -5
View File
@@ -1060,22 +1060,21 @@ export const makeChatsSocket = (config: SocketConfig) => {
} }
} }
/** sending non-abt props may fix QR scan fail if server expects */ /** fetch AB props */
const fetchProps = async () => { const fetchProps = async () => {
//TODO: implement both protocol 1 and protocol 2 prop fetching, specially for abKey for WM
const resultNode = await query({ const resultNode = await query({
tag: 'iq', tag: 'iq',
attrs: { attrs: {
to: S_WHATSAPP_NET, to: S_WHATSAPP_NET,
xmlns: 'w', xmlns: 'abt',
type: 'get' type: 'get'
}, },
content: [ content: [
{ {
tag: 'props', tag: 'props',
attrs: { attrs: {
protocol: '2', protocol: '1',
hash: authState?.creds?.lastPropHash || '' ...(authState?.creds?.lastPropHash ? { hash: authState.creds.lastPropHash } : {})
} }
} }
] ]
+6
View File
@@ -343,11 +343,13 @@ export const extractGroupMetadata = (result: BinaryNode) => {
let descId: string | undefined let descId: string | undefined
let descOwner: string | undefined let descOwner: string | undefined
let descOwnerPn: string | undefined let descOwnerPn: string | undefined
let descOwnerUsername: string | undefined
let descTime: number | undefined let descTime: number | undefined
if (descChild) { if (descChild) {
desc = getBinaryNodeChildString(descChild, 'body') desc = getBinaryNodeChildString(descChild, 'body')
descOwner = descChild.attrs.participant ? jidNormalizedUser(descChild.attrs.participant) : undefined descOwner = descChild.attrs.participant ? jidNormalizedUser(descChild.attrs.participant) : undefined
descOwnerPn = descChild.attrs.participant_pn ? jidNormalizedUser(descChild.attrs.participant_pn) : undefined descOwnerPn = descChild.attrs.participant_pn ? jidNormalizedUser(descChild.attrs.participant_pn) : undefined
descOwnerUsername = descChild.attrs.participant_username || undefined
descTime = +descChild.attrs.t! descTime = +descChild.attrs.t!
descId = descChild.attrs.id descId = descChild.attrs.id
} }
@@ -362,16 +364,19 @@ export const extractGroupMetadata = (result: BinaryNode) => {
subject: group.attrs.subject!, subject: group.attrs.subject!,
subjectOwner: group.attrs.s_o, subjectOwner: group.attrs.s_o,
subjectOwnerPn: group.attrs.s_o_pn, subjectOwnerPn: group.attrs.s_o_pn,
subjectOwnerUsername: group.attrs.s_o_username,
subjectTime: +(group.attrs.s_t ?? '0'), subjectTime: +(group.attrs.s_t ?? '0'),
size: group.attrs.size ? +group.attrs.size : getBinaryNodeChildren(group, 'participant').length, size: group.attrs.size ? +group.attrs.size : getBinaryNodeChildren(group, 'participant').length,
creation: +(group.attrs.creation ?? '0'), creation: +(group.attrs.creation ?? '0'),
owner: group.attrs.creator ? jidNormalizedUser(group.attrs.creator) : undefined, owner: group.attrs.creator ? jidNormalizedUser(group.attrs.creator) : undefined,
ownerPn: group.attrs.creator_pn ? jidNormalizedUser(group.attrs.creator_pn) : undefined, ownerPn: group.attrs.creator_pn ? jidNormalizedUser(group.attrs.creator_pn) : undefined,
ownerUsername: group.attrs.creator_username || undefined,
owner_country_code: group.attrs.creator_country_code, owner_country_code: group.attrs.creator_country_code,
desc, desc,
descId, descId,
descOwner, descOwner,
descOwnerPn, descOwnerPn,
descOwnerUsername,
descTime, descTime,
linkedParent: getBinaryNodeChild(group, 'linked_parent')?.attrs.jid || undefined, linkedParent: getBinaryNodeChild(group, 'linked_parent')?.attrs.jid || undefined,
restrict: !!getBinaryNodeChild(group, 'locked'), restrict: !!getBinaryNodeChild(group, 'locked'),
@@ -386,6 +391,7 @@ export const extractGroupMetadata = (result: BinaryNode) => {
id: attrs.jid!, id: attrs.jid!,
phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined, phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined, lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
username: attrs.participant_username || attrs.username || undefined,
admin: (attrs.type || null) as GroupParticipant['admin'] admin: (attrs.type || null) as GroupParticipant['admin']
} }
}), }),
+39 -17
View File
@@ -2124,12 +2124,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
if (resolvedRemoteJid) key.remoteJid = resolvedRemoteJid if (resolvedRemoteJid) key.remoteJid = resolvedRemoteJid
if (resolvedParticipant) key.participant = resolvedParticipant if (resolvedParticipant) key.participant = resolvedParticipant
if (shouldIgnoreJid(remoteJid!) && remoteJid !== S_WHATSAPP_NET) {
logger.trace({ remoteJid }, 'ignoring receipt from jid')
await sendMessageAck(node)
return
}
const ids = [attrs.id!] const ids = [attrs.id!]
if (Array.isArray(content)) { if (Array.isArray(content)) {
const items = getBinaryNodeChildren(content[0], 'item') const items = getBinaryNodeChildren(content[0], 'item')
@@ -2205,11 +2199,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const handleNotification = async (node: BinaryNode) => { const handleNotification = async (node: BinaryNode) => {
const remoteJid = node.attrs.from const remoteJid = node.attrs.from
if (shouldIgnoreJid(remoteJid!) && remoteJid !== S_WHATSAPP_NET) {
logger.trace({ remoteJid }, 'ignored notification')
await sendMessageAck(node)
return
}
try { try {
await Promise.all([ await Promise.all([
@@ -2223,6 +2212,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
fromMe, fromMe,
participant: node.attrs.participant, participant: node.attrs.participant,
participantAlt, participantAlt,
participantUsername: node.attrs.participant_username,
addressingMode, addressingMode,
id: node.attrs.id, id: node.attrs.id,
...(msg.key || {}) ...(msg.key || {})
@@ -2231,6 +2221,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
msg.messageTimestamp = +node.attrs.t! msg.messageTimestamp = +node.attrs.t!
const fullMsg = proto.WebMessageInfo.fromObject(msg) as WAMessage const fullMsg = proto.WebMessageInfo.fromObject(msg) as WAMessage
// Preserve custom WAMessageKey fields (participantAlt, participantUsername,
// addressingMode) that proto.WebMessageInfo.fromObject strips because
// they aren't part of the proto.MessageKey schema.
fullMsg.key = msg.key
await upsertMessage(fullMsg, 'append') await upsertMessage(fullMsg, 'append')
} }
}) })
@@ -2241,12 +2235,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
} }
const handleMessage = async (node: BinaryNode) => { const handleMessage = async (node: BinaryNode) => {
if (shouldIgnoreJid(node.attrs.from!) && node.attrs.from !== S_WHATSAPP_NET) {
logger.trace({ from: node.attrs.from }, 'ignored message')
await sendMessageAck(node)
return
}
const encNode = getBinaryNodeChild(node, 'enc') const encNode = getBinaryNodeChild(node, 'enc')
const unavailableNode = getBinaryNodeChild(node, 'unavailable') const unavailableNode = getBinaryNodeChild(node, 'unavailable')
@@ -3100,6 +3088,40 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
identifier: string, identifier: string,
exec: (node: BinaryNode) => Promise<void> exec: (node: BinaryNode) => Promise<void>
) => { ) => {
// Fast path: ack and drop ignored JIDs before entering the buffer/queue.
// Skips type='call' so call events are never silently dropped via
// shouldIgnoreJid (preserves InfiniteAPI's pre-existing behavior).
// Wrapped in try/catch so a throw from shouldIgnoreJid (user callback)
// or sendMessageAck (e.g. websocket closed) is routed through
// onUnexpectedError instead of becoming an unhandled rejection —
// matches the protection processNodeWithBuffer provides.
if (type !== 'call') {
try {
const from = node.attrs.from
let ignoreJid = from
if (type === 'receipt' && from) {
const attrs = node.attrs
const isLid = attrs.from!.includes('lid')
const isNodeFromMe = areJidsSameUser(
attrs.participant || attrs.from,
isLid ? authState.creds.me?.lid : authState.creds.me?.id
)
ignoreJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient
}
if (ignoreJid && ignoreJid !== S_WHATSAPP_NET && shouldIgnoreJid(ignoreJid)) {
// Plain ack (no NACK error code) preserves InfiniteAPI's prior
// behavior — ignored stanzas are an intentional drop, not a
// processing failure, so we don't want server-side retries.
await sendMessageAck(node)
return
}
} catch (error) {
onUnexpectedError(error as Error, identifier)
return
}
}
const isOffline = !!node.attrs.offline const isOffline = !!node.attrs.offline
if (isOffline) { if (isOffline) {
+2
View File
@@ -9,6 +9,8 @@ export interface Contact {
name?: string name?: string
/** name of the contact, the contact has set on their own on WA */ /** name of the contact, the contact has set on their own on WA */
notify?: string notify?: string
/** username associated with this contact, when provided by WA */
username?: string
/** I have no idea */ /** I have no idea */
verifiedName?: string verifiedName?: string
// Baileys Added // Baileys Added
+2
View File
@@ -81,6 +81,7 @@ export type BaileysEventMap = {
id: string id: string
author: string author: string
authorPn?: string authorPn?: string
authorUsername?: string
participants: GroupParticipant[] participants: GroupParticipant[]
action: ParticipantAction action: ParticipantAction
} }
@@ -88,6 +89,7 @@ export type BaileysEventMap = {
id: string id: string
author: string author: string
authorPn?: string authorPn?: string
authorUsername?: string
participant: string participant: string
participantPn?: string participantPn?: string
action: RequestJoinAction action: RequestJoinAction
+4
View File
@@ -20,17 +20,20 @@ export interface GroupMetadata {
addressingMode?: WAMessageAddressingMode addressingMode?: WAMessageAddressingMode
owner: string | undefined owner: string | undefined
ownerPn?: string | undefined ownerPn?: string | undefined
ownerUsername?: string | undefined
owner_country_code?: string | undefined owner_country_code?: string | undefined
subject: string subject: string
/** group subject owner */ /** group subject owner */
subjectOwner?: string subjectOwner?: string
subjectOwnerPn?: string subjectOwnerPn?: string
subjectOwnerUsername?: string
/** group subject modification date */ /** group subject modification date */
subjectTime?: number subjectTime?: number
creation?: number creation?: number
desc?: string desc?: string
descOwner?: string descOwner?: string
descOwnerPn?: string descOwnerPn?: string
descOwnerUsername?: string
descId?: string descId?: string
descTime?: number descTime?: number
/** if this group is part of a community, it returns the jid of the community to which it belongs */ /** if this group is part of a community, it returns the jid of the community to which it belongs */
@@ -56,6 +59,7 @@ export interface GroupMetadata {
/** the person who added you to group or changed some setting in group */ /** the person who added you to group or changed some setting in group */
author?: string author?: string
authorPn?: string authorPn?: string
authorUsername?: string
} }
export interface WAGroupCreateResponse { export interface WAGroupCreateResponse {
+2
View File
@@ -19,7 +19,9 @@ export type WAContactMessage = proto.Message.IContactMessage
export type WAContactsArrayMessage = proto.Message.IContactsArrayMessage export type WAContactsArrayMessage = proto.Message.IContactsArrayMessage
export type WAMessageKey = proto.IMessageKey & { export type WAMessageKey = proto.IMessageKey & {
remoteJidAlt?: string remoteJidAlt?: string
remoteJidUsername?: string
participantAlt?: string participantAlt?: string
participantUsername?: string
server_id?: string server_id?: string
addressingMode?: string addressingMode?: string
isViewOnce?: boolean // TODO: remove out of the message key, place in WebMessageInfo isViewOnce?: boolean // TODO: remove out of the message key, place in WebMessageInfo
+1
View File
@@ -1083,6 +1083,7 @@ export const processSyncAction = (
action.lidContactAction.firstName || action.lidContactAction.firstName ||
action.lidContactAction.username || action.lidContactAction.username ||
undefined, undefined,
username: action.lidContactAction.username || undefined,
lid: id!, lid: id!,
phoneNumber: undefined phoneNumber: undefined
} }
+4
View File
@@ -242,10 +242,14 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin
const key: WAMessageKey = { const key: WAMessageKey = {
remoteJid: chatId, remoteJid: chatId,
remoteJidAlt: !isJidGroup(chatId) ? addressingContext.senderAlt : undefined, remoteJidAlt: !isJidGroup(chatId) ? addressingContext.senderAlt : undefined,
remoteJidUsername: !isJidGroup(chatId)
? stanza.attrs.peer_recipient_username || stanza.attrs.recipient_username
: undefined,
fromMe, fromMe,
id: msgId, id: msgId,
participant, participant,
participantAlt: isJidGroup(chatId) ? addressingContext.senderAlt : undefined, participantAlt: isJidGroup(chatId) ? addressingContext.senderAlt : undefined,
participantUsername: stanza.attrs.participant ? stanza.attrs.participant_username : undefined,
addressingMode: addressingContext.addressingMode, addressingMode: addressingContext.addressingMode,
...(msgType === 'newsletter' && stanza.attrs.server_id ? { server_id: stanza.attrs.server_id } : {}) ...(msgType === 'newsletter' && stanza.attrs.server_id ? { server_id: stanza.attrs.server_id } : {})
} }
+12 -11
View File
@@ -1,5 +1,6 @@
import { pipeline } from 'stream/promises'
import { promisify } from 'util' import { promisify } from 'util'
import { inflate } from 'zlib' import { createInflate, inflate } from 'zlib'
import { proto } from '../../WAProto/index.js' import { proto } from '../../WAProto/index.js'
import type { Chat, Contact, LIDMapping, WAMessage } from '../Types' import type { Chat, Contact, LIDMapping, WAMessage } from '../Types'
import { WAMessageStubType } from '../Types' import { WAMessageStubType } from '../Types'
@@ -29,16 +30,15 @@ const inflatePromise = promisify(inflate)
*/ */
export const downloadHistory = async (msg: proto.Message.IHistorySyncNotification, options: RequestInit) => { export const downloadHistory = async (msg: proto.Message.IHistorySyncNotification, options: RequestInit) => {
const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options }) const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options })
const bufferArray: Buffer[] = [] // Pipe decrypted stream directly through zlib inflate.
for await (const chunk of stream) { // Avoids allocating an intermediate buffer for the compressed payload —
bufferArray.push(chunk) // memory peaks during 50MB history syncs drop ~50% (PR upstream #2333).
} const inflater = createInflate()
const chunks: Buffer[] = []
let buffer: Buffer = Buffer.concat(bufferArray) inflater.on('data', (chunk: Buffer) => chunks.push(chunk))
await pipeline(stream, inflater)
// decompress buffer
buffer = await inflatePromise(buffer)
const buffer = Buffer.concat(chunks)
const syncData = proto.HistorySync.decode(buffer) const syncData = proto.HistorySync.decode(buffer)
return syncData return syncData
} }
@@ -315,6 +315,7 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
contacts.push({ contacts.push({
id: chatId, id: chatId,
name: chat.displayName || chat.name || chat.username || undefined, name: chat.displayName || chat.name || chat.username || undefined,
username: chat.username || undefined,
lid: chat.lidJid || chat.accountLid || undefined, lid: chat.lidJid || chat.accountLid || undefined,
phoneNumber: chat.pnJid || undefined phoneNumber: chat.pnJid || undefined
}) })
@@ -360,7 +361,7 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
} }
} }
chats.push({ ...chat }) chats.push(chat)
} }
break break
+1 -1
View File
@@ -618,7 +618,7 @@ export const downloadEncryptedContent = async (
const output = new Transform({ const output = new Transform({
transform(chunk, _, callback) { transform(chunk, _, callback) {
let data = Buffer.concat([remainingBytes, chunk]) let data = remainingBytes.length ? Buffer.concat([remainingBytes, chunk]) : chunk
const decryptLength = toSmallestChunkSize(data.length) const decryptLength = toSmallestChunkSize(data.length)
remainingBytes = data.slice(decryptLength) remainingBytes = data.slice(decryptLength)
+9 -1
View File
@@ -763,12 +763,19 @@ const processMessage = async (
id: jid, id: jid,
author: message.key.participant!, author: message.key.participant!,
authorPn: message.key.participantAlt!, authorPn: message.key.participantAlt!,
authorUsername: message.key.participantUsername!,
participants, participants,
action action
}) })
const emitGroupUpdate = (update: Partial<GroupMetadata>) => { const emitGroupUpdate = (update: Partial<GroupMetadata>) => {
ev.emit('groups.update', [ ev.emit('groups.update', [
{ id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt } {
id: jid,
...update,
author: message.key.participant ?? undefined,
authorPn: message.key.participantAlt,
authorUsername: message.key.participantUsername
}
]) ])
} }
@@ -777,6 +784,7 @@ const processMessage = async (
id: jid, id: jid,
author: message.key.participant!, author: message.key.participant!,
authorPn: message.key.participantAlt!, authorPn: message.key.participantAlt!,
authorUsername: message.key.participantUsername!,
participant: participant.lid, participant: participant.lid,
participantPn: participant.pn, participantPn: participant.pn,
action, action,
+1
View File
@@ -46,6 +46,7 @@ export const processContactAction = (
{ {
id, id,
name: action.fullName || action.firstName || action.username || undefined, name: action.fullName || action.firstName || action.username || undefined,
username: action.username || undefined,
lid: lidJid || undefined, lid: lidJid || undefined,
phoneNumber phoneNumber
} }
+29 -3
View File
@@ -13,11 +13,37 @@ export class USyncContactProtocol implements USyncQueryProtocol {
} }
getUserElement(user: USyncUser): BinaryNode { getUserElement(user: USyncUser): BinaryNode {
//TODO: Implement type / username fields (not yet supported) if (user.phone) {
return {
tag: 'contact',
attrs: {},
content: user.phone
}
}
if (user.username) {
return {
tag: 'contact',
attrs: {
username: user.username,
...(user.usernameKey ? { pin: user.usernameKey } : {}),
...(user.lid ? { lid: user.lid } : {})
}
}
}
if (user.type) {
return {
tag: 'contact',
attrs: {
type: user.type
}
}
}
return { return {
tag: 'contact', tag: 'contact',
attrs: {}, attrs: {}
content: user.phone
} }
} }
@@ -0,0 +1,28 @@
import type { USyncQueryProtocol } from '../../Types/USync'
import { assertNodeErrorFree, type BinaryNode } from '../../WABinary'
import { USyncUser } from '../USyncUser'
export class USyncUsernameProtocol implements USyncQueryProtocol {
name = 'username'
getQueryElement(): BinaryNode {
return {
tag: 'username',
attrs: {}
}
}
getUserElement(user: USyncUser): BinaryNode | null {
void user
return null
}
parser(node: BinaryNode): string | null {
if (node.tag === 'username') {
assertNodeErrorFree(node)
return typeof node.content === 'string' ? node.content : null
}
return null
}
}
+1
View File
@@ -2,3 +2,4 @@ export * from './USyncDeviceProtocol'
export * from './USyncContactProtocol' export * from './USyncContactProtocol'
export * from './USyncStatusProtocol' export * from './USyncStatusProtocol'
export * from './USyncDisappearingModeProtocol' export * from './USyncDisappearingModeProtocol'
export * from './USyncUsernameProtocol'
+7 -1
View File
@@ -6,7 +6,8 @@ import {
USyncContactProtocol, USyncContactProtocol,
USyncDeviceProtocol, USyncDeviceProtocol,
USyncDisappearingModeProtocol, USyncDisappearingModeProtocol,
USyncStatusProtocol USyncStatusProtocol,
USyncUsernameProtocol
} from './Protocols' } from './Protocols'
import { USyncUser } from './USyncUser' import { USyncUser } from './USyncUser'
@@ -137,4 +138,9 @@ export class USyncQuery {
this.protocols.push(new USyncLIDProtocol()) this.protocols.push(new USyncLIDProtocol())
return this return this
} }
withUsernameProtocol() {
this.protocols.push(new USyncUsernameProtocol())
return this
}
} }
+12
View File
@@ -2,6 +2,8 @@ export class USyncUser {
id?: string id?: string
lid?: string lid?: string
phone?: string phone?: string
username?: string
usernameKey?: string
type?: string type?: string
personaId?: string personaId?: string
@@ -20,6 +22,16 @@ export class USyncUser {
return this return this
} }
withUsername(username: string) {
this.username = username
return this
}
withUsernameKey(usernameKey: string) {
this.usernameKey = usernameKey
return this
}
withType(type: string) { withType(type: string) {
this.type = type this.type = type
return this return this
+209
View File
@@ -0,0 +1,209 @@
import Long from 'long'
import $protobuf from 'protobufjs/minimal.js'
const $util = $protobuf.util
// proto implementation
function longToStringOld(value: any, unsigned?: boolean): string {
if (typeof value === 'string') return value
if (typeof value === 'number') return String(value)
if (!$util.Long) return String(value)
const normalized = ($util.Long as any).fromValue(value)
const prepared =
unsigned && normalized && typeof normalized.toUnsigned === 'function' ? normalized.toUnsigned() : normalized
return prepared.toString()
}
// bigint implementation
function longToStringNew(value: any, unsigned?: boolean): string {
if (typeof value === 'string') return value
if (typeof value === 'number') return String(value)
if (value && typeof value.low === 'number' && typeof value.high === 'number') {
const high = value.high | 0
const lo = BigInt(value.low >>> 0)
const hi = BigInt(value.high >>> 0)
const combined = (hi << 32n) | lo
if (!unsigned && high < 0) {
return (combined - (1n << 64n)).toString()
}
return combined.toString()
}
return String(value)
}
function longToNumberOld(value: any, unsigned?: boolean): number {
if (typeof value === 'number') return value
if (typeof value === 'string') return Number(value)
if (!$util.Long) return Number(value)
const normalized = ($util.Long as any).fromValue(value)
const prepared =
unsigned && normalized && typeof normalized.toUnsigned === 'function'
? normalized.toUnsigned()
: typeof normalized.toSigned === 'function'
? normalized.toSigned()
: normalized
return prepared.toNumber()
}
function longToNumberNew(value: any, unsigned?: boolean): number {
if (typeof value === 'number') return value
if (typeof value === 'string') return Number(value)
if (value && typeof value.low === 'number' && typeof value.high === 'number') {
const high = value.high | 0
const lo = BigInt(value.low >>> 0)
const hi = BigInt(value.high >>> 0)
const combined = (hi << 32n) | lo
if (!unsigned && high < 0) {
return Number(combined - (1n << 64n))
}
return Number(combined)
}
return Number(value)
}
describe('BigInt vs Long equivalence validation', () => {
// Test cases: [description, Long value, unsigned flag]
const testCases: [string, Long, boolean][] = [
// Basic values
['zero unsigned', Long.fromNumber(0, true), true],
['zero signed', Long.fromNumber(0, false), false],
['one unsigned', Long.fromNumber(1, true), true],
['one signed', Long.fromNumber(1, false), false],
// Typical WhatsApp timestamps (seconds since epoch)
['timestamp 2023', Long.fromNumber(1700000000, true), true],
['timestamp 2025', Long.fromNumber(1750000000, true), true],
['timestamp as signed', Long.fromNumber(1700000000, false), false],
// File sizes
['small file', Long.fromNumber(1024, true), true],
['medium file 10MB', Long.fromNumber(10485760, true), true],
['large file 2GB', Long.fromNumber(2147483648, true), true],
// Boundary values - 32-bit
['max int32', Long.fromNumber(2147483647, false), false],
['min int32', Long.fromNumber(-2147483648, false), false],
['max uint32', Long.fromNumber(4294967295, true), true],
// Around MAX_SAFE_INTEGER
['MAX_SAFE_INTEGER', Long.fromString('9007199254740991', false), false],
['MAX_SAFE_INTEGER + 1', Long.fromString('9007199254740992', false), false],
['MAX_SAFE_INTEGER unsigned', Long.fromString('9007199254740991', true), true],
// Large unsigned values
['large unsigned', Long.fromString('9999999999999999999', true), true],
['max uint64', Long.fromString('18446744073709551615', true), true],
['max uint64 - 1', Long.fromString('18446744073709551614', true), true],
// Signed negative values
['negative one', Long.fromNumber(-1, false), false],
['negative small', Long.fromNumber(-100, false), false],
['negative large', Long.fromNumber(-2147483648, false), false],
['min int64', Long.fromString('-9223372036854775808', false), false],
['max int64', Long.fromString('9223372036854775807', false), false],
['-1 as signed Long', Long.fromBits(-1, -1, false), false],
// Tricky bit patterns
['high=1, low=0', Long.fromBits(0, 1, true), true],
['high=0, low=-1 (0xFFFFFFFF)', Long.fromBits(-1, 0, true), true],
['high=-1, low=-1 unsigned (max uint64)', Long.fromBits(-1, -1, true), true],
['high=0x7FFFFFFF, low=0xFFFFFFFF (max int64)', Long.fromBits(-1, 0x7fffffff, false), false],
['high=0x80000000, low=0 (min int64)', Long.fromBits(0, -2147483648, false), false],
// Powers of 2
['2^32', Long.fromString('4294967296', true), true],
['2^32 signed', Long.fromString('4294967296', false), false],
['2^48', Long.fromString('281474976710656', true), true],
['2^63', Long.fromString('9223372036854775808', true), true]
]
describe('longToString equivalence', () => {
for (const [desc, longVal, unsigned] of testCases) {
it(`${desc}: Long(${longVal.low}, ${longVal.high}, ${unsigned})`, () => {
const oldResult = longToStringOld(longVal, unsigned)
const newResult = longToStringNew(longVal, unsigned)
expect(newResult).toBe(oldResult)
})
}
})
describe('longToNumber equivalence', () => {
for (const [desc, longVal, unsigned] of testCases) {
it(`${desc}: Long(${longVal.low}, ${longVal.high}, ${unsigned})`, () => {
const oldResult = longToNumberOld(longVal, unsigned)
const newResult = longToNumberNew(longVal, unsigned)
// For large values, both may lose precision equally (Number limitation)
// So we compare string representations
expect(newResult.toString()).toBe(oldResult.toString())
})
}
})
describe('non-Long inputs (fallback paths)', () => {
it('string passthrough', () => {
expect(longToStringNew('12345')).toBe('12345')
expect(longToStringNew('0')).toBe('0')
})
it('number passthrough', () => {
expect(longToStringNew(42)).toBe('42')
expect(longToStringNew(0)).toBe('0')
expect(longToStringNew(-1)).toBe('-1')
})
it('null/undefined fallback', () => {
expect(longToStringNew(null)).toBe('null')
expect(longToStringNew(undefined)).toBe('undefined')
})
it('object without low/high', () => {
expect(longToStringNew({ foo: 'bar' })).toBe('[object Object]')
})
// Defensive: a raw {low, high} JSON object can carry `high` as an
// unsigned 32-bit value (Long.fromBits would normalize via `| 0`,
// but plain JSON deserialization does not). Without `value.high | 0`
// the sign-bit check fails and the result is interpreted as unsigned.
// Upstream Baileys PR #2333 has this latent bug; InfiniteAPI fixes it.
it('signed sign-bit detection on raw unsigned high (-1 as {low: -1, high: 4294967295})', () => {
const raw = { low: -1, high: 0xffffffff }
// signed: -1
expect(longToStringNew(raw, false)).toBe('-1')
// unsigned: max uint64
expect(longToStringNew(raw, true)).toBe('18446744073709551615')
})
it('signed sign-bit detection on raw unsigned high (-4294967296 as {low: 0, high: 4294967295})', () => {
const raw = { low: 0, high: 0xffffffff }
// signed: -4294967296 (high word = -1 after normalization)
expect(longToStringNew(raw, false)).toBe('-4294967296')
// unsigned: 0xFFFFFFFF00000000 = 18446744069414584320
expect(longToStringNew(raw, true)).toBe('18446744069414584320')
})
})
describe('fuzz: random Long values', () => {
it('100 random unsigned values match', () => {
for (let i = 0; i < 100; i++) {
const low = (Math.random() * 0xffffffff) | 0
const high = (Math.random() * 0xffffffff) | 0
const longVal = Long.fromBits(low, high, true)
const oldResult = longToStringOld(longVal, true)
const newResult = longToStringNew(longVal, true)
expect(newResult).toBe(oldResult)
}
})
it('100 random signed values match', () => {
for (let i = 0; i < 100; i++) {
const low = (Math.random() * 0xffffffff) | 0
const high = (Math.random() * 0xffffffff) | 0
const longVal = Long.fromBits(low, high, false)
const oldResult = longToStringOld(longVal, false)
const newResult = longToStringNew(longVal, false)
expect(newResult).toBe(oldResult)
}
})
})
})
@@ -0,0 +1,146 @@
import { jest } from '@jest/globals'
import { proto, type WAMessage } from '../..'
import { DEFAULT_CONNECTION_CONFIG } from '../../Defaults'
import makeWASocket from '../../Socket'
import { makeSession, mockWebSocket } from '../TestUtils/session'
mockWebSocket()
// chats.ts treats a connection as a reconnection when EITHER signal is true:
// 1. authState.creds.accountSyncCounter > 0
// (at least one full history sync completed in a previous session)
// 2. socketSkippedOfflineBuffer (forwarded from socket.ts as `skipOfflineBuffer`)
// (socket.ts already decided to skip the offline buffer, e.g. because
// routingInfo was stale and was discarded on startup)
// On reconnection, AwaitingInitialSync is skipped and the buffer is flushed
// immediately so live messages are not held in the initial-sync window.
describe('Reconnection Sync Skip', () => {
it('should skip the history sync wait on reconnection (accountSyncCounter > 0)', async () => {
const { state, clear } = await makeSession()
state.creds.me = { id: '1234567890:1@s.whatsapp.net', name: 'Test User' }
// Simulate a session that has already synced before
state.creds.accountSyncCounter = 1
const sock = makeWASocket({
...DEFAULT_CONNECTION_CONFIG,
auth: state
})
const messageListener = jest.fn()
sock.ev.on('messages.upsert', messageListener)
// Simulate receiving pending notifications (triggers AwaitingInitialSync)
sock.ev.emit('connection.update', { receivedPendingNotifications: true })
// Emit a message immediately after — if the wait is skipped,
// the buffer should already be flushed and this message should be delivered.
const msg = proto.WebMessageInfo.fromObject({
key: { remoteJid: '1234567890@s.whatsapp.net', fromMe: false, id: 'MSG_AFTER_RECONNECT' },
messageTimestamp: Date.now() / 1000,
message: { conversation: 'Hello after reconnect' }
}) as WAMessage
sock.ev.emit('messages.upsert', { messages: [msg], type: 'notify' })
await new Promise(resolve => setTimeout(resolve, 50))
// Message should be delivered immediately, NOT buffered
expect(messageListener).toHaveBeenCalledTimes(1)
expect(messageListener).toHaveBeenCalledWith(
expect.objectContaining({
messages: expect.arrayContaining([expect.objectContaining({ key: msg.key })]),
type: 'notify'
})
)
await sock.end(new Error('Test completed'))
await clear()
})
it('should skip the history sync wait when socketSkippedOfflineBuffer is true (stale routingInfo, accountSyncCounter === 0)', async () => {
const { state, clear } = await makeSession()
state.creds.me = { id: '1234567890:1@s.whatsapp.net', name: 'Test User' }
// Fresh-looking session from the counter perspective
state.creds.accountSyncCounter = 0
// But routingInfo is present and we ask the socket to discard it on start.
// socket.ts will set hadStaleRoutingInfo=true → skipOfflineBuffer=true,
// which chats.ts forwards as socketSkippedOfflineBuffer. Without this
// branch, the buffers would be misaligned (offline buffer skipped while
// AwaitingInitialSync still waits) and live messages would stall.
state.creds.routingInfo = Buffer.from([1, 2, 3, 4])
const sock = makeWASocket({
...DEFAULT_CONNECTION_CONFIG,
auth: state,
clearRoutingInfoOnStart: true
})
const messageListener = jest.fn()
sock.ev.on('messages.upsert', messageListener)
sock.ev.emit('connection.update', { receivedPendingNotifications: true })
const msg = proto.WebMessageInfo.fromObject({
key: { remoteJid: '1234567890@s.whatsapp.net', fromMe: false, id: 'MSG_AFTER_STALE_ROUTING' },
messageTimestamp: Date.now() / 1000,
message: { conversation: 'Hello after stale routing reconnect' }
}) as WAMessage
sock.ev.emit('messages.upsert', { messages: [msg], type: 'notify' })
await new Promise(resolve => setTimeout(resolve, 50))
expect(messageListener).toHaveBeenCalledTimes(1)
expect(messageListener).toHaveBeenCalledWith(
expect.objectContaining({
messages: expect.arrayContaining([expect.objectContaining({ key: msg.key })]),
type: 'notify'
})
)
await sock.end(new Error('Test completed'))
await clear()
})
it('should still wait for history sync on fresh pairing (both signals false)', async () => {
const { state, clear } = await makeSession()
state.creds.me = { id: '1234567890:1@s.whatsapp.net', name: 'Test User' }
// Fresh pairing — both reconnect signals are false:
// accountSyncCounter is 0 (default) and no stale routingInfo to clear.
state.creds.accountSyncCounter = 0
const sock = makeWASocket({
...DEFAULT_CONNECTION_CONFIG,
auth: state
})
const messageListener = jest.fn()
sock.ev.on('messages.upsert', messageListener)
sock.ev.emit('connection.update', { receivedPendingNotifications: true })
const msg = proto.WebMessageInfo.fromObject({
key: { remoteJid: '1234567890@s.whatsapp.net', fromMe: false, id: 'MSG_DURING_INITIAL_SYNC' },
messageTimestamp: Date.now() / 1000,
message: { conversation: 'Hello during initial sync' }
}) as WAMessage
sock.ev.emit('messages.upsert', { messages: [msg], type: 'notify' })
await new Promise(resolve => setTimeout(resolve, 50))
// Message should be BUFFERED (not delivered) because we're waiting for history sync
expect(messageListener).toHaveBeenCalledTimes(0)
// Drain the 2s AwaitingInitialSync timeout (chats.ts:1522) so its callback
// doesn't fire after Jest considers the test done — otherwise we get a
// "Cannot log after tests are done" warning from the post-teardown flush
// and the suite is flagged with --detectOpenHandles. After the timer
// fires, syncState becomes Online and the buffer flushes; that delivery
// is post-assertion and irrelevant to the test goal.
await new Promise(resolve => setTimeout(resolve, 2_100))
await sock.end(new Error('Test completed'))
await clear()
}, 10_000)
})
+88
View File
@@ -1,3 +1,4 @@
import Long from 'long'
import '../index.js' import '../index.js'
import { proto } from '../../WAProto/index.js' import { proto } from '../../WAProto/index.js'
@@ -28,4 +29,91 @@ describe('proto serialization', () => {
const json = message.toJSON() const json = message.toJSON()
expect(json.message?.imageMessage?.fileLength).toBe('1234567890123456789') expect(json.message?.imageMessage?.fileLength).toBe('1234567890123456789')
}) })
it('converts Long objects to strings correctly via BigInt fast path', () => {
const message = proto.WebMessageInfo.fromObject({
key: {
remoteJid: '123@s.whatsapp.net',
id: 'ABC123',
fromMe: false
},
messageTimestamp: Long.fromNumber(1700000000, true),
message: {
imageMessage: {
fileLength: Long.fromString('9876543210', true)
}
}
})
const json = message.toJSON()
expect(json.messageTimestamp).toBe('1700000000')
expect(json.message?.imageMessage?.fileLength).toBe('9876543210')
})
it('handles large unsigned Long values (> MAX_SAFE_INTEGER)', () => {
const message = proto.WebMessageInfo.fromObject({
key: {
remoteJid: '123@s.whatsapp.net',
id: 'ABC123',
fromMe: false
},
messageTimestamp: Long.fromString('18446744073709551615', true), // max uint64
message: {
imageMessage: {
fileLength: Long.fromString('9999999999999999999', true)
}
}
})
const json = message.toJSON()
expect(json.messageTimestamp).toBe('18446744073709551615')
expect(json.message?.imageMessage?.fileLength).toBe('9999999999999999999')
})
it('handles zero and small Long values', () => {
const message = proto.WebMessageInfo.fromObject({
key: {
remoteJid: '123@s.whatsapp.net',
id: 'ABC123',
fromMe: false
},
messageTimestamp: Long.fromNumber(0, true),
message: {
imageMessage: {
fileLength: Long.fromNumber(1, true)
}
}
})
const json = message.toJSON()
expect(json.messageTimestamp).toBe('0')
expect(json.message?.imageMessage?.fileLength).toBe('1')
})
it('roundtrips encode/decode with Long fields preserving values (> MAX_SAFE_INTEGER)', () => {
// Use uint64 values above Number.MAX_SAFE_INTEGER (2^53 - 1) so the
// roundtrip actually exercises the BigInt fast path on decode->toJSON.
// Safe integers would silently work even if longToString fell back to
// Number(value).
const original = proto.WebMessageInfo.fromObject({
key: {
remoteJid: '123@s.whatsapp.net',
id: 'ABC123',
fromMe: false
},
messageTimestamp: Long.fromString('18446744073709551615', true),
message: {
imageMessage: {
fileLength: Long.fromString('9999999999999999999', true)
}
}
})
const encoded = proto.WebMessageInfo.encode(original).finish()
const decoded = proto.WebMessageInfo.decode(encoded)
const json = decoded.toJSON()
expect(json.messageTimestamp).toBe('18446744073709551615')
expect(json.message?.imageMessage?.fileLength).toBe('9999999999999999999')
})
}) })