Compare commits

..

2 Commits

Author SHA1 Message Date
Renato Alcara 4a745f2167 fix: move clearTimeout to finally block in fetchLatestBaileysVersion
Ensures timer cleanup on both success and fetch rejection paths,
preventing dangling timers when DNS/network fails before abort fires.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 09:37:48 -03:00
Renato Alcara a1def8c2c0 fix: add timeout to fetchLatestBaileysVersion to prevent hanging connections
AbortController with 5s default timeout prevents indefinite blocking
when GitHub is unreachable (DNS failure, network issues). Falls back
to bundled version via existing catch block. Ref: Baileys#2385.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 09:27:16 -03:00
15 changed files with 189 additions and 1375 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
syntax = "proto3";
package proto;
/// WhatsApp Version: 2.3000.1034274421
/// WhatsApp Version: 2.3000.1034238531
message ADVDeviceIdentity {
optional uint32 rawId = 1;
+1 -1
View File
@@ -1 +1 @@
{"version":[2,3000,1034279434]}
{"version":[2,3000,1034258243]}
+2 -2
View File
@@ -67,7 +67,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
emitOwnEvents: true,
defaultQueryTimeoutMs: 30_000,
customUploadHosts: [],
retryRequestDelayMs: 150,
retryRequestDelayMs: 250,
maxMsgRetryCount: 5,
fireInitQueries: true,
auth: undefined as unknown as AuthenticationState,
@@ -78,7 +78,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
shouldSyncHistoryMessage: () => true,
shouldIgnoreJid: () => false,
linkPreviewImageThumbnailWidth: 192,
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 1000 },
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
generateHighQualityLinkPreview: false,
enableAutoSessionRecreation: true,
enableRecentMessageCache: true,
+19 -46
View File
@@ -50,8 +50,7 @@ import {
isMissingKeyError,
MAX_SYNC_ATTEMPTS,
newLTHashState,
processSyncAction,
resolveLidToPn
processSyncAction
} from '../Utils'
import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex'
import processMessage from '../Utils/process-message'
@@ -103,14 +102,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
/** this mutex ensures that messages from the same chat are processed in order, while allowing parallel processing of messages from different chats */
const messageMutex = makeKeyedMutex()
/** this mutex ensures that receipts from the same chat are processed in order, while allowing parallel processing across chats */
const receiptMutex = makeKeyedMutex()
/** this mutex ensures that receipts are processed in order */
const receiptMutex = makeMutex()
/** this mutex ensures that app state patches are processed in order */
const appStatePatchMutex = makeMutex()
/** this mutex ensures that notifications from the same chat are processed in order, while allowing parallel processing across chats */
const notificationMutex = makeKeyedMutex()
/** this mutex ensures that notifications are processed in order */
const notificationMutex = makeMutex()
// Timeout for AwaitingInitialSync state
let awaitingSyncTimeout: NodeJS.Timeout | undefined
@@ -758,16 +757,9 @@ export const makeChatsSocket = (config: SocketConfig) => {
}, authState?.creds?.me?.id || 'resync-app-state')
const { onMutation } = newAppStateChunkHandler(isInitialSync)
const lidMapping = signalRepository.lidMapping
for (const key in globalMutationMap) {
const mutation = globalMutationMap[key]
if (!mutation) continue
// Normalize LID→PN in sync action index[1] (chat/contact ID)
if (mutation.index[1] && isAnyLidUser(mutation.index[1])) {
const resolved = await resolveLidToPn(mutation.index[1], lidMapping, logger)
if (resolved) mutation.index[1] = resolved
}
onMutation(mutation)
}
}
@@ -920,16 +912,16 @@ export const makeChatsSocket = (config: SocketConfig) => {
})
}
const handlePresenceUpdate = async ({ tag, attrs, content }: BinaryNode) => {
const handlePresenceUpdate = ({ tag, attrs, content }: BinaryNode) => {
let presence: PresenceData | undefined
const rawJid = attrs.from
const rawParticipant = attrs.participant || attrs.from
if (!rawJid) {
const jid = attrs.from
const participant = attrs.participant || attrs.from
if (!jid) {
logger.warn({ attrs }, 'handlePresenceUpdate: jid (attrs.from) is missing, skipping')
return
}
if (shouldIgnoreJid(rawJid) && rawJid !== S_WHATSAPP_NET) {
if (shouldIgnoreJid(jid) && jid !== S_WHATSAPP_NET) {
return
}
@@ -941,7 +933,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
} else if (Array.isArray(content)) {
const [firstChild] = content
if (!firstChild) {
logger.warn({ jid: rawJid }, 'handlePresenceUpdate: firstChild content is empty, skipping')
logger.warn({ jid }, 'handlePresenceUpdate: firstChild content is empty, skipping')
return
}
@@ -960,19 +952,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
}
if (presence) {
if (!rawParticipant) {
logger.warn({ jid: rawJid }, 'handlePresenceUpdate: participant is missing, skipping')
if (!participant) {
logger.warn({ jid }, 'handlePresenceUpdate: participant is missing, skipping')
return
}
// Resolve LID→PN so consumers always see phone-number JIDs
const lidMapping = signalRepository.lidMapping
const [jid, participant] = await Promise.all([
resolveLidToPn(rawJid, lidMapping, logger),
resolveLidToPn(rawParticipant, lidMapping, logger)
])
ev.emit('presence.update', { id: jid!, presences: { [participant!]: presence } })
ev.emit('presence.update', { id: jid, presences: { [participant]: presence } })
}
}
@@ -1046,16 +1031,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
undefined,
logger
)
const lidMapping = signalRepository.lidMapping
for (const key in mutationMap) {
const mutation = mutationMap[key]!
// Normalize LID→PN in sync action index[1] (chat/contact ID)
if (mutation.index[1] && isAnyLidUser(mutation.index[1])) {
const resolved = await resolveLidToPn(mutation.index[1], lidMapping, logger)
if (resolved) mutation.index[1] = resolved
}
onMutation(mutation)
onMutation(mutationMap[key]!)
}
}
}
@@ -1399,12 +1376,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
}
})
ws.on('CB:presence', (node: BinaryNode) => {
handlePresenceUpdate(node).catch(err => onUnexpectedError(err, 'handling presence update'))
})
ws.on('CB:chatstate', (node: BinaryNode) => {
handlePresenceUpdate(node).catch(err => onUnexpectedError(err, 'handling chatstate update'))
})
ws.on('CB:presence', handlePresenceUpdate)
ws.on('CB:chatstate', handlePresenceUpdate)
ws.on('CB:ib,,dirty', async (node: BinaryNode) => {
const { attrs } = getBinaryNodeChild(node, 'dirty')!
@@ -1522,7 +1495,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
awaitingSyncTimeout = setTimeout(() => {
if (syncState === SyncState.AwaitingInitialSync) {
logger.warn('Timeout in AwaitingInitialSync (2s), forcing state to Online and flushing buffer')
logger.warn('Timeout in AwaitingInitialSync (4s), forcing state to Online and flushing buffer')
syncState = SyncState.Online
ev.flush()
@@ -1532,7 +1505,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1
ev.emit('creds.update', { accountSyncCounter })
}
}, 2_000)
}, 4_000)
})
// When an app state sync key arrives (myAppStateKeyId is set) and there are
+7 -43
View File
@@ -1,7 +1,7 @@
import { proto } from '../../WAProto/index.js'
import type { GroupMetadata, GroupParticipant, ParticipantAction, SocketConfig, WAMessageKey } from '../Types'
import { WAMessageAddressingMode, WAMessageStubType } from '../Types'
import { generateMessageIDV2, resolveLidToPn, unixTimestampSeconds } from '../Utils'
import { generateMessageIDV2, unixTimestampSeconds } from '../Utils'
import {
type BinaryNode,
getBinaryNodeChild,
@@ -17,43 +17,6 @@ import { makeChatsSocket } from './chats'
export const makeGroupsSocket = (config: SocketConfig) => {
const sock = makeChatsSocket(config)
const { authState, ev, query, upsertMessage } = sock
const { signalRepository } = sock
const { logger } = config
/** Normalize group metadata participant IDs from LID to PN */
const normalizeGroupMetadata = async (metadata: GroupMetadata): Promise<GroupMetadata> => {
const lidMapping = signalRepository.lidMapping
// Resolve all participant LIDs in parallel for better performance on large groups
await Promise.all(metadata.participants.map(async (p) => {
if (isLidUser(p.id)) {
if (p.phoneNumber) {
p.lid = p.id
p.id = p.phoneNumber
} else {
const resolved = await resolveLidToPn(p.id, lidMapping, logger)
if (resolved && resolved !== p.id) {
p.lid = p.id
p.id = resolved
}
}
}
}))
// Normalize owner/subjectOwner if LID (parallel)
const [resolvedOwner, resolvedSubjectOwner] = await Promise.all([
metadata.owner && isLidUser(metadata.owner)
? (metadata.ownerPn || resolveLidToPn(metadata.owner, lidMapping, logger))
: null,
metadata.subjectOwner && isLidUser(metadata.subjectOwner)
? (metadata.subjectOwnerPn || resolveLidToPn(metadata.subjectOwner, lidMapping, logger))
: null
])
if (resolvedOwner) metadata.owner = resolvedOwner
if (resolvedSubjectOwner) metadata.subjectOwner = resolvedSubjectOwner
return metadata
}
const groupQuery = async (jid: string, type: 'get' | 'set', content: BinaryNode[]) =>
query({
@@ -68,7 +31,7 @@ export const makeGroupsSocket = (config: SocketConfig) => {
const groupMetadata = async (jid: string) => {
const result = await groupQuery(jid, 'get', [{ tag: 'query', attrs: { request: 'interactive' } }])
return normalizeGroupMetadata(extractGroupMetadata(result))
return extractGroupMetadata(result)
}
const groupFetchAllParticipating = async () => {
@@ -95,15 +58,16 @@ export const makeGroupsSocket = (config: SocketConfig) => {
if (groupsChild) {
const groups = getBinaryNodeChildren(groupsChild, 'group')
for (const groupNode of groups) {
const meta = await normalizeGroupMetadata(extractGroupMetadata({
const meta = extractGroupMetadata({
tag: 'result',
attrs: {},
content: [groupNode]
}))
})
data[meta.id] = meta
}
}
// TODO: properly parse LID / PN DATA
sock.ev.emit('groups.update', Object.values(data))
return data
@@ -137,7 +101,7 @@ export const makeGroupsSocket = (config: SocketConfig) => {
}))
}
])
return normalizeGroupMetadata(extractGroupMetadata(result))
return extractGroupMetadata(result)
},
groupLeave: async (id: string) => {
await groupQuery('@g.us', 'set', [
@@ -313,7 +277,7 @@ export const makeGroupsSocket = (config: SocketConfig) => {
),
groupGetInviteInfo: async (code: string) => {
const results = await groupQuery('@g.us', 'get', [{ tag: 'invite', attrs: { code } }])
return normalizeGroupMetadata(extractGroupMetadata(results))
return extractGroupMetadata(results)
},
groupToggleEphemeral: async (jid: string, ephemeralExpiration: number) => {
const content: BinaryNode = ephemeralExpiration
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1667,8 +1667,8 @@ export const makeSocket = (config: SocketConfig) => {
// already-authenticated session (no QR re-scan needed). In this case we skip
// the offline buffer entirely so live messages are not held hostage waiting for the
// server to finish flushing the pending-message backlog (CB:ib,,offline).
// For normal restarts (no stale routingInfo) the standard 2 s safety cap applies.
const OFFLINE_BUFFER_TIMEOUT_MS = 2_000
// For normal restarts (no stale routingInfo) the standard 5 s safety cap applies.
const OFFLINE_BUFFER_TIMEOUT_MS = 5_000
let offlineBufferTimeout: NodeJS.Timeout | undefined
process.nextTick(() => {
+1 -43
View File
@@ -1,30 +1,4 @@
export type WACallUpdateType =
| 'offer'
| 'ringing'
| 'timeout'
| 'reject'
| 'accept'
| 'terminate'
| 'preaccept'
| 'transport'
| 'relaylatency'
| 'group_update'
| 'reminder'
| 'heartbeat'
| 'mute_v2'
| 'enc_rekey'
| 'video'
| 'relay'
export type WACallParticipant = {
jid?: string
/** 'connected' | 'invited' | 'left' etc. */
state?: string
/** Phone number in s.whatsapp.net format */
userPn?: string
/** 'admin' for group call link creator */
type?: string
}
export type WACallUpdateType = 'offer' | 'ringing' | 'timeout' | 'reject' | 'accept' | 'terminate'
export type WACallEvent = {
chatId: string
@@ -39,20 +13,4 @@ export type WACallEvent = {
latencyMs?: number
/** Phone number of the caller (sanitized to fix Brazilian landline bug) */
callerPn?: string
/** Call link token (forms URL: https://call.whatsapp.com/video/<token>) */
linkToken?: string
/** JID of who created the call link */
linkCreator?: string
/** Phone number of link creator */
linkCreatorPn?: string
/** Media type: 'video' or 'audio' */
media?: string
/** Max participants for group/link calls */
connectedLimit?: number
/** Participants in a group/link call */
participants?: WACallParticipant[]
/** Call duration in ms (from terminate/call_summary) */
duration?: number
/** Terminate reason (e.g. 'group_call_ended', 'accepted_elsewhere', 'timeout') */
terminateReason?: string
}
+26 -14
View File
@@ -276,6 +276,7 @@ export const decryptMessageNode = (
meLid: string,
repository: SignalRepositoryWithLIDStore,
logger: ILogger,
autoCleanCorrupted = true
) => {
const { fullMessage, author, sender } = decodeMessageNode(stanza, meId, meLid)
return {
@@ -421,19 +422,34 @@ export const decryptMessageNode = (
if (isRetryExhausted) {
logger.error(
errorContext,
`⚠️ Session corrupted after ${err.attempts} attempts. Retry+pkmsg flow will recover.`
`⚠️ Session corrupted and recovery failed after ${err.attempts} attempts. Auto-cleanup will attempt to recover.`
)
} else {
// First occurrence - log as warning since auto-recovery will attempt
logger.warn(errorContext, '⚠️ Corrupted session detected - attempting auto-recovery')
}
// Session cleanup is deferred to retry exhaustion (safety net).
// The Signal Protocol handles recovery naturally via retry+pkmsg:
// Bad MAC -> retry receipt -> sender re-sends as pkmsg -> new session.
// Deleting sessions here (hot path) causes cascading failures when
// multiple messages from the same contact arrive simultaneously.
// See: messages-recv.ts sendRetryRequest() for deferred cleanup.
// Automatic cleanup of corrupted session (if enabled)
// eslint-disable-next-line max-depth
if (autoCleanCorrupted) {
// eslint-disable-next-line max-depth
try {
const deletedCount = await cleanupCorruptedSession(decryptionJid, repository, logger)
// Mask only user portion of JID for privacy (preserve domain info)
const { user, server } = jidDecode(decryptionJid) || {}
const maskedUser =
user && user.length > 8 ? `${user.substring(0, 4)}****${user.substring(user.length - 4)}` : user
const maskedJid = maskedUser && server ? `${maskedUser}@${server}` : decryptionJid
logger.info(
{ jid: decryptionJid, maskedJid, targetedDevices: deletedCount },
`🔄 Session Reset | JID: ${maskedJid} | Targeted: ${deletedCount} devices | Will recreate on next message`
)
} catch (cleanupErr) {
logger.error({ decryptionJid, err: cleanupErr }, '❌ Failed to cleanup corrupted session')
}
}
} else if (isSessionRecord) {
// Session record errors are transient - retry should handle them
// eslint-disable-next-line max-depth
@@ -488,14 +504,10 @@ export function isCorruptedSessionError(error: any): boolean {
}
/**
* Clean up corrupted session by deleting all device sessions for a JID.
* Signal Protocol will automatically recreate the session on next message.
*
* NOTE: This should NOT be called on every Bad MAC error (hot path).
* Instead, let the retry+pkmsg flow handle recovery naturally (like WhatsApp does).
* Only call this as a safety net when retries are exhausted.
* Clean up corrupted session by deleting all device sessions for a JID
* Signal Protocol will automatically recreate the session on next message
*/
export async function cleanupCorruptedSession(
async function cleanupCorruptedSession(
jid: string,
repository: SignalRepositoryWithLIDStore,
logger: ILogger
+4 -4
View File
@@ -54,15 +54,15 @@ export interface BufferConfig {
*/
export function loadBufferConfig(): BufferConfig {
return {
// perf(inbound-latency): reduced from 15s → 3s so the safety auto-flush fires sooner.
// perf(inbound-latency): reduced from 15s → 5s so the safety auto-flush fires sooner.
// processNodeWithBuffer always calls ev.flush() explicitly (no-op for this timer),
// but socket.ts's offline-phase buffer and any stalled buffer benefit from the lower cap.
bufferTimeoutMs: parseInt(process.env.BAILEYS_BUFFER_TIMEOUT_MS || '3000', 10),
bufferTimeoutMs: parseInt(process.env.BAILEYS_BUFFER_TIMEOUT_MS || '5000', 10),
minBufferTimeoutMs: parseInt(process.env.BAILEYS_BUFFER_MIN_TIMEOUT_MS || '1000', 10),
maxBufferTimeoutMs: parseInt(process.env.BAILEYS_BUFFER_MAX_TIMEOUT_MS || '3000', 10),
maxBufferTimeoutMs: parseInt(process.env.BAILEYS_BUFFER_MAX_TIMEOUT_MS || '8000', 10),
maxHistoryCacheSize: parseInt(process.env.BAILEYS_BUFFER_MAX_HISTORY_CACHE || '10000', 10),
maxBufferSize: parseInt(process.env.BAILEYS_BUFFER_MAX_SIZE || '5000', 10),
flushDebounceMs: parseInt(process.env.BAILEYS_BUFFER_FLUSH_DEBOUNCE_MS || '10', 10),
flushDebounceMs: parseInt(process.env.BAILEYS_BUFFER_FLUSH_DEBOUNCE_MS || '100', 10),
enableAdaptiveTimeout: process.env.BAILEYS_BUFFER_ADAPTIVE_TIMEOUT !== 'false',
enableMetrics: process.env.BAILEYS_BUFFER_METRICS === 'true' || process.env.BAILEYS_PROMETHEUS_ENABLED === 'true',
lruCleanupRatio: parseFloat(process.env.BAILEYS_BUFFER_LRU_CLEANUP_RATIO || '0.2'),
-30
View File
@@ -429,36 +429,6 @@ export const getCallStatusFromNode = ({ tag, attrs }: BinaryNode) => {
case 'accept':
status = 'accept'
break
case 'preaccept':
status = 'preaccept'
break
case 'transport':
status = 'transport'
break
case 'relaylatency':
status = 'relaylatency'
break
case 'group_update':
status = 'group_update'
break
case 'reminder':
status = 'reminder'
break
case 'heartbeat':
status = 'heartbeat'
break
case 'mute_v2':
status = 'mute_v2'
break
case 'enc_rekey':
status = 'enc_rekey'
break
case 'video':
status = 'video'
break
case 'relay':
status = 'relay'
break
default:
status = 'ringing'
break
+4 -22
View File
@@ -211,33 +211,15 @@ export class MessageRetryManager {
}
}
// MAC errors require session recreation, but rate-limited to prevent loops.
// When multiple messages fail with Bad MAC simultaneously, only the first
// should trigger recreation; subsequent ones within the cooldown window
// piggyback on the same new session established by the retry+pkmsg flow.
// MAC errors require IMMEDIATE session recreation regardless of history
// This handles the case where contact reinstalled WhatsApp (identity key changed)
if (errorCode !== undefined && MAC_ERROR_CODES.has(errorCode)) {
const now = Date.now()
const prevTime = this.sessionRecreateHistory.get(jid)
const MAC_ERROR_COOLDOWN_MS = 10_000 // 10 seconds
if (prevTime && now - prevTime < MAC_ERROR_COOLDOWN_MS) {
const reasonName = RetryReason[errorCode] || `code_${errorCode}`
this.logger.debug(
{ jid, errorCode: reasonName, msSinceLast: now - prevTime },
'MAC error session recreation skipped — cooldown active'
)
return {
reason: '',
recreate: false
}
}
this.sessionRecreateHistory.set(jid, now)
this.sessionRecreateHistory.set(jid, Date.now())
this.statistics.sessionRecreations++
const reasonName = RetryReason[errorCode] || `code_${errorCode}`
metrics.signalMacErrors?.inc({ action: 'session_recreation' })
metrics.signalSessionRecreations?.inc({ reason: 'mac_error' })
this.logger.warn({ jid, errorCode: reasonName }, 'MAC error detected, recreating session')
this.logger.warn({ jid, errorCode: reasonName }, 'MAC error detected, forcing immediate session recreation')
return {
reason: `MAC error (${reasonName}) - contact may have reinstalled WhatsApp`,
recreate: true
+25 -98
View File
@@ -19,13 +19,10 @@ import type {
WAMessage,
WAMessageKey
} from '../Types'
import type { LIDMappingStore } from '../Signal/lid-mapping'
import { WAMessageStubType } from '../Types'
import { getContentType, normalizeMessageContent } from '../Utils/messages'
import {
areJidsSameUser,
isAnyLidUser,
isAnyPnUser,
isHostedLidUser,
isHostedPnUser,
isJidBroadcast,
@@ -116,63 +113,8 @@ export const cleanMessage = (message: WAMessage, meId: string, meLid: string) =>
msgKey.remoteJid = message.key.remoteJid
// set participant of the message
msgKey.participant = msgKey.participant || message.key.participant
} else {
// fromMe reactions/polls: normalise remoteJid to match the chat JID
// ensures DM reaction keys are consistent with group behavior
msgKey.remoteJid = message.key.remoteJid
// in groups, normalise participant for own messages too
if (message.key.participant) {
msgKey.participant = msgKey.participant || message.key.participant
}
}
}
}
/**
* Resolves a LID JID to its PN equivalent using the LID mapping store.
* Returns the original JID if it's not a LID or if no mapping is found.
* Safe to call with any JID type (group, newsletter, PN, etc.).
*/
export const resolveLidToPn = async (
jid: string | undefined | null,
lidMapping: LIDMappingStore,
logger?: ILogger
): Promise<string | undefined> => {
if (!jid) {
return undefined
}
if (isAnyLidUser(jid)) {
const pn = await lidMapping.getPNForLID(jid)
if (pn) {
logger?.debug({ lid: jid, pn }, 'Resolved LID to PN')
}
return pn || jid
}
return jid
}
/**
* Normalizes a WAMessageKey by resolving LIDPN for remoteJid and participant.
*/
export const normalizeKeyLidToPn = async (
key: WAMessageKey,
lidMapping: LIDMappingStore,
logger?: ILogger
): Promise<void> => {
const [resolvedRemoteJid, resolvedParticipant] = await Promise.all([
resolveLidToPn(key.remoteJid, lidMapping, logger),
resolveLidToPn(key.participant, lidMapping, logger)
])
if (resolvedRemoteJid) {
key.remoteJid = resolvedRemoteJid
}
if (resolvedParticipant) {
key.participant = resolvedParticipant
}
}
export const normalizeMessageJids = async (
@@ -180,42 +122,37 @@ export const normalizeMessageJids = async (
signalRepository: SignalRepositoryWithLIDStore,
logger?: ILogger
): Promise<void> => {
const lidMapping = signalRepository.lidMapping
const key = message.key
// FAST PATH: Use alt JIDs directly when available (avoids LIDMappingStore race condition).
// The stanza always carries both formats (LID + PN) in the attributes.
// When addressing_mode=lid, remoteJid is LID and remoteJidAlt is PN.
// When addressing_mode=pn, remoteJid is already PN (no conversion needed).
if (key.remoteJid && isAnyLidUser(key.remoteJid) && key.remoteJidAlt && isAnyPnUser(key.remoteJidAlt)) {
logger?.debug({ lid: key.remoteJid, pn: key.remoteJidAlt }, 'Resolved remoteJid LID→PN via alt (fast path)')
key.remoteJid = key.remoteJidAlt
const resolveLidToPn = async (jid: string | undefined | null): Promise<string | undefined> => {
if (!jid) {
return undefined
}
if (key.participant && isAnyLidUser(key.participant) && key.participantAlt && isAnyPnUser(key.participantAlt)) {
logger?.debug({ lid: key.participant, pn: key.participantAlt }, 'Resolved participant LID→PN via alt (fast path)')
key.participant = key.participantAlt
if (isLidUser(jid) || isHostedLidUser(jid)) {
const pn = await signalRepository.lidMapping.getPNForLID(jid)
if (pn) {
logger?.debug({ lid: jid, pn }, 'Resolved LID to PN for inbound message')
} else {
logger?.debug({ lid: jid }, 'PN not found for inbound LID, keeping LID')
}
// SLOW PATH: Resolve any remaining LIDs via LIDMappingStore lookup
await normalizeKeyLidToPn(key, lidMapping, logger)
// Also normalize participantAlt (the alternative JID format — can be LID when addressing_mode=pn)
if (key.participantAlt && isAnyLidUser(key.participantAlt)) {
const resolved = await resolveLidToPn(key.participantAlt, lidMapping, logger)
if (resolved) {
key.participantAlt = resolved
}
return pn || jid
}
// Normalize nested message keys (reaction, poll) that may contain LID JIDs
const content = normalizeMessageContent(message.message)
if (content?.reactionMessage?.key) {
await normalizeKeyLidToPn(content.reactionMessage.key, lidMapping, logger)
return jid
}
if (content?.pollUpdateMessage?.pollCreationMessageKey) {
await normalizeKeyLidToPn(content.pollUpdateMessage.pollCreationMessageKey, lidMapping, logger)
// Execute both lookups in parallel instead of sequentially to reduce latency
const [resolvedRemoteJid, resolvedParticipant] = await Promise.all([
resolveLidToPn(message.key.remoteJid),
resolveLidToPn(message.key.participant)
])
if (resolvedRemoteJid) {
message.key.remoteJid = resolvedRemoteJid
}
if (resolvedParticipant) {
message.key.participant = resolvedParticipant
}
}
@@ -589,12 +526,6 @@ const processMessage = async (
'CTWA: Successfully recovered message via placeholder resend'
)
// Normalize LID→PN in PDO-recovered message key before emitting
// eslint-disable-next-line max-depth
if (webMessageInfo.key && signalRepository) {
await normalizeKeyLidToPn(webMessageInfo.key as WAMessageKey, signalRepository.lidMapping, logger)
}
// wait till another upsert event is available, don't want it to be part of the PDO response message
// TODO: parse through proper message handling utilities (to add relevant key fields)
ev.emit('messages.upsert', {
@@ -735,13 +666,9 @@ const processMessage = async (
response: responseMsg
}
// Normalize creationMsgKey JIDs for the emitted event
const normalizedCreationKey = { ...creationMsgKey }
await normalizeKeyLidToPn(normalizedCreationKey, signalRepository.lidMapping, logger)
ev.emit('messages.update', [
{
key: normalizedCreationKey,
key: creationMsgKey,
update: {
eventResponses: [eventResponse]
}
@@ -784,7 +711,7 @@ const processMessage = async (
})
}
const participantsIncludesMe = () => participants.find(p => areJidsSameUser(meId, p.id) || areJidsSameUser(meId, p.phoneNumber))
const participantsIncludesMe = () => participants.find(jid => areJidsSameUser(meId, jid.phoneNumber)) // ADD SUPPORT FOR LID
switch (message.messageStubType) {
case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER:
+1 -1
View File
@@ -20,7 +20,7 @@ const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
secondary: config.version[1],
tertiary: config.version[2]
},
platform: proto.ClientPayload.UserAgent.Platform.MACOS,
platform: proto.ClientPayload.UserAgent.Platform.WEB,
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
osVersion: '0.1',
device: 'Desktop',
@@ -20,7 +20,7 @@ import { jest } from '@jest/globals'
* bad-ack-handling.test.ts.
*/
const OFFLINE_BUFFER_TIMEOUT_MS = 2_000
const OFFLINE_BUFFER_TIMEOUT_MS = 5_000
/** Mirrors the state variables declared at the top of makeSocket */
interface OfflineBufferState {
@@ -101,10 +101,10 @@ describe('offline-buffer safety timer (socket.ts)', () => {
})
// -------------------------------------------------------------------------
// 1. Timeout path — CB:ib,,offline never arrives within 2 s
// 1. Timeout path — CB:ib,,offline never arrives within 5 s
// -------------------------------------------------------------------------
it('fires after 2 s and flushes when CB:ib,,offline is delayed', () => {
it('fires after 5 s and flushes when CB:ib,,offline is delayed', () => {
startBuffer(state, mockFlush, mockWarn)
expect(mockFlush).not.toHaveBeenCalled()
@@ -144,17 +144,17 @@ describe('offline-buffer safety timer (socket.ts)', () => {
})
// -------------------------------------------------------------------------
// 2. Happy path — CB:ib,,offline arrives before the 2 s timer fires
// 2. Happy path — CB:ib,,offline arrives before the 5 s timer fires
// -------------------------------------------------------------------------
it('CB:ib,,offline cancels the timer and flushes exactly once', () => {
startBuffer(state, mockFlush, mockWarn)
// Server responds before the 2 s timeout
// Server responds before the 5 s timeout
jest.advanceTimersByTime(1_000)
onOffline(state, mockFlush)
// Timer should be cancelled — advancing past 2 s must not cause a second flush
// Timer should be cancelled — advancing past 5 s must not cause a second flush
jest.advanceTimersByTime(OFFLINE_BUFFER_TIMEOUT_MS)
expect(mockFlush).toHaveBeenCalledTimes(1)
@@ -192,7 +192,7 @@ describe('offline-buffer safety timer (socket.ts)', () => {
onClose(state)
// Timer must be gone — advancing past 2 s must not trigger any flush
// Timer must be gone — advancing past 5 s must not trigger any flush
jest.advanceTimersByTime(OFFLINE_BUFFER_TIMEOUT_MS)
expect(mockFlush).not.toHaveBeenCalled()
@@ -236,7 +236,7 @@ describe('offline-buffer safety timer (socket.ts)', () => {
// 4. Boundary / timing edge cases
// -------------------------------------------------------------------------
it('does not flush before exactly 2 s have elapsed', () => {
it('does not flush before exactly 5 s have elapsed', () => {
startBuffer(state, mockFlush, mockWarn)
jest.advanceTimersByTime(OFFLINE_BUFFER_TIMEOUT_MS - 1)
@@ -244,7 +244,7 @@ describe('offline-buffer safety timer (socket.ts)', () => {
expect(mockFlush).not.toHaveBeenCalled()
})
it('flushes at exactly the 2 s boundary', () => {
it('flushes at exactly the 5 s boundary', () => {
startBuffer(state, mockFlush, mockWarn)
jest.advanceTimersByTime(OFFLINE_BUFFER_TIMEOUT_MS)