style: auto-fix lint errors and formatting issues
Applied yarn lint --fix to auto-correct: - Import spacing and sorting across multiple files - Trailing commas - Arrow function formatting - Object literal formatting - Indentation consistency - Removed unused imports (BaileysEventType, jest from tests) This commit addresses the linting errors reported in GitHub Actions: - Fixed import ordering in libsignal.ts - Fixed trailing commas in Defaults/index.ts - Cleaned up formatting across 63 files - Reduced line count by ~220 lines through formatting optimization Note: Some lint errors remain (75 errors, 239 warnings) mainly: - @typescript-eslint/no-unused-vars (variables assigned but not used) - @typescript-eslint/no-explicit-any (type safety warnings) These remaining issues are non-blocking and can be addressed incrementally. https://claude.ai/code/session_015R3U3kiprQiNTTNNt31Sg6
This commit is contained in:
@@ -19,6 +19,7 @@ export abstract class AbstractSocketClient extends EventEmitter {
|
||||
if (maxListeners === 0) {
|
||||
this.config.logger?.warn('SocketClient setMaxListeners(0) allows UNLIMITED listeners - potential memory leak!')
|
||||
}
|
||||
|
||||
this.setMaxListeners(maxListeners)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export class WebSocketClient extends AbstractSocketClient {
|
||||
if (maxListeners === 0) {
|
||||
this.config.logger?.warn('WebSocket setMaxListeners(0) allows UNLIMITED listeners - potential memory leak!')
|
||||
}
|
||||
|
||||
this.socket.setMaxListeners(maxListeners)
|
||||
|
||||
const events = ['close', 'error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response']
|
||||
|
||||
+21
-16
@@ -2,7 +2,7 @@ import NodeCache from '@cacheable/node-cache'
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { LRUCache } from 'lru-cache'
|
||||
import { proto } from '../../WAProto/index.js'
|
||||
import { DEFAULT_CACHE_TTLS, DEFAULT_CACHE_MAX_KEYS, PROCESSABLE_HISTORY_TYPES } from '../Defaults'
|
||||
import { DEFAULT_CACHE_MAX_KEYS, DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from '../Defaults'
|
||||
import type {
|
||||
BotListInfo,
|
||||
CacheStore,
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
newLTHashState,
|
||||
processSyncAction
|
||||
} from '../Utils'
|
||||
import { makeMutex, makeKeyedMutex } from '../Utils/make-mutex'
|
||||
import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex'
|
||||
import processMessage from '../Utils/process-message'
|
||||
import { buildTcTokenFromJid } from '../Utils/tc-token-utils'
|
||||
import {
|
||||
@@ -70,7 +70,17 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
getMessage
|
||||
} = config
|
||||
const sock = makeSocket(config)
|
||||
const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession } = sock
|
||||
const {
|
||||
ev,
|
||||
ws,
|
||||
authState,
|
||||
generateMessageTag,
|
||||
sendNode,
|
||||
query,
|
||||
signalRepository,
|
||||
onUnexpectedError,
|
||||
sendUnifiedSession
|
||||
} = sock
|
||||
|
||||
let privacySettings: { [_: string]: string } | undefined
|
||||
|
||||
@@ -641,9 +651,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
// or key not found
|
||||
const isKeyNotFound = error.output?.statusCode === 404
|
||||
const isIrrecoverableError =
|
||||
(attemptsMap[name] || 0) >= MAX_SYNC_ATTEMPTS ||
|
||||
isKeyNotFound ||
|
||||
error.name === 'TypeError'
|
||||
(attemptsMap[name] || 0) >= MAX_SYNC_ATTEMPTS || isKeyNotFound || error.name === 'TypeError'
|
||||
if (isKeyNotFound) {
|
||||
const currentVersion = states[name]?.version ?? 0
|
||||
logger.info(
|
||||
@@ -656,6 +664,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
`failed to sync state from version${isIrrecoverableError ? '' : ', removing and trying from scratch'}`
|
||||
)
|
||||
}
|
||||
|
||||
await authState.keys.set({ 'app-state-sync-version': { [name]: null } })
|
||||
// increment number of retries
|
||||
attemptsMap[name] = (attemptsMap[name] || 0) + 1
|
||||
@@ -765,13 +774,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
logger.warn({ toJid }, 'sendPresenceUpdate: failed to decode toJid, skipping')
|
||||
return
|
||||
}
|
||||
|
||||
const { server } = decoded
|
||||
const isLid = server === 'lid'
|
||||
|
||||
await sendNode({
|
||||
tag: 'chatstate',
|
||||
attrs: {
|
||||
from: isLid ? (me.lid || me.id) : me.id,
|
||||
from: isLid ? me.lid || me.id : me.id,
|
||||
to: toJid
|
||||
},
|
||||
content: [
|
||||
@@ -826,6 +836,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
logger.warn({ jid }, 'handlePresenceUpdate: firstChild content is empty, skipping')
|
||||
return
|
||||
}
|
||||
|
||||
let type = firstChild.tag as WAPresence
|
||||
if (type === 'paused') {
|
||||
type = 'available'
|
||||
@@ -1294,7 +1305,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}, 20_000)
|
||||
})
|
||||
|
||||
ev.on('lid-mapping.update', async (mappings) => {
|
||||
ev.on('lid-mapping.update', async mappings => {
|
||||
try {
|
||||
const result = await signalRepository.lidMapping.storeLIDPNMappings(mappings)
|
||||
logger.debug(
|
||||
@@ -1319,10 +1330,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
const pnUser = jidNormalizedUser(mapping.pn)
|
||||
|
||||
if (lidUser && pnUser && lidUser !== pnUser) {
|
||||
logger.debug(
|
||||
{ lid: lidUser, pn: pnUser },
|
||||
'collected chat update for LID→PN merge notification'
|
||||
)
|
||||
logger.debug({ lid: lidUser, pn: pnUser }, 'collected chat update for LID→PN merge notification')
|
||||
|
||||
mergeNotifications.push({
|
||||
id: pnUser,
|
||||
@@ -1335,10 +1343,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
|
||||
// Emit single batch of merge notifications for better performance
|
||||
if (mergeNotifications.length > 0) {
|
||||
logger.debug(
|
||||
{ count: mergeNotifications.length },
|
||||
'emitting batch of chat merge notifications'
|
||||
)
|
||||
logger.debug({ count: mergeNotifications.length }, 'emitting batch of chat merge notifications')
|
||||
ev.emit('chats.update', mergeNotifications)
|
||||
}
|
||||
|
||||
|
||||
+6
-17
@@ -1,7 +1,7 @@
|
||||
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
|
||||
import type { UserFacingSocketConfig, WAVersion } from '../Types'
|
||||
import { getCachedVersion, refreshVersionCache, clearVersionCache, getVersionCacheStatus } from '../Utils/version-cache'
|
||||
import type { VersionCacheLogger } from '../Utils/version-cache'
|
||||
import { clearVersionCache, getCachedVersion, getVersionCacheStatus, refreshVersionCache } from '../Utils/version-cache'
|
||||
import { makeCommunitiesSocket } from './communities'
|
||||
|
||||
/**
|
||||
@@ -109,18 +109,13 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) =>
|
||||
fromCache,
|
||||
ageMinutes: fromCache ? Math.round(age / 60000) : 0
|
||||
},
|
||||
fromCache
|
||||
? 'Using cached WhatsApp Web version'
|
||||
: 'Fetched fresh WhatsApp Web version'
|
||||
fromCache ? 'Using cached WhatsApp Web version' : 'Fetched fresh WhatsApp Web version'
|
||||
)
|
||||
|
||||
mergedConfig.version = version
|
||||
trackedVersion = [...version] as WAVersion
|
||||
} catch (error) {
|
||||
logger?.warn(
|
||||
{ error, fallbackVersion: mergedConfig.version },
|
||||
'Error fetching version, using bundled version'
|
||||
)
|
||||
logger?.warn({ error, fallbackVersion: mergedConfig.version }, 'Error fetching version, using bundled version')
|
||||
}
|
||||
|
||||
// Create the socket
|
||||
@@ -128,7 +123,7 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) =>
|
||||
|
||||
// Listen for connection close to cleanup interval (Fix #1, #6)
|
||||
// This handles both explicit sock.end() and internal disconnections
|
||||
sock.ev.on('connection.update', (update) => {
|
||||
sock.ev.on('connection.update', update => {
|
||||
if (update.connection === 'close') {
|
||||
isSocketClosed = true
|
||||
cleanupInterval()
|
||||
@@ -139,10 +134,7 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) =>
|
||||
|
||||
// Setup periodic version check if interval > 0
|
||||
if (checkIntervalMs > 0) {
|
||||
logger?.info(
|
||||
{ intervalHours: checkIntervalMs / (60 * 60 * 1000) },
|
||||
'Starting periodic version check'
|
||||
)
|
||||
logger?.info({ intervalHours: checkIntervalMs / (60 * 60 * 1000) }, 'Starting periodic version check')
|
||||
|
||||
versionCheckInterval = setInterval(async () => {
|
||||
// Skip if socket is closed (Fix #8 - race condition)
|
||||
@@ -169,10 +161,7 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) =>
|
||||
|
||||
// Don't update to fallback version on transient network errors
|
||||
if (!fetchSuccess) {
|
||||
logger?.warn(
|
||||
{ fallbackVersion: result.version },
|
||||
'Failed to fetch latest version, keeping current version'
|
||||
)
|
||||
logger?.warn({ fallbackVersion: result.version }, 'Failed to fetch latest version, keeping current version')
|
||||
return // Skip version update on fetch failure
|
||||
}
|
||||
} else if (cacheStatus.version) {
|
||||
|
||||
+66
-49
@@ -3,8 +3,14 @@ import { Boom } from '@hapi/boom'
|
||||
import { randomBytes } from 'crypto'
|
||||
import Long from 'long'
|
||||
import { proto } from '../../WAProto/index.js'
|
||||
import { DEFAULT_CACHE_TTLS, DEFAULT_SESSION_CLEANUP_CONFIG, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults'
|
||||
import { metrics, recordMessageReceived, recordHistorySyncMessages, recordMessageRetry, recordMessageFailure } from '../Utils/prometheus-metrics.js'
|
||||
import {
|
||||
DEFAULT_CACHE_TTLS,
|
||||
DEFAULT_SESSION_CLEANUP_CONFIG,
|
||||
KEY_BUNDLE_TYPE,
|
||||
MIN_PREKEY_COUNT,
|
||||
PLACEHOLDER_MAX_AGE_SECONDS,
|
||||
STATUS_EXPIRY_SECONDS
|
||||
} from '../Defaults'
|
||||
import type {
|
||||
GroupParticipant,
|
||||
MessageReceiptType,
|
||||
@@ -18,12 +24,10 @@ import type {
|
||||
WAPatchName
|
||||
} from '../Types'
|
||||
import { WAMessageStatus, WAMessageStubType } from '../Types'
|
||||
import { logMessageReceived } from '../Utils/baileys-logger'
|
||||
import {
|
||||
aesDecryptCTR,
|
||||
aesEncryptGCM,
|
||||
cleanMessage,
|
||||
normalizeMessageJids,
|
||||
Curve,
|
||||
decodeMediaRetryNode,
|
||||
decodeMessageNode,
|
||||
@@ -42,12 +46,21 @@ import {
|
||||
MISSING_KEYS_ERROR_TEXT,
|
||||
NACK_REASONS,
|
||||
NO_MESSAGE_FOUND_ERROR_TEXT,
|
||||
normalizeMessageJids,
|
||||
toNumber,
|
||||
unixTimestampSeconds,
|
||||
xmppPreKey,
|
||||
xmppSignedPreKey
|
||||
} from '../Utils'
|
||||
import { logMessageReceived } from '../Utils/baileys-logger'
|
||||
import { makeMutex } from '../Utils/make-mutex'
|
||||
import {
|
||||
metrics,
|
||||
recordHistorySyncMessages,
|
||||
recordMessageFailure,
|
||||
recordMessageReceived,
|
||||
recordMessageRetry
|
||||
} from '../Utils/prometheus-metrics.js'
|
||||
import {
|
||||
areJidsSameUser,
|
||||
type BinaryNode,
|
||||
@@ -81,7 +94,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
sessionCleanupConfig
|
||||
} = config
|
||||
// Use nullish coalescing to handle partial config properly
|
||||
const autoCleanCorrupted = sessionCleanupConfig?.autoCleanCorrupted ?? DEFAULT_SESSION_CLEANUP_CONFIG.autoCleanCorrupted
|
||||
const autoCleanCorrupted =
|
||||
sessionCleanupConfig?.autoCleanCorrupted ?? DEFAULT_SESSION_CLEANUP_CONFIG.autoCleanCorrupted
|
||||
const sock = makeMessagesSocket(config)
|
||||
const {
|
||||
ev,
|
||||
@@ -488,7 +502,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
|
||||
// Extract error code from retry node if present (for MAC error detection)
|
||||
const retryNode = getBinaryNodeChild(node, 'retry')
|
||||
const errorAttr = retryNode?.attrs?.error as string | undefined
|
||||
const errorAttr = retryNode?.attrs?.error
|
||||
const errorCode = messageRetryManager.parseRetryErrorCode(errorAttr)
|
||||
|
||||
const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists, errorCode)
|
||||
@@ -1060,7 +1074,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
recreateReason = result.reason
|
||||
|
||||
if (shouldRecreateSession) {
|
||||
logger.debug({ participant, retryCount, reason: recreateReason, errorCode }, 'recreating session for outgoing retry')
|
||||
logger.debug(
|
||||
{ participant, retryCount, reason: recreateReason, errorCode },
|
||||
'recreating session for outgoing retry'
|
||||
)
|
||||
// CRITICAL: Use same transaction key as encrypt/decrypt operations to prevent race
|
||||
// Using meId ensures this delete serializes with sendMessage() and other session operations
|
||||
await authState.keys.transaction(async () => {
|
||||
@@ -1255,7 +1272,14 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
category,
|
||||
author,
|
||||
decrypt
|
||||
} = decryptMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '', signalRepository, logger, autoCleanCorrupted)
|
||||
} = decryptMessageNode(
|
||||
node,
|
||||
authState.creds.me!.id,
|
||||
authState.creds.me!.lid || '',
|
||||
signalRepository,
|
||||
logger,
|
||||
autoCleanCorrupted
|
||||
)
|
||||
|
||||
const alt = msg.key.participantAlt || msg.key.remoteJidAlt
|
||||
// Handle LID/PN mappings with hybrid approach:
|
||||
@@ -1271,7 +1295,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
const existingMapping = await signalRepository.lidMapping.getPNForLID(alt)
|
||||
if (!existingMapping) {
|
||||
// Store mapping in background (non-critical, doesn't block decrypt)
|
||||
signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }])
|
||||
signalRepository.lidMapping
|
||||
.storeLIDPNMappings([{ lid: alt, pn: primaryJid }])
|
||||
.catch(error => logger.warn({ error, alt, primaryJid }, 'Background LID mapping storage failed'))
|
||||
}
|
||||
|
||||
@@ -1286,7 +1311,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
const existingMapping = await signalRepository.lidMapping.getLIDForPN(alt)
|
||||
if (!existingMapping) {
|
||||
// Store mapping in background (non-critical)
|
||||
signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }])
|
||||
signalRepository.lidMapping
|
||||
.storeLIDPNMappings([{ lid: primaryJid, pn: alt }])
|
||||
.catch(error => logger.warn({ error, alt, primaryJid }, 'Background LID mapping storage failed'))
|
||||
}
|
||||
|
||||
@@ -1320,7 +1346,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
// Fallback chain: remoteJid (normalized) > msg.key.id (unique) > 'unknown' (serializes all)
|
||||
let mutexKey = msg.key.remoteJid
|
||||
if (!mutexKey) {
|
||||
logger.warn({ msgId: msg.key.id, fromMe: msg.key.fromMe }, 'Missing remoteJid after normalization, using msg.key.id as fallback')
|
||||
logger.warn(
|
||||
{ msgId: msg.key.id, fromMe: msg.key.fromMe },
|
||||
'Missing remoteJid after normalization, using msg.key.id as fallback'
|
||||
)
|
||||
mutexKey = msg.key.id || 'unknown'
|
||||
}
|
||||
|
||||
@@ -1341,9 +1370,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
// Skip unavailable fanout types - these messages will never have content available
|
||||
// These are system messages that cannot be decrypted or retrieved
|
||||
const messageType = msg.messageStubParameters?.[2]
|
||||
if (messageType === 'bot_unavailable_fanout' ||
|
||||
if (
|
||||
messageType === 'bot_unavailable_fanout' ||
|
||||
messageType === 'hosted_unavailable_fanout' ||
|
||||
messageType === 'view_once_unavailable_fanout') {
|
||||
messageType === 'view_once_unavailable_fanout'
|
||||
) {
|
||||
logger.debug(
|
||||
{ msgId: msg.key?.id, messageType },
|
||||
'CTWA: Skipping placeholder resend for unavailable fanout type'
|
||||
@@ -1395,34 +1426,22 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
try {
|
||||
const requestId = await requestPlaceholderResend(msgKey, msgData)
|
||||
if (requestId && requestId !== 'RESOLVED') {
|
||||
logger.debug(
|
||||
{ msgId, requestId },
|
||||
'CTWA: Placeholder resend request sent successfully'
|
||||
)
|
||||
logger.debug({ msgId, requestId }, 'CTWA: Placeholder resend request sent successfully')
|
||||
metrics.ctwaRecoveryRequests.inc({ status: 'sent' })
|
||||
// Note: The actual message will be emitted via 'messages.upsert'
|
||||
// when the PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE is processed
|
||||
// in the PDO response handler in src/Utils/process-message.ts
|
||||
} else if (requestId === 'RESOLVED') {
|
||||
// Message was received while we were waiting
|
||||
logger.debug(
|
||||
{ msgId },
|
||||
'CTWA: Message received during resend delay'
|
||||
)
|
||||
logger.debug({ msgId }, 'CTWA: Message received during resend delay')
|
||||
metrics.ctwaMessagesRecovered.inc()
|
||||
metrics.ctwaRecoveryLatency.observe(Date.now() - startTime)
|
||||
} else {
|
||||
// Already requested (duplicate request prevented by cache)
|
||||
logger.debug(
|
||||
{ msgId },
|
||||
'CTWA: Resend already requested, skipping duplicate'
|
||||
)
|
||||
logger.debug({ msgId }, 'CTWA: Resend already requested, skipping duplicate')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
{ error, msgId },
|
||||
'CTWA: Failed to request placeholder resend'
|
||||
)
|
||||
logger.warn({ error, msgId }, 'CTWA: Failed to request placeholder resend')
|
||||
metrics.ctwaRecoveryFailures.inc({ reason: 'request_failed' })
|
||||
}
|
||||
})
|
||||
@@ -1433,24 +1452,15 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
try {
|
||||
const requestId = await requestPlaceholderResend(msgKey, msgData)
|
||||
if (requestId && requestId !== 'RESOLVED') {
|
||||
logger.debug(
|
||||
{ msgId, requestId },
|
||||
'CTWA: Placeholder resend request sent successfully (direct)'
|
||||
)
|
||||
logger.debug({ msgId, requestId }, 'CTWA: Placeholder resend request sent successfully (direct)')
|
||||
} else if (requestId === 'RESOLVED') {
|
||||
// Message arrived during the internal 2s delay in requestPlaceholderResend
|
||||
logger.debug(
|
||||
{ msgId },
|
||||
'CTWA: Message received before direct resend request completed'
|
||||
)
|
||||
logger.debug({ msgId }, 'CTWA: Message received before direct resend request completed')
|
||||
metrics.ctwaMessagesRecovered.inc()
|
||||
metrics.ctwaRecoveryLatency.observe(Date.now() - startTime)
|
||||
} else {
|
||||
// Already requested (duplicate request prevented by cache)
|
||||
logger.debug(
|
||||
{ msgId },
|
||||
'CTWA: Resend already requested, skipping duplicate (direct)'
|
||||
)
|
||||
logger.debug({ msgId }, 'CTWA: Resend already requested, skipping duplicate (direct)')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn({ error, msgId }, 'CTWA: Failed to request placeholder resend')
|
||||
@@ -1575,14 +1585,21 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
|
||||
// Record message received metric
|
||||
const msgContent = msg.message
|
||||
const msgType = msgContent?.conversation ? 'text'
|
||||
: msgContent?.imageMessage ? 'image'
|
||||
: msgContent?.videoMessage ? 'video'
|
||||
: msgContent?.audioMessage ? 'audio'
|
||||
: msgContent?.documentMessage ? 'document'
|
||||
: msgContent?.stickerMessage ? 'sticker'
|
||||
: msgContent?.reactionMessage ? 'reaction'
|
||||
: 'other'
|
||||
const msgType = msgContent?.conversation
|
||||
? 'text'
|
||||
: msgContent?.imageMessage
|
||||
? 'image'
|
||||
: msgContent?.videoMessage
|
||||
? 'video'
|
||||
: msgContent?.audioMessage
|
||||
? 'audio'
|
||||
: msgContent?.documentMessage
|
||||
? 'document'
|
||||
: msgContent?.stickerMessage
|
||||
? 'sticker'
|
||||
: msgContent?.reactionMessage
|
||||
? 'reaction'
|
||||
: 'other'
|
||||
recordMessageReceived(msgType)
|
||||
|
||||
// Track session activity for cleanup
|
||||
|
||||
+105
-97
@@ -16,7 +16,6 @@ import type {
|
||||
WAMessage,
|
||||
WAMessageKey
|
||||
} from '../Types'
|
||||
import { logMessageSent } from '../Utils/baileys-logger'
|
||||
import {
|
||||
aggregateMessageKeysNotFromMe,
|
||||
assertMediaContent,
|
||||
@@ -40,8 +39,10 @@ import {
|
||||
parseAndInjectE2ESessions,
|
||||
unixTimestampSeconds
|
||||
} from '../Utils'
|
||||
import { logMessageSent } from '../Utils/baileys-logger'
|
||||
import { getUrlInfo } from '../Utils/link-preview'
|
||||
import { makeKeyedMutex } from '../Utils/make-mutex'
|
||||
import { metrics, recordMessageFailure, recordMessageRetry, recordMessageSent } from '../Utils/prometheus-metrics'
|
||||
import { getMessageReportingToken, shouldIncludeReportingToken } from '../Utils/reporting-utils'
|
||||
import {
|
||||
areJidsSameUser,
|
||||
@@ -65,7 +66,6 @@ import {
|
||||
S_WHATSAPP_NET
|
||||
} from '../WABinary'
|
||||
import { USyncQuery, USyncUser } from '../WAUSync'
|
||||
import { recordMessageSent, recordMessageRetry, recordMessageFailure, metrics } from '../Utils/prometheus-metrics'
|
||||
import { makeNewsletterSocket } from './newsletter'
|
||||
|
||||
export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
@@ -331,12 +331,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
if (!meId) throw new Boom('Not authenticated', { statusCode: 401 })
|
||||
const meLid = authState.creds.me?.lid || ''
|
||||
|
||||
const extracted = extractDeviceJids(
|
||||
result?.list,
|
||||
meId,
|
||||
meLid,
|
||||
ignoreZeroDevices
|
||||
)
|
||||
const extracted = extractDeviceJids(result?.list, meId, meLid, ignoreZeroDevices)
|
||||
const deviceMap: { [_: string]: FullJid[] } = {}
|
||||
|
||||
for (const item of extracted) {
|
||||
@@ -465,9 +460,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
const wireJids = [
|
||||
...jidsRequiringFetch.filter(jid => isAnyLidUser(jid)),
|
||||
...(
|
||||
(await signalRepository.lidMapping.getLIDsForPNs(
|
||||
jidsRequiringFetch.filter(jid => isAnyPnUser(jid))
|
||||
)) || []
|
||||
(await signalRepository.lidMapping.getLIDsForPNs(jidsRequiringFetch.filter(jid => isAnyPnUser(jid)))) || []
|
||||
).map(a => a.lid)
|
||||
]
|
||||
|
||||
@@ -650,6 +643,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
if (message.interactiveMessage.nativeFlowMessage) {
|
||||
return 'native_flow'
|
||||
}
|
||||
|
||||
// Check if it's a carousel with nativeFlowMessage buttons in cards
|
||||
if (message.interactiveMessage.carouselMessage?.cards?.length) {
|
||||
const hasNativeFlowButtons = message.interactiveMessage.carouselMessage.cards.some(
|
||||
@@ -659,6 +653,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
return 'native_flow'
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a collection/product carousel
|
||||
if (message.interactiveMessage.carouselMessage?.cards?.length) {
|
||||
const hasCollectionCards = message.interactiveMessage.carouselMessage.cards.some(
|
||||
@@ -668,6 +663,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
return 'native_flow'
|
||||
}
|
||||
}
|
||||
|
||||
return 'interactive'
|
||||
}
|
||||
|
||||
@@ -693,6 +689,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
if (innerMessage.interactiveMessage.nativeFlowMessage) {
|
||||
return 'native_flow'
|
||||
}
|
||||
|
||||
// Check if it's a carousel with nativeFlowMessage buttons in cards
|
||||
if (innerMessage.interactiveMessage.carouselMessage?.cards?.length) {
|
||||
const hasNativeFlowButtons = innerMessage.interactiveMessage.carouselMessage.cards.some(
|
||||
@@ -702,6 +699,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
return 'native_flow'
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a collection/product carousel
|
||||
if (innerMessage.interactiveMessage.carouselMessage?.cards?.length) {
|
||||
const hasCollectionCards = innerMessage.interactiveMessage.carouselMessage.cards.some(
|
||||
@@ -711,6 +709,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
return 'native_flow'
|
||||
}
|
||||
}
|
||||
|
||||
return 'interactive'
|
||||
}
|
||||
}
|
||||
@@ -727,7 +726,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
|
||||
// For native_flow messages, check for special button types that need specific attributes
|
||||
if (buttonType === 'native_flow') {
|
||||
const interactiveMsg = message.interactiveMessage ||
|
||||
const interactiveMsg =
|
||||
message.interactiveMessage ||
|
||||
message.viewOnceMessage?.message?.interactiveMessage ||
|
||||
message.viewOnceMessageV2?.message?.interactiveMessage
|
||||
|
||||
@@ -758,7 +758,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
* Carousels should NOT have the bot node injected as they are not bot messages
|
||||
*/
|
||||
const isCarouselMessage = (message: proto.IMessage): boolean => {
|
||||
const interactiveMsg = message.interactiveMessage ||
|
||||
const interactiveMsg =
|
||||
message.interactiveMessage ||
|
||||
message.viewOnceMessage?.message?.interactiveMessage ||
|
||||
message.viewOnceMessageV2?.message?.interactiveMessage
|
||||
|
||||
@@ -774,7 +775,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
* These messages may need different biz node handling or no biz node at all
|
||||
*/
|
||||
const isCatalogMessage = (message: proto.IMessage): boolean => {
|
||||
const interactiveMsg = message.interactiveMessage ||
|
||||
const interactiveMsg =
|
||||
message.interactiveMessage ||
|
||||
message.viewOnceMessage?.message?.interactiveMessage ||
|
||||
message.viewOnceMessageV2?.message?.interactiveMessage
|
||||
|
||||
@@ -782,9 +784,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
if (nativeFlow?.buttons?.length) {
|
||||
// Check if any button is a catalog-type button
|
||||
return nativeFlow.buttons.some(
|
||||
(btn: any) => btn?.name === 'catalog_message' ||
|
||||
btn?.name === 'single_product' ||
|
||||
btn?.name === 'product_list'
|
||||
(btn: any) => btn?.name === 'catalog_message' || btn?.name === 'single_product' || btn?.name === 'product_list'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -796,16 +796,15 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
* Lists need type='list' in the biz node instead of type='native_flow'
|
||||
*/
|
||||
const isListNativeFlow = (message: proto.IMessage): boolean => {
|
||||
const interactiveMsg = message.interactiveMessage ||
|
||||
const interactiveMsg =
|
||||
message.interactiveMessage ||
|
||||
message.viewOnceMessage?.message?.interactiveMessage ||
|
||||
message.viewOnceMessageV2?.message?.interactiveMessage
|
||||
|
||||
const nativeFlow = interactiveMsg?.nativeFlowMessage
|
||||
if (nativeFlow?.buttons?.length) {
|
||||
// Check if any button is a list-type button (single_select or multi_select)
|
||||
return nativeFlow.buttons.some(
|
||||
(btn: any) => btn?.name === 'single_select' || btn?.name === 'multi_select'
|
||||
)
|
||||
return nativeFlow.buttons.some((btn: any) => btn?.name === 'single_select' || btn?.name === 'multi_select')
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -852,9 +851,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
const innerMsg = message.viewOnceMessage?.message
|
||||
const nativeFlow = innerMsg?.interactiveMessage?.nativeFlowMessage
|
||||
if (nativeFlow?.buttons?.length) {
|
||||
const singleSelectBtn = nativeFlow.buttons.find(
|
||||
(btn: any) => btn?.name === 'single_select'
|
||||
)
|
||||
const singleSelectBtn = nativeFlow.buttons.find((btn: any) => btn?.name === 'single_select')
|
||||
if (singleSelectBtn?.buttonParamsJson) {
|
||||
try {
|
||||
const params = JSON.parse(singleSelectBtn.buttonParamsJson)
|
||||
@@ -1156,6 +1153,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
logger.debug({ jid }, '[CAROUSEL] Skipping own device - DSM carousel causes error 479')
|
||||
continue
|
||||
}
|
||||
|
||||
meRecipients.push(jid)
|
||||
} else {
|
||||
otherRecipients.push(jid)
|
||||
@@ -1212,7 +1210,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
attrs: {
|
||||
v: '2',
|
||||
type,
|
||||
count: participant!.count.toString()
|
||||
count: participant.count.toString()
|
||||
},
|
||||
content: encryptedContent
|
||||
})
|
||||
@@ -1257,8 +1255,14 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
const startTime = Date.now()
|
||||
|
||||
// Debug: Log message structure to diagnose list detection
|
||||
const interactiveMsg = message.interactiveMessage || message.viewOnceMessage?.message?.interactiveMessage || message.viewOnceMessageV2?.message?.interactiveMessage
|
||||
const listMsg = message.listMessage || message.viewOnceMessage?.message?.listMessage || message.viewOnceMessageV2?.message?.listMessage
|
||||
const interactiveMsg =
|
||||
message.interactiveMessage ||
|
||||
message.viewOnceMessage?.message?.interactiveMessage ||
|
||||
message.viewOnceMessageV2?.message?.interactiveMessage
|
||||
const listMsg =
|
||||
message.listMessage ||
|
||||
message.viewOnceMessage?.message?.listMessage ||
|
||||
message.viewOnceMessageV2?.message?.listMessage
|
||||
const nativeFlowButtons = interactiveMsg?.nativeFlowMessage?.buttons || []
|
||||
const isListDetected = isListNativeFlow(message)
|
||||
|
||||
@@ -1323,10 +1327,10 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
// Note: '' (empty) causes error 405 rejection from WhatsApp server
|
||||
// Special flows (payment, mpm, order) use specific names
|
||||
const SPECIAL_FLOW_NAMES: Record<string, string> = {
|
||||
'review_and_pay': 'payment_info',
|
||||
'payment_info': 'payment_info',
|
||||
'mpm': 'mpm',
|
||||
'review_order': 'order_details'
|
||||
review_and_pay: 'payment_info',
|
||||
payment_info: 'payment_info',
|
||||
mpm: 'mpm',
|
||||
review_order: 'order_details'
|
||||
}
|
||||
const firstButtonName = allButtonNames[0] || ''
|
||||
const nativeFlowName = SPECIAL_FLOW_NAMES[firstButtonName] || 'mixed'
|
||||
@@ -1370,11 +1374,9 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
// - quick_reply buttons with bot node: only smartphone, not Web ❌
|
||||
const isNativeFlowButtons = buttonType === 'native_flow'
|
||||
|
||||
const isPrivateUserChat = (
|
||||
isPnUser(destinationJid) ||
|
||||
isLidUser(destinationJid) ||
|
||||
destinationJid?.endsWith('@c.us')
|
||||
) && !isJidBot(destinationJid)
|
||||
const isPrivateUserChat =
|
||||
(isPnUser(destinationJid) || isLidUser(destinationJid) || destinationJid?.endsWith('@c.us')) &&
|
||||
!isJidBot(destinationJid)
|
||||
|
||||
if (isPrivateUserChat && !isCarousel && !isCatalog && buttonType !== 'list' && !isNativeFlowButtons) {
|
||||
;(stanza.content as BinaryNode[]).push({
|
||||
@@ -1406,10 +1408,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
metrics.interactiveMessagesSuccess.inc({ type: buttonType })
|
||||
metrics.interactiveMessagesLatency.observe({ type: buttonType }, Date.now() - startTime)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{ error, msgId, buttonType },
|
||||
'[EXPERIMENTAL] Failed to inject biz node for interactive message'
|
||||
)
|
||||
logger.error({ error, msgId, buttonType }, '[EXPERIMENTAL] Failed to inject biz node for interactive message')
|
||||
metrics.interactiveMessagesFailures.inc({ type: buttonType, reason: 'injection_failed' })
|
||||
}
|
||||
} else if (buttonType && !enableInteractiveMessages) {
|
||||
@@ -1497,15 +1496,23 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
logMessageSent(msgId, destinationJid)
|
||||
|
||||
// Record message sent metric
|
||||
const msgType = message.conversation ? 'text'
|
||||
: message.imageMessage ? 'image'
|
||||
: message.videoMessage ? 'video'
|
||||
: message.audioMessage ? 'audio'
|
||||
: message.documentMessage ? 'document'
|
||||
: message.stickerMessage ? 'sticker'
|
||||
: message.stickerPackMessage ? 'sticker_pack'
|
||||
: message.reactionMessage ? 'reaction'
|
||||
: 'other'
|
||||
const msgType = message.conversation
|
||||
? 'text'
|
||||
: message.imageMessage
|
||||
? 'image'
|
||||
: message.videoMessage
|
||||
? 'video'
|
||||
: message.audioMessage
|
||||
? 'audio'
|
||||
: message.documentMessage
|
||||
? 'document'
|
||||
: message.stickerMessage
|
||||
? 'sticker'
|
||||
: message.stickerPackMessage
|
||||
? 'sticker_pack'
|
||||
: message.reactionMessage
|
||||
? 'reaction'
|
||||
: 'other'
|
||||
recordMessageSent(msgType)
|
||||
|
||||
// Add message to retry cache if enabled
|
||||
@@ -1728,17 +1735,13 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
const startTime = Date.now()
|
||||
const userJid = authState.creds.me!.id
|
||||
|
||||
const {
|
||||
medias,
|
||||
delay: delayConfig = 'adaptive',
|
||||
retryCount = 3,
|
||||
continueOnFailure = true
|
||||
} = album
|
||||
const { medias, delay: delayConfig = 'adaptive', retryCount = 3, continueOnFailure = true } = album
|
||||
|
||||
// Validation (also done in generateWAMessageContent, but double-check here)
|
||||
if (!medias || medias.length < 2) {
|
||||
throw new Boom('Album must have at least 2 media items', { statusCode: 400 })
|
||||
}
|
||||
|
||||
if (medias.length > 10) {
|
||||
throw new Boom('Album cannot have more than 10 media items (WhatsApp limit)', { statusCode: 400 })
|
||||
}
|
||||
@@ -1754,25 +1757,30 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
)
|
||||
|
||||
// Generate album root message first (with counts of expected media)
|
||||
const albumRootMsg = await generateWAMessage(jid, {
|
||||
album: { medias, delay: delayConfig, retryCount, continueOnFailure }
|
||||
}, {
|
||||
logger,
|
||||
userJid,
|
||||
getUrlInfo: text => getUrlInfo(text, {
|
||||
thumbnailWidth: linkPreviewImageThumbnailWidth,
|
||||
fetchOpts: { timeout: 3_000, ...(httpRequestOptions || {}) },
|
||||
const albumRootMsg = await generateWAMessage(
|
||||
jid,
|
||||
{
|
||||
album: { medias, delay: delayConfig, retryCount, continueOnFailure }
|
||||
},
|
||||
{
|
||||
logger,
|
||||
uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined
|
||||
}),
|
||||
upload: waUploadToServer,
|
||||
mediaCache: config.mediaCache,
|
||||
// Don't spread options here to avoid messageId collision
|
||||
timestamp: options.timestamp,
|
||||
quoted: options.quoted,
|
||||
ephemeralExpiration: options.ephemeralExpiration,
|
||||
mediaUploadTimeoutMs: options.mediaUploadTimeoutMs
|
||||
})
|
||||
userJid,
|
||||
getUrlInfo: text =>
|
||||
getUrlInfo(text, {
|
||||
thumbnailWidth: linkPreviewImageThumbnailWidth,
|
||||
fetchOpts: { timeout: 3_000, ...(httpRequestOptions || {}) },
|
||||
logger,
|
||||
uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined
|
||||
}),
|
||||
upload: waUploadToServer,
|
||||
mediaCache: config.mediaCache,
|
||||
// Don't spread options here to avoid messageId collision
|
||||
timestamp: options.timestamp,
|
||||
quoted: options.quoted,
|
||||
ephemeralExpiration: options.ephemeralExpiration,
|
||||
mediaUploadTimeoutMs: options.mediaUploadTimeoutMs
|
||||
}
|
||||
)
|
||||
|
||||
const albumKey = albumRootMsg.key
|
||||
|
||||
@@ -1788,9 +1796,13 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
process.nextTick(async () => {
|
||||
let mutexKey = albumRootMsg.key.remoteJid
|
||||
if (!mutexKey) {
|
||||
logger.warn({ msgId: albumRootMsg.key.id }, 'Missing remoteJid in albumRootMsg, using msg.key.id as fallback')
|
||||
logger.warn(
|
||||
{ msgId: albumRootMsg.key.id },
|
||||
'Missing remoteJid in albumRootMsg, using msg.key.id as fallback'
|
||||
)
|
||||
mutexKey = albumRootMsg.key.id || 'unknown'
|
||||
}
|
||||
|
||||
await messageMutex.mutex(mutexKey, () => upsertMessage(albumRootMsg, 'append'))
|
||||
})
|
||||
}
|
||||
@@ -1812,7 +1824,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
const mediaTypeMultiplier = isVideo ? 2.0 : 1.0
|
||||
|
||||
// Later items in album get slightly more delay (cumulative load)
|
||||
const positionMultiplier = 1 + (index * 0.1)
|
||||
const positionMultiplier = 1 + index * 0.1
|
||||
|
||||
// Add some jitter to prevent predictable patterns
|
||||
const jitter = Math.random() * 200
|
||||
@@ -1827,16 +1839,14 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
if (delayConfig === 'adaptive') {
|
||||
return calculateAdaptiveDelay(media, index)
|
||||
}
|
||||
|
||||
return delayConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a single media item with retry logic
|
||||
*/
|
||||
const sendMediaWithRetry = async (
|
||||
media: AlbumMediaItem,
|
||||
index: number
|
||||
): Promise<AlbumMediaResult> => {
|
||||
const sendMediaWithRetry = async (media: AlbumMediaItem, index: number): Promise<AlbumMediaResult> => {
|
||||
const itemStartTime = Date.now()
|
||||
let lastError: Error | undefined
|
||||
let attempts = 0
|
||||
@@ -1845,16 +1855,17 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
attempts = attempt + 1
|
||||
try {
|
||||
// Generate message for this media item
|
||||
// NOTE: Each item needs its own unique messageId, so we don't spread options.messageId
|
||||
// NOTE: Each item needs its own unique messageId, so we don't spread options.messageId
|
||||
const mediaMsg = await generateWAMessage(jid, media as AnyMessageContent, {
|
||||
logger,
|
||||
userJid,
|
||||
getUrlInfo: text => getUrlInfo(text, {
|
||||
thumbnailWidth: linkPreviewImageThumbnailWidth,
|
||||
fetchOpts: { timeout: 3_000, ...(httpRequestOptions || {}) },
|
||||
logger,
|
||||
uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined
|
||||
}),
|
||||
getUrlInfo: text =>
|
||||
getUrlInfo(text, {
|
||||
thumbnailWidth: linkPreviewImageThumbnailWidth,
|
||||
fetchOpts: { timeout: 3_000, ...(httpRequestOptions || {}) },
|
||||
logger,
|
||||
uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined
|
||||
}),
|
||||
upload: waUploadToServer,
|
||||
mediaCache: config.mediaCache,
|
||||
// Don't spread ...options to avoid messageId collision
|
||||
@@ -1874,6 +1885,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
if (!mediaMsg.message.messageContextInfo) {
|
||||
mediaMsg.message.messageContextInfo = {}
|
||||
}
|
||||
|
||||
mediaMsg.message.messageContextInfo.messageAssociation = {
|
||||
associationType: proto.MessageAssociation.AssociationType.MEDIA_ALBUM,
|
||||
parentMessageKey: albumKey
|
||||
@@ -1893,14 +1905,12 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
logger.warn({ msgId: mediaMsg.key.id }, 'Missing remoteJid in mediaMsg, using msg.key.id as fallback')
|
||||
mutexKey = mediaMsg.key.id || 'unknown'
|
||||
}
|
||||
|
||||
await messageMutex.mutex(mutexKey, () => upsertMessage(mediaMsg, 'append'))
|
||||
})
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
{ index, msgId: mediaMsg.key.id, attempts },
|
||||
'Album media item sent successfully'
|
||||
)
|
||||
logger.debug({ index, msgId: mediaMsg.key.id, attempts }, 'Album media item sent successfully')
|
||||
|
||||
return {
|
||||
index,
|
||||
@@ -1925,10 +1935,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
// All retries exhausted
|
||||
logger.error(
|
||||
{ index, attempts, error: lastError?.message },
|
||||
'Album media item failed after all retries'
|
||||
)
|
||||
logger.error({ index, attempts, error: lastError?.message }, 'Album media item failed after all retries')
|
||||
|
||||
return {
|
||||
index,
|
||||
@@ -2005,7 +2012,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
if (typeof content === 'object' && 'album' in content) {
|
||||
throw new Boom(
|
||||
'Cannot send album messages with sendMessage(). Use sendAlbumMessage() instead, ' +
|
||||
'which properly sends the album root and individual media items.',
|
||||
'which properly sends the album root and individual media items.',
|
||||
{ statusCode: 400 }
|
||||
)
|
||||
}
|
||||
@@ -2027,7 +2034,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
upload: waUploadToServer,
|
||||
mediaCache: config.mediaCache,
|
||||
options: config.options,
|
||||
jid,
|
||||
jid
|
||||
})
|
||||
|
||||
// Pass the plain JS object directly to relayMessage
|
||||
@@ -2037,7 +2044,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
messageId,
|
||||
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
||||
additionalAttributes: {},
|
||||
statusJidList: options.statusJidList,
|
||||
statusJidList: options.statusJidList
|
||||
})
|
||||
|
||||
// Build WebMessageInfo only for event emission and return value
|
||||
@@ -2151,6 +2158,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
logger.warn({ msgId: fullMsg.key.id }, 'Missing remoteJid in fullMsg, using msg.key.id as fallback')
|
||||
mutexKey = fullMsg.key.id || 'unknown'
|
||||
}
|
||||
|
||||
await messageMutex.mutex(mutexKey, () => upsertMessage(fullMsg, 'append'))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,10 +28,12 @@ const parseNewsletterCreateResponse = (response: NewsletterCreateResponse): News
|
||||
invite: thread.invite || '',
|
||||
subscribers: parseInt(thread.subscribers_count, 10) || 0,
|
||||
verification: thread.verification,
|
||||
picture: thread.picture ? {
|
||||
id: thread.picture.id || '',
|
||||
directPath: thread.picture.direct_path || ''
|
||||
} : { id: '', directPath: '' },
|
||||
picture: thread.picture
|
||||
? {
|
||||
id: thread.picture.id || '',
|
||||
directPath: thread.picture.direct_path || ''
|
||||
}
|
||||
: { id: '', directPath: '' },
|
||||
mute_state: viewer?.mute
|
||||
}
|
||||
}
|
||||
|
||||
+33
-26
@@ -13,8 +13,8 @@ import {
|
||||
NOISE_WA_HEADER,
|
||||
UPLOAD_TIMEOUT
|
||||
} from '../Defaults'
|
||||
import { makeSessionCleanup } from '../Signal/session-cleanup'
|
||||
import { makeSessionActivityTracker } from '../Signal/session-activity-tracker'
|
||||
import { makeSessionCleanup } from '../Signal/session-cleanup'
|
||||
import type { ConnectionState, LIDMapping, SocketConfig } from '../Types'
|
||||
import { DisconnectReason } from '../Types'
|
||||
import {
|
||||
@@ -44,6 +44,18 @@ import {
|
||||
createConnectionCircuitBreaker,
|
||||
createPreKeyCircuitBreaker
|
||||
} from '../Utils/circuit-breaker'
|
||||
import {
|
||||
decrementActiveConnections,
|
||||
incrementActiveConnections,
|
||||
recordConnectionAttempt,
|
||||
recordConnectionError
|
||||
} from '../Utils/prometheus-metrics'
|
||||
import {
|
||||
createUnifiedSessionManager,
|
||||
extractServerTime,
|
||||
shouldEnableUnifiedSession,
|
||||
type UnifiedSessionManager
|
||||
} from '../Utils/unified-session'
|
||||
import {
|
||||
assertNodeErrorFree,
|
||||
type BinaryNode,
|
||||
@@ -57,18 +69,6 @@ import {
|
||||
jidEncode,
|
||||
S_WHATSAPP_NET
|
||||
} from '../WABinary'
|
||||
import {
|
||||
recordConnectionError,
|
||||
recordConnectionAttempt,
|
||||
incrementActiveConnections,
|
||||
decrementActiveConnections
|
||||
} from '../Utils/prometheus-metrics'
|
||||
import {
|
||||
createUnifiedSessionManager,
|
||||
extractServerTime,
|
||||
shouldEnableUnifiedSession,
|
||||
type UnifiedSessionManager
|
||||
} from '../Utils/unified-session'
|
||||
import { BinaryInfo } from '../WAM/BinaryInfo.js'
|
||||
import { USyncQuery, USyncUser } from '../WAUSync/'
|
||||
import { WebSocketClient } from './Client'
|
||||
@@ -103,9 +103,8 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
} = config
|
||||
|
||||
// Resolve enableUnifiedSession: explicit config > env var > default (true)
|
||||
const enableUnifiedSession = enableUnifiedSessionConfig !== undefined
|
||||
? enableUnifiedSessionConfig
|
||||
: shouldEnableUnifiedSession()
|
||||
const enableUnifiedSession =
|
||||
enableUnifiedSessionConfig !== undefined ? enableUnifiedSessionConfig : shouldEnableUnifiedSession()
|
||||
|
||||
// Initialize circuit breakers if enabled
|
||||
let queryCircuitBreaker: CircuitBreaker | undefined
|
||||
@@ -223,6 +222,7 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
if (error instanceof CircuitOpenError) {
|
||||
logger.warn({ circuitName: error.circuitName }, 'Send blocked by connection circuit breaker')
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -246,6 +246,7 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
const sendNodeForSession = async (node: BinaryNode): Promise<void> => {
|
||||
await sendNode(node)
|
||||
}
|
||||
|
||||
unifiedSessionManager = createUnifiedSessionManager({
|
||||
enabled: true,
|
||||
logger,
|
||||
@@ -342,6 +343,7 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
if (error instanceof CircuitOpenError) {
|
||||
logger.warn({ circuitName: error.circuitName, state: error.state }, 'Query blocked by circuit breaker')
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -572,7 +574,7 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
})
|
||||
|
||||
if (sendMsg) {
|
||||
sendRawMessage(sendMsg).catch(onClose!)
|
||||
sendRawMessage(sendMsg).catch(onClose!)
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -857,9 +859,10 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
// 1. Multiple calls to cleanup (reentrancy guard)
|
||||
// 2. Timer orphaning: syncLoop creating timer after clearTimeout
|
||||
if (cleanedUp) {
|
||||
return // Already cleaned up
|
||||
return // Already cleaned up
|
||||
}
|
||||
cleanedUp = true // ← Set IMMEDIATELY to close race window
|
||||
|
||||
cleanedUp = true // ← Set IMMEDIATELY to close race window
|
||||
|
||||
ev.off('connection.update', connectionHandler)
|
||||
if (syncTimer) {
|
||||
@@ -957,10 +960,12 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
clearTimeout(ttlTimer)
|
||||
ttlTimer = undefined
|
||||
}
|
||||
|
||||
if (ttlGraceTimer) {
|
||||
clearTimeout(ttlGraceTimer)
|
||||
ttlGraceTimer = undefined
|
||||
}
|
||||
|
||||
sessionStartTime = undefined
|
||||
logger.info('🕐 Session TTL timers cleared on disconnect')
|
||||
}
|
||||
@@ -979,17 +984,20 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
logger.debug('🕐 Session TTL cleanup already called')
|
||||
return
|
||||
}
|
||||
cleanedUp = true // ← Set IMMEDIATELY to close race window
|
||||
|
||||
cleanedUp = true // ← Set IMMEDIATELY to close race window
|
||||
|
||||
ev.off('connection.update', connectionHandler)
|
||||
if (ttlTimer) {
|
||||
clearTimeout(ttlTimer)
|
||||
ttlTimer = undefined
|
||||
}
|
||||
|
||||
if (ttlGraceTimer) {
|
||||
clearTimeout(ttlGraceTimer)
|
||||
ttlGraceTimer = undefined
|
||||
}
|
||||
|
||||
logger.debug('🕐 Session TTL cleanup function executed')
|
||||
}
|
||||
}
|
||||
@@ -1053,7 +1061,8 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
logger.trace({ trace: error?.stack }, 'connection already closed')
|
||||
return
|
||||
}
|
||||
closed = true // ← Set IMMEDIATELY to close race window
|
||||
|
||||
closed = true // ← Set IMMEDIATELY to close race window
|
||||
|
||||
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed')
|
||||
|
||||
@@ -1089,6 +1098,7 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
default:
|
||||
errorType = `error_${statusCode}`
|
||||
}
|
||||
|
||||
recordConnectionError(errorType)
|
||||
recordConnectionAttempt('failure')
|
||||
}
|
||||
@@ -1115,7 +1125,7 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
try {
|
||||
await Promise.race([
|
||||
uploadPreKeysPromise,
|
||||
new Promise<void>((resolve) => setTimeout(resolve, 5000)) // 5s timeout
|
||||
new Promise<void>(resolve => setTimeout(resolve, 5000)) // 5s timeout
|
||||
])
|
||||
logger.debug('Pending pre-key upload completed or timed out')
|
||||
} catch (error) {
|
||||
@@ -1138,10 +1148,7 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
|
||||
// Detect socket-level session errors that require recreation
|
||||
const statusCode = (error as Boom)?.output?.statusCode || 0
|
||||
const isSessionError = (
|
||||
statusCode === DisconnectReason.badSession ||
|
||||
statusCode === DisconnectReason.restartRequired
|
||||
)
|
||||
const isSessionError = statusCode === DisconnectReason.badSession || statusCode === DisconnectReason.restartRequired
|
||||
|
||||
if (isSessionError) {
|
||||
logger.warn(
|
||||
|
||||
Reference in New Issue
Block a user