Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a745f2167 | |||
| a1def8c2c0 |
@@ -1,7 +1,7 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
package proto;
|
package proto;
|
||||||
|
|
||||||
/// WhatsApp Version: 2.3000.1034274421
|
/// WhatsApp Version: 2.3000.1034238531
|
||||||
|
|
||||||
message ADVDeviceIdentity {
|
message ADVDeviceIdentity {
|
||||||
optional uint32 rawId = 1;
|
optional uint32 rawId = 1;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":[2,3000,1034279434]}
|
{"version":[2,3000,1034258243]}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
|||||||
emitOwnEvents: true,
|
emitOwnEvents: true,
|
||||||
defaultQueryTimeoutMs: 30_000,
|
defaultQueryTimeoutMs: 30_000,
|
||||||
customUploadHosts: [],
|
customUploadHosts: [],
|
||||||
retryRequestDelayMs: 150,
|
retryRequestDelayMs: 250,
|
||||||
maxMsgRetryCount: 5,
|
maxMsgRetryCount: 5,
|
||||||
fireInitQueries: true,
|
fireInitQueries: true,
|
||||||
auth: undefined as unknown as AuthenticationState,
|
auth: undefined as unknown as AuthenticationState,
|
||||||
@@ -78,7 +78,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
|||||||
shouldSyncHistoryMessage: () => true,
|
shouldSyncHistoryMessage: () => true,
|
||||||
shouldIgnoreJid: () => false,
|
shouldIgnoreJid: () => false,
|
||||||
linkPreviewImageThumbnailWidth: 192,
|
linkPreviewImageThumbnailWidth: 192,
|
||||||
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 1000 },
|
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
|
||||||
generateHighQualityLinkPreview: false,
|
generateHighQualityLinkPreview: false,
|
||||||
enableAutoSessionRecreation: true,
|
enableAutoSessionRecreation: true,
|
||||||
enableRecentMessageCache: true,
|
enableRecentMessageCache: true,
|
||||||
|
|||||||
+19
-46
@@ -50,8 +50,7 @@ import {
|
|||||||
isMissingKeyError,
|
isMissingKeyError,
|
||||||
MAX_SYNC_ATTEMPTS,
|
MAX_SYNC_ATTEMPTS,
|
||||||
newLTHashState,
|
newLTHashState,
|
||||||
processSyncAction,
|
processSyncAction
|
||||||
resolveLidToPn
|
|
||||||
} from '../Utils'
|
} from '../Utils'
|
||||||
import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex'
|
import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex'
|
||||||
import processMessage from '../Utils/process-message'
|
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 */
|
/** 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()
|
const messageMutex = makeKeyedMutex()
|
||||||
|
|
||||||
/** this mutex ensures that receipts from the same chat are processed in order, while allowing parallel processing across chats */
|
/** this mutex ensures that receipts are processed in order */
|
||||||
const receiptMutex = makeKeyedMutex()
|
const receiptMutex = makeMutex()
|
||||||
|
|
||||||
/** this mutex ensures that app state patches are processed in order */
|
/** this mutex ensures that app state patches are processed in order */
|
||||||
const appStatePatchMutex = makeMutex()
|
const appStatePatchMutex = makeMutex()
|
||||||
|
|
||||||
/** this mutex ensures that notifications from the same chat are processed in order, while allowing parallel processing across chats */
|
/** this mutex ensures that notifications are processed in order */
|
||||||
const notificationMutex = makeKeyedMutex()
|
const notificationMutex = makeMutex()
|
||||||
|
|
||||||
// Timeout for AwaitingInitialSync state
|
// Timeout for AwaitingInitialSync state
|
||||||
let awaitingSyncTimeout: NodeJS.Timeout | undefined
|
let awaitingSyncTimeout: NodeJS.Timeout | undefined
|
||||||
@@ -758,16 +757,9 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
}, authState?.creds?.me?.id || 'resync-app-state')
|
}, authState?.creds?.me?.id || 'resync-app-state')
|
||||||
|
|
||||||
const { onMutation } = newAppStateChunkHandler(isInitialSync)
|
const { onMutation } = newAppStateChunkHandler(isInitialSync)
|
||||||
const lidMapping = signalRepository.lidMapping
|
|
||||||
for (const key in globalMutationMap) {
|
for (const key in globalMutationMap) {
|
||||||
const mutation = globalMutationMap[key]
|
const mutation = globalMutationMap[key]
|
||||||
if (!mutation) continue
|
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)
|
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
|
let presence: PresenceData | undefined
|
||||||
const rawJid = attrs.from
|
const jid = attrs.from
|
||||||
const rawParticipant = attrs.participant || attrs.from
|
const participant = attrs.participant || attrs.from
|
||||||
if (!rawJid) {
|
if (!jid) {
|
||||||
logger.warn({ attrs }, 'handlePresenceUpdate: jid (attrs.from) is missing, skipping')
|
logger.warn({ attrs }, 'handlePresenceUpdate: jid (attrs.from) is missing, skipping')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldIgnoreJid(rawJid) && rawJid !== S_WHATSAPP_NET) {
|
if (shouldIgnoreJid(jid) && jid !== S_WHATSAPP_NET) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -941,7 +933,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
} else if (Array.isArray(content)) {
|
} else if (Array.isArray(content)) {
|
||||||
const [firstChild] = content
|
const [firstChild] = content
|
||||||
if (!firstChild) {
|
if (!firstChild) {
|
||||||
logger.warn({ jid: rawJid }, 'handlePresenceUpdate: firstChild content is empty, skipping')
|
logger.warn({ jid }, 'handlePresenceUpdate: firstChild content is empty, skipping')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -960,19 +952,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (presence) {
|
if (presence) {
|
||||||
if (!rawParticipant) {
|
if (!participant) {
|
||||||
logger.warn({ jid: rawJid }, 'handlePresenceUpdate: participant is missing, skipping')
|
logger.warn({ jid }, 'handlePresenceUpdate: participant is missing, skipping')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve LID→PN so consumers always see phone-number JIDs
|
ev.emit('presence.update', { id: jid, presences: { [participant]: presence } })
|
||||||
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 } })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1046,16 +1031,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
undefined,
|
undefined,
|
||||||
logger
|
logger
|
||||||
)
|
)
|
||||||
const lidMapping = signalRepository.lidMapping
|
|
||||||
for (const key in mutationMap) {
|
for (const key in mutationMap) {
|
||||||
const mutation = mutationMap[key]!
|
onMutation(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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1399,12 +1376,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ws.on('CB:presence', (node: BinaryNode) => {
|
ws.on('CB:presence', handlePresenceUpdate)
|
||||||
handlePresenceUpdate(node).catch(err => onUnexpectedError(err, 'handling presence update'))
|
ws.on('CB:chatstate', handlePresenceUpdate)
|
||||||
})
|
|
||||||
ws.on('CB:chatstate', (node: BinaryNode) => {
|
|
||||||
handlePresenceUpdate(node).catch(err => onUnexpectedError(err, 'handling chatstate update'))
|
|
||||||
})
|
|
||||||
|
|
||||||
ws.on('CB:ib,,dirty', async (node: BinaryNode) => {
|
ws.on('CB:ib,,dirty', async (node: BinaryNode) => {
|
||||||
const { attrs } = getBinaryNodeChild(node, 'dirty')!
|
const { attrs } = getBinaryNodeChild(node, 'dirty')!
|
||||||
@@ -1522,7 +1495,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
awaitingSyncTimeout = setTimeout(() => {
|
awaitingSyncTimeout = setTimeout(() => {
|
||||||
if (syncState === SyncState.AwaitingInitialSync) {
|
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
|
syncState = SyncState.Online
|
||||||
ev.flush()
|
ev.flush()
|
||||||
|
|
||||||
@@ -1532,7 +1505,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1
|
const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1
|
||||||
ev.emit('creds.update', { accountSyncCounter })
|
ev.emit('creds.update', { accountSyncCounter })
|
||||||
}
|
}
|
||||||
}, 2_000)
|
}, 4_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
// When an app state sync key arrives (myAppStateKeyId is set) and there are
|
// When an app state sync key arrives (myAppStateKeyId is set) and there are
|
||||||
|
|||||||
+7
-43
@@ -1,7 +1,7 @@
|
|||||||
import { proto } from '../../WAProto/index.js'
|
import { proto } from '../../WAProto/index.js'
|
||||||
import type { GroupMetadata, GroupParticipant, ParticipantAction, SocketConfig, WAMessageKey } from '../Types'
|
import type { GroupMetadata, GroupParticipant, ParticipantAction, SocketConfig, WAMessageKey } from '../Types'
|
||||||
import { WAMessageAddressingMode, WAMessageStubType } from '../Types'
|
import { WAMessageAddressingMode, WAMessageStubType } from '../Types'
|
||||||
import { generateMessageIDV2, resolveLidToPn, unixTimestampSeconds } from '../Utils'
|
import { generateMessageIDV2, unixTimestampSeconds } from '../Utils'
|
||||||
import {
|
import {
|
||||||
type BinaryNode,
|
type BinaryNode,
|
||||||
getBinaryNodeChild,
|
getBinaryNodeChild,
|
||||||
@@ -17,43 +17,6 @@ import { makeChatsSocket } from './chats'
|
|||||||
export const makeGroupsSocket = (config: SocketConfig) => {
|
export const makeGroupsSocket = (config: SocketConfig) => {
|
||||||
const sock = makeChatsSocket(config)
|
const sock = makeChatsSocket(config)
|
||||||
const { authState, ev, query, upsertMessage } = sock
|
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[]) =>
|
const groupQuery = async (jid: string, type: 'get' | 'set', content: BinaryNode[]) =>
|
||||||
query({
|
query({
|
||||||
@@ -68,7 +31,7 @@ export const makeGroupsSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
const groupMetadata = async (jid: string) => {
|
const groupMetadata = async (jid: string) => {
|
||||||
const result = await groupQuery(jid, 'get', [{ tag: 'query', attrs: { request: 'interactive' } }])
|
const result = await groupQuery(jid, 'get', [{ tag: 'query', attrs: { request: 'interactive' } }])
|
||||||
return normalizeGroupMetadata(extractGroupMetadata(result))
|
return extractGroupMetadata(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupFetchAllParticipating = async () => {
|
const groupFetchAllParticipating = async () => {
|
||||||
@@ -95,15 +58,16 @@ export const makeGroupsSocket = (config: SocketConfig) => {
|
|||||||
if (groupsChild) {
|
if (groupsChild) {
|
||||||
const groups = getBinaryNodeChildren(groupsChild, 'group')
|
const groups = getBinaryNodeChildren(groupsChild, 'group')
|
||||||
for (const groupNode of groups) {
|
for (const groupNode of groups) {
|
||||||
const meta = await normalizeGroupMetadata(extractGroupMetadata({
|
const meta = extractGroupMetadata({
|
||||||
tag: 'result',
|
tag: 'result',
|
||||||
attrs: {},
|
attrs: {},
|
||||||
content: [groupNode]
|
content: [groupNode]
|
||||||
}))
|
})
|
||||||
data[meta.id] = meta
|
data[meta.id] = meta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: properly parse LID / PN DATA
|
||||||
sock.ev.emit('groups.update', Object.values(data))
|
sock.ev.emit('groups.update', Object.values(data))
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@@ -137,7 +101,7 @@ export const makeGroupsSocket = (config: SocketConfig) => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
return normalizeGroupMetadata(extractGroupMetadata(result))
|
return extractGroupMetadata(result)
|
||||||
},
|
},
|
||||||
groupLeave: async (id: string) => {
|
groupLeave: async (id: string) => {
|
||||||
await groupQuery('@g.us', 'set', [
|
await groupQuery('@g.us', 'set', [
|
||||||
@@ -313,7 +277,7 @@ export const makeGroupsSocket = (config: SocketConfig) => {
|
|||||||
),
|
),
|
||||||
groupGetInviteInfo: async (code: string) => {
|
groupGetInviteInfo: async (code: string) => {
|
||||||
const results = await groupQuery('@g.us', 'get', [{ tag: 'invite', attrs: { code } }])
|
const results = await groupQuery('@g.us', 'get', [{ tag: 'invite', attrs: { code } }])
|
||||||
return normalizeGroupMetadata(extractGroupMetadata(results))
|
return extractGroupMetadata(results)
|
||||||
},
|
},
|
||||||
groupToggleEphemeral: async (jid: string, ephemeralExpiration: number) => {
|
groupToggleEphemeral: async (jid: string, ephemeralExpiration: number) => {
|
||||||
const content: BinaryNode = ephemeralExpiration
|
const content: BinaryNode = ephemeralExpiration
|
||||||
|
|||||||
+83
-993
File diff suppressed because it is too large
Load Diff
@@ -1667,8 +1667,8 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
// already-authenticated session (no QR re-scan needed). In this case we skip
|
// 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
|
// 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).
|
// server to finish flushing the pending-message backlog (CB:ib,,offline).
|
||||||
// For normal restarts (no stale routingInfo) the standard 2 s safety cap applies.
|
// For normal restarts (no stale routingInfo) the standard 5 s safety cap applies.
|
||||||
const OFFLINE_BUFFER_TIMEOUT_MS = 2_000
|
const OFFLINE_BUFFER_TIMEOUT_MS = 5_000
|
||||||
let offlineBufferTimeout: NodeJS.Timeout | undefined
|
let offlineBufferTimeout: NodeJS.Timeout | undefined
|
||||||
|
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
|
|||||||
+1
-43
@@ -1,30 +1,4 @@
|
|||||||
export type WACallUpdateType =
|
export type WACallUpdateType = 'offer' | 'ringing' | 'timeout' | 'reject' | 'accept' | 'terminate'
|
||||||
| '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 WACallEvent = {
|
export type WACallEvent = {
|
||||||
chatId: string
|
chatId: string
|
||||||
@@ -39,20 +13,4 @@ export type WACallEvent = {
|
|||||||
latencyMs?: number
|
latencyMs?: number
|
||||||
/** Phone number of the caller (sanitized to fix Brazilian landline bug) */
|
/** Phone number of the caller (sanitized to fix Brazilian landline bug) */
|
||||||
callerPn?: string
|
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,15 +54,15 @@ export interface BufferConfig {
|
|||||||
*/
|
*/
|
||||||
export function loadBufferConfig(): BufferConfig {
|
export function loadBufferConfig(): BufferConfig {
|
||||||
return {
|
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),
|
// 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.
|
// 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),
|
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),
|
maxHistoryCacheSize: parseInt(process.env.BAILEYS_BUFFER_MAX_HISTORY_CACHE || '10000', 10),
|
||||||
maxBufferSize: parseInt(process.env.BAILEYS_BUFFER_MAX_SIZE || '5000', 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',
|
enableAdaptiveTimeout: process.env.BAILEYS_BUFFER_ADAPTIVE_TIMEOUT !== 'false',
|
||||||
enableMetrics: process.env.BAILEYS_BUFFER_METRICS === 'true' || process.env.BAILEYS_PROMETHEUS_ENABLED === 'true',
|
enableMetrics: process.env.BAILEYS_BUFFER_METRICS === 'true' || process.env.BAILEYS_PROMETHEUS_ENABLED === 'true',
|
||||||
lruCleanupRatio: parseFloat(process.env.BAILEYS_BUFFER_LRU_CLEANUP_RATIO || '0.2'),
|
lruCleanupRatio: parseFloat(process.env.BAILEYS_BUFFER_LRU_CLEANUP_RATIO || '0.2'),
|
||||||
|
|||||||
@@ -429,36 +429,6 @@ export const getCallStatusFromNode = ({ tag, attrs }: BinaryNode) => {
|
|||||||
case 'accept':
|
case 'accept':
|
||||||
status = 'accept'
|
status = 'accept'
|
||||||
break
|
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:
|
default:
|
||||||
status = 'ringing'
|
status = 'ringing'
|
||||||
break
|
break
|
||||||
|
|||||||
+28
-101
@@ -19,13 +19,10 @@ import type {
|
|||||||
WAMessage,
|
WAMessage,
|
||||||
WAMessageKey
|
WAMessageKey
|
||||||
} from '../Types'
|
} from '../Types'
|
||||||
import type { LIDMappingStore } from '../Signal/lid-mapping'
|
|
||||||
import { WAMessageStubType } from '../Types'
|
import { WAMessageStubType } from '../Types'
|
||||||
import { getContentType, normalizeMessageContent } from '../Utils/messages'
|
import { getContentType, normalizeMessageContent } from '../Utils/messages'
|
||||||
import {
|
import {
|
||||||
areJidsSameUser,
|
areJidsSameUser,
|
||||||
isAnyLidUser,
|
|
||||||
isAnyPnUser,
|
|
||||||
isHostedLidUser,
|
isHostedLidUser,
|
||||||
isHostedPnUser,
|
isHostedPnUser,
|
||||||
isJidBroadcast,
|
isJidBroadcast,
|
||||||
@@ -116,106 +113,46 @@ export const cleanMessage = (message: WAMessage, meId: string, meLid: string) =>
|
|||||||
msgKey.remoteJid = message.key.remoteJid
|
msgKey.remoteJid = message.key.remoteJid
|
||||||
// set participant of the message
|
// set participant of the message
|
||||||
msgKey.participant = msgKey.participant || message.key.participant
|
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 LID→PN 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 (
|
export const normalizeMessageJids = async (
|
||||||
message: WAMessage,
|
message: WAMessage,
|
||||||
signalRepository: SignalRepositoryWithLIDStore,
|
signalRepository: SignalRepositoryWithLIDStore,
|
||||||
logger?: ILogger
|
logger?: ILogger
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const lidMapping = signalRepository.lidMapping
|
const resolveLidToPn = async (jid: string | undefined | null): Promise<string | undefined> => {
|
||||||
const key = message.key
|
if (!jid) {
|
||||||
|
return undefined
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
|
||||||
|
return pn || jid
|
||||||
|
}
|
||||||
|
|
||||||
|
return jid
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize nested message keys (reaction, poll) that may contain LID JIDs
|
// Execute both lookups in parallel instead of sequentially to reduce latency
|
||||||
const content = normalizeMessageContent(message.message)
|
const [resolvedRemoteJid, resolvedParticipant] = await Promise.all([
|
||||||
if (content?.reactionMessage?.key) {
|
resolveLidToPn(message.key.remoteJid),
|
||||||
await normalizeKeyLidToPn(content.reactionMessage.key, lidMapping, logger)
|
resolveLidToPn(message.key.participant)
|
||||||
|
])
|
||||||
|
|
||||||
|
if (resolvedRemoteJid) {
|
||||||
|
message.key.remoteJid = resolvedRemoteJid
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content?.pollUpdateMessage?.pollCreationMessageKey) {
|
if (resolvedParticipant) {
|
||||||
await normalizeKeyLidToPn(content.pollUpdateMessage.pollCreationMessageKey, lidMapping, logger)
|
message.key.participant = resolvedParticipant
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,12 +526,6 @@ const processMessage = async (
|
|||||||
'CTWA: Successfully recovered message via placeholder resend'
|
'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
|
// 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)
|
// TODO: parse through proper message handling utilities (to add relevant key fields)
|
||||||
ev.emit('messages.upsert', {
|
ev.emit('messages.upsert', {
|
||||||
@@ -735,13 +666,9 @@ const processMessage = async (
|
|||||||
response: responseMsg
|
response: responseMsg
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize creationMsgKey JIDs for the emitted event
|
|
||||||
const normalizedCreationKey = { ...creationMsgKey }
|
|
||||||
await normalizeKeyLidToPn(normalizedCreationKey, signalRepository.lidMapping, logger)
|
|
||||||
|
|
||||||
ev.emit('messages.update', [
|
ev.emit('messages.update', [
|
||||||
{
|
{
|
||||||
key: normalizedCreationKey,
|
key: creationMsgKey,
|
||||||
update: {
|
update: {
|
||||||
eventResponses: [eventResponse]
|
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) {
|
switch (message.messageStubType) {
|
||||||
case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER:
|
case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
|
|||||||
secondary: config.version[1],
|
secondary: config.version[1],
|
||||||
tertiary: config.version[2]
|
tertiary: config.version[2]
|
||||||
},
|
},
|
||||||
platform: proto.ClientPayload.UserAgent.Platform.MACOS,
|
platform: proto.ClientPayload.UserAgent.Platform.WEB,
|
||||||
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
|
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
|
||||||
osVersion: '0.1',
|
osVersion: '0.1',
|
||||||
device: 'Desktop',
|
device: 'Desktop',
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { jest } from '@jest/globals'
|
|||||||
* bad-ack-handling.test.ts.
|
* 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 */
|
/** Mirrors the state variables declared at the top of makeSocket */
|
||||||
interface OfflineBufferState {
|
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)
|
startBuffer(state, mockFlush, mockWarn)
|
||||||
|
|
||||||
expect(mockFlush).not.toHaveBeenCalled()
|
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', () => {
|
it('CB:ib,,offline cancels the timer and flushes exactly once', () => {
|
||||||
startBuffer(state, mockFlush, mockWarn)
|
startBuffer(state, mockFlush, mockWarn)
|
||||||
|
|
||||||
// Server responds before the 2 s timeout
|
// Server responds before the 5 s timeout
|
||||||
jest.advanceTimersByTime(1_000)
|
jest.advanceTimersByTime(1_000)
|
||||||
onOffline(state, mockFlush)
|
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)
|
jest.advanceTimersByTime(OFFLINE_BUFFER_TIMEOUT_MS)
|
||||||
|
|
||||||
expect(mockFlush).toHaveBeenCalledTimes(1)
|
expect(mockFlush).toHaveBeenCalledTimes(1)
|
||||||
@@ -192,7 +192,7 @@ describe('offline-buffer safety timer (socket.ts)', () => {
|
|||||||
|
|
||||||
onClose(state)
|
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)
|
jest.advanceTimersByTime(OFFLINE_BUFFER_TIMEOUT_MS)
|
||||||
|
|
||||||
expect(mockFlush).not.toHaveBeenCalled()
|
expect(mockFlush).not.toHaveBeenCalled()
|
||||||
@@ -236,7 +236,7 @@ describe('offline-buffer safety timer (socket.ts)', () => {
|
|||||||
// 4. Boundary / timing edge cases
|
// 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)
|
startBuffer(state, mockFlush, mockWarn)
|
||||||
|
|
||||||
jest.advanceTimersByTime(OFFLINE_BUFFER_TIMEOUT_MS - 1)
|
jest.advanceTimersByTime(OFFLINE_BUFFER_TIMEOUT_MS - 1)
|
||||||
@@ -244,7 +244,7 @@ describe('offline-buffer safety timer (socket.ts)', () => {
|
|||||||
expect(mockFlush).not.toHaveBeenCalled()
|
expect(mockFlush).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('flushes at exactly the 2 s boundary', () => {
|
it('flushes at exactly the 5 s boundary', () => {
|
||||||
startBuffer(state, mockFlush, mockWarn)
|
startBuffer(state, mockFlush, mockWarn)
|
||||||
|
|
||||||
jest.advanceTimersByTime(OFFLINE_BUFFER_TIMEOUT_MS)
|
jest.advanceTimersByTime(OFFLINE_BUFFER_TIMEOUT_MS)
|
||||||
|
|||||||
Reference in New Issue
Block a user