fix(types): remove non-null assertions in remaining 12 files

Files fixed:
- messages-media.ts: stream/buffer guards, file path validation
- decode-wa-message.ts: message field guards, optional chaining
- version-cache.ts: narrowing after null checks
- jid-utils.ts: split result guards, user fallbacks
- communities.ts: attrs fallbacks with ?? operator
- sticker-pack.ts: response guards
- business.ts: attrs guards
- socket.ts: onClose guard, pairingCode guard, creds.me guard
- event-buffer.ts: null guards
- signal.ts: null guards
- encode.ts (WAM): id null check with continue
- upstream history.ts: conversations/pushnames null guards

All 26 files across the project are now corrected.
Total: ~248 non-null assertions replaced with proper null guards.

https://claude.ai/code/session_01E2cfX1N3sJgCJBTvzGazSG
This commit is contained in:
Claude
2026-02-09 00:32:04 +00:00
parent 7e88ddb858
commit 1c9fb86cc3
12 changed files with 215 additions and 104 deletions
+16 -4
View File
@@ -118,14 +118,18 @@ export const makeBusinessSocket = (config: SocketConfig) => {
content: [ content: [
{ {
tag: 'cover_photo', tag: 'cover_photo',
attrs: { id: String(fbid), op: 'update', token: meta_hmac!, ts: String(ts) } attrs: { id: String(fbid), op: 'update', token: meta_hmac ?? '', ts: String(ts) }
} }
] ]
} }
] ]
}) })
return fbid! if (!fbid) {
throw new Error('Cover photo upload failed: missing fbid')
}
return fbid
} }
const removeCoverPhoto = async (id: string) => { const removeCoverPhoto = async (id: string) => {
@@ -332,7 +336,11 @@ export const makeBusinessSocket = (config: SocketConfig) => {
const productCatalogEditNode = getBinaryNodeChild(result, 'product_catalog_edit') const productCatalogEditNode = getBinaryNodeChild(result, 'product_catalog_edit')
const productNode = getBinaryNodeChild(productCatalogEditNode, 'product') const productNode = getBinaryNodeChild(productCatalogEditNode, 'product')
return parseProductNode(productNode!) if (!productNode) {
throw new Error('Product node not found in catalog edit response')
}
return parseProductNode(productNode)
} }
const productCreate = async (create: ProductCreate) => { const productCreate = async (create: ProductCreate) => {
@@ -372,7 +380,11 @@ export const makeBusinessSocket = (config: SocketConfig) => {
const productCatalogAddNode = getBinaryNodeChild(result, 'product_catalog_add') const productCatalogAddNode = getBinaryNodeChild(result, 'product_catalog_add')
const productNode = getBinaryNodeChild(productCatalogAddNode, 'product') const productNode = getBinaryNodeChild(productCatalogAddNode, 'product')
return parseProductNode(productNode!) if (!productNode) {
throw new Error('Product node not found in catalog add response')
}
return parseProductNode(productNode)
} }
const productDelete = async (productIds: string[]) => { const productDelete = async (productIds: string[]) => {
+17 -8
View File
@@ -100,7 +100,12 @@ export const makeCommunitiesSocket = (config: SocketConfig) => {
} }
sock.ws.on('CB:ib,,dirty', async (node: BinaryNode) => { sock.ws.on('CB:ib,,dirty', async (node: BinaryNode) => {
const { attrs } = getBinaryNodeChild(node, 'dirty')! const dirtyNode = getBinaryNodeChild(node, 'dirty')
if (!dirtyNode) {
return
}
const { attrs } = dirtyNode
if (attrs.type !== 'communities') { if (attrs.type !== 'communities') {
return return
} }
@@ -353,13 +358,13 @@ export const makeCommunitiesSocket = (config: SocketConfig) => {
communityAcceptInviteV4: ev.createBufferedFunction( communityAcceptInviteV4: ev.createBufferedFunction(
async (key: string | WAMessageKey, inviteMessage: proto.Message.IGroupInviteMessage) => { async (key: string | WAMessageKey, inviteMessage: proto.Message.IGroupInviteMessage) => {
key = typeof key === 'string' ? { remoteJid: key } : key key = typeof key === 'string' ? { remoteJid: key } : key
const results = await communityQuery(inviteMessage.groupJid!, 'set', [ const results = await communityQuery(inviteMessage.groupJid ?? '', 'set', [
{ {
tag: 'accept', tag: 'accept',
attrs: { attrs: {
code: inviteMessage.inviteCode!, code: inviteMessage.inviteCode ?? '',
expiration: inviteMessage.inviteExpiration!.toString(), expiration: (inviteMessage.inviteExpiration ?? 0).toString(),
admin: key.remoteJid! admin: key.remoteJid ?? ''
} }
} }
]) ])
@@ -432,7 +437,11 @@ export const makeCommunitiesSocket = (config: SocketConfig) => {
} }
export const extractCommunityMetadata = (result: BinaryNode) => { export const extractCommunityMetadata = (result: BinaryNode) => {
const community = getBinaryNodeChild(result, 'community')! const community = getBinaryNodeChild(result, 'community')
if (!community) {
throw new Error('Missing community node in result')
}
const descChild = getBinaryNodeChild(community, 'description') const descChild = getBinaryNodeChild(community, 'description')
let desc: string | undefined let desc: string | undefined
let descId: string | undefined let descId: string | undefined
@@ -466,12 +475,12 @@ export const extractCommunityMetadata = (result: BinaryNode) => {
participants: getBinaryNodeChildren(community, 'participant').map(({ attrs }) => { participants: getBinaryNodeChildren(community, 'participant').map(({ attrs }) => {
return { return {
// TODO: IMPLEMENT THE PN/LID FIELDS HERE!! // TODO: IMPLEMENT THE PN/LID FIELDS HERE!!
id: attrs.jid!, id: attrs.jid ?? '',
admin: (attrs.type || null) as GroupParticipant['admin'] admin: (attrs.type || null) as GroupParticipant['admin']
} }
}), }),
ephemeralDuration: eph ? +eph : undefined, ephemeralDuration: eph ? +eph : undefined,
addressingMode: getBinaryNodeChildString(community, 'addressing_mode')! as GroupMetadata['addressingMode'] addressingMode: getBinaryNodeChildString(community, 'addressing_mode') as GroupMetadata['addressingMode']
} }
return metadata return metadata
} }
+38 -10
View File
@@ -552,7 +552,11 @@ export const makeSocket = (config: SocketConfig) => {
}) })
if (sendMsg) { if (sendMsg) {
sendRawMessage(sendMsg).catch(onClose!) if (!onClose) {
throw new Error('onClose handler not initialized')
}
sendRawMessage(sendMsg).catch(onClose)
} }
return result return result
@@ -609,8 +613,12 @@ export const makeSocket = (config: SocketConfig) => {
}, },
content: [{ tag: 'count', attrs: {} }] content: [{ tag: 'count', attrs: {} }]
}) })
const countChild = getBinaryNodeChild(result, 'count')! const countChild = getBinaryNodeChild(result, 'count')
return +countChild.attrs.value! if (!countChild) {
return 0
}
return +(countChild.attrs.value ?? 0)
} }
// Pre-key upload state management // Pre-key upload state management
@@ -885,7 +893,11 @@ export const makeSocket = (config: SocketConfig) => {
return return
} }
const duration = Date.now() - sessionStartTime! if (!sessionStartTime) {
return
}
const duration = Date.now() - sessionStartTime
const durationHours = Math.floor(duration / 1000 / 60 / 60) const durationHours = Math.floor(duration / 1000 / 60 / 60)
logger.info(`🕐 Session TTL reached after ${durationHours}h, initiating graceful cleanup`) logger.info(`🕐 Session TTL reached after ${durationHours}h, initiating graceful cleanup`)
@@ -1305,7 +1317,11 @@ export const makeSocket = (config: SocketConfig) => {
async function generatePairingKey() { async function generatePairingKey() {
const salt = randomBytes(32) const salt = randomBytes(32)
const randomIv = randomBytes(16) const randomIv = randomBytes(16)
const key = await derivePairingCodeKey(authState.creds.pairingCode!, salt) if (!authState.creds.pairingCode) {
throw new Error('Pairing code not set')
}
const key = await derivePairingCodeKey(authState.creds.pairingCode, salt)
const ciphered = aesEncryptCTR(authState.creds.pairingEphemeralKeyPair.public, key, randomIv) const ciphered = aesEncryptCTR(authState.creds.pairingEphemeralKeyPair.public, key, randomIv)
return Buffer.concat([salt, randomIv, ciphered]) return Buffer.concat([salt, randomIv, ciphered])
} }
@@ -1352,7 +1368,7 @@ export const makeSocket = (config: SocketConfig) => {
attrs: { attrs: {
to: S_WHATSAPP_NET, to: S_WHATSAPP_NET,
type: 'result', type: 'result',
id: stanza.attrs.id! id: stanza.attrs.id ?? ''
} }
} }
await sendNode(iq) await sendNode(iq)
@@ -1431,7 +1447,9 @@ export const makeSocket = (config: SocketConfig) => {
logger.info('opened connection to WA') logger.info('opened connection to WA')
clearTimeout(qrTimer) // will never happen in all likelyhood -- but just in case WA sends success on first try clearTimeout(qrTimer) // will never happen in all likelyhood -- but just in case WA sends success on first try
ev.emit('creds.update', { me: { ...authState.creds.me!, lid: node.attrs.lid } }) if (authState.creds.me) {
ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } })
}
ev.emit('connection.update', { connection: 'open' }) ev.emit('connection.update', { connection: 'open' })
@@ -1454,13 +1472,23 @@ export const makeSocket = (config: SocketConfig) => {
const myLID = node.attrs.lid const myLID = node.attrs.lid
process.nextTick(async () => { process.nextTick(async () => {
try { try {
const myPN = authState.creds.me!.id const me = authState.creds.me
if (!me) {
return
}
const myPN = me.id
// Store our own LID-PN mapping // Store our own LID-PN mapping
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }]) await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }])
// Create device list for our own user (needed for bulk migration) // Create device list for our own user (needed for bulk migration)
const { user, device } = jidDecode(myPN)! const decoded = jidDecode(myPN)
if (!decoded) {
return
}
const { user, device } = decoded
await authState.keys.set({ await authState.keys.set({
'device-list': { 'device-list': {
[user]: [device?.toString() || '0'] [user]: [device?.toString() || '0']
@@ -1548,7 +1576,7 @@ export const makeSocket = (config: SocketConfig) => {
logger.debug({ name }, 'updated pushName') logger.debug({ name }, 'updated pushName')
sendNode({ sendNode({
tag: 'presence', tag: 'presence',
attrs: { name: name! } attrs: { name: name ?? '' }
}).catch(err => { }).catch(err => {
logger.warn({ trace: err.stack }, 'error in sending presence update on name change') logger.warn({ trace: err.stack }, 'error in sending presence update on name change')
}) })
+18 -12
View File
@@ -127,6 +127,10 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin
const msgId = stanza.attrs.id const msgId = stanza.attrs.id
const from = stanza.attrs.from const from = stanza.attrs.from
if (!from) {
throw new Boom('Missing from attribute in message', { data: stanza })
}
const participant: string | undefined = stanza.attrs.participant const participant: string | undefined = stanza.attrs.participant
const recipient: string | undefined = stanza.attrs.recipient const recipient: string | undefined = stanza.attrs.recipient
@@ -137,21 +141,21 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin
if (isPnUser(from) || isLidUser(from) || isHostedLidUser(from) || isHostedPnUser(from)) { if (isPnUser(from) || isLidUser(from) || isHostedLidUser(from) || isHostedPnUser(from)) {
if (recipient && !isJidMetaAI(recipient)) { if (recipient && !isJidMetaAI(recipient)) {
if (!isMe(from!) && !isMeLid(from!)) { if (!isMe(from) && !isMeLid(from)) {
throw new Boom('receipient present, but msg not from me', { data: stanza }) throw new Boom('receipient present, but msg not from me', { data: stanza })
} }
if (isMe(from!) || isMeLid(from!)) { if (isMe(from) || isMeLid(from)) {
fromMe = true fromMe = true
} }
chatId = recipient chatId = recipient
} else { } else {
chatId = from! chatId = from
} }
msgType = 'chat' msgType = 'chat'
author = from! author = from
} else if (isJidGroup(from)) { } else if (isJidGroup(from)) {
if (!participant) { if (!participant) {
throw new Boom('No participant in group message') throw new Boom('No participant in group message')
@@ -163,28 +167,28 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin
msgType = 'group' msgType = 'group'
author = participant author = participant
chatId = from! chatId = from
} else if (isJidBroadcast(from)) { } else if (isJidBroadcast(from)) {
if (!participant) { if (!participant) {
throw new Boom('No participant in group message') throw new Boom('No participant in group message')
} }
const isParticipantMe = isMe(participant) const isParticipantMe = isMe(participant)
if (isJidStatusBroadcast(from!)) { if (isJidStatusBroadcast(from)) {
msgType = isParticipantMe ? 'direct_peer_status' : 'other_status' msgType = isParticipantMe ? 'direct_peer_status' : 'other_status'
} else { } else {
msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast' msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast'
} }
fromMe = isParticipantMe fromMe = isParticipantMe
chatId = from! chatId = from
author = participant author = participant
} else if (isJidNewsletter(from)) { } else if (isJidNewsletter(from)) {
msgType = 'newsletter' msgType = 'newsletter'
chatId = from! chatId = from
author = from! author = from
if (isMe(from!) || isMeLid(from!)) { if (isMe(from) || isMeLid(from)) {
fromMe = true fromMe = true
} }
} else { } else {
@@ -207,7 +211,7 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin
const fullMessage: WAMessage = { const fullMessage: WAMessage = {
key, key,
category: stanza.attrs.category, category: stanza.attrs.category,
messageTimestamp: +stanza.attrs.t!, messageTimestamp: +(stanza.attrs.t ?? 0),
pushName: pushname, pushName: pushname,
broadcast: isJidBroadcast(from) broadcast: isJidBroadcast(from)
} }
@@ -241,9 +245,11 @@ export const decryptMessageNode = (
for (const { tag, attrs, content } of stanza.content) { for (const { tag, attrs, content } of stanza.content) {
if (tag === 'verified_name' && content instanceof Uint8Array) { if (tag === 'verified_name' && content instanceof Uint8Array) {
const cert = proto.VerifiedNameCertificate.decode(content) const cert = proto.VerifiedNameCertificate.decode(content)
const details = proto.VerifiedNameCertificate.Details.decode(cert.details!) if (cert.details) {
const details = proto.VerifiedNameCertificate.Details.decode(cert.details)
fullMessage.verifiedBizName = details.verifiedName fullMessage.verifiedBizName = details.verifiedName
} }
}
if (tag === 'unavailable' && attrs.type === 'view_once') { if (tag === 'unavailable' && attrs.type === 'view_once') {
fullMessage.key.isViewOnce = true // TODO: remove from here and add a STUB TYPE fullMessage.key.isViewOnce = true // TODO: remove from here and add a STUB TYPE
+27 -17
View File
@@ -289,8 +289,9 @@ class AdaptiveTimeoutCalculator {
return // Not enough data return // Not enough data
} }
const oldest = this.eventTimestamps[0]! const oldest = this.eventTimestamps[0]
const newest = this.eventTimestamps[this.eventTimestamps.length - 1]! const newest = this.eventTimestamps[this.eventTimestamps.length - 1]
if (oldest === undefined || newest === undefined) return
const timeSpan = newest - oldest const timeSpan = newest - oldest
if (timeSpan === 0) return if (timeSpan === 0) return
@@ -337,8 +338,9 @@ class AdaptiveTimeoutCalculator {
if (this.eventTimestamps.length < 2) { if (this.eventTimestamps.length < 2) {
return 0 return 0
} }
const oldest = this.eventTimestamps[0]! const oldest = this.eventTimestamps[0]
const newest = this.eventTimestamps[this.eventTimestamps.length - 1]! const newest = this.eventTimestamps[this.eventTimestamps.length - 1]
if (oldest === undefined || newest === undefined) return 0
const timeSpan = newest - oldest const timeSpan = newest - oldest
if (timeSpan === 0) return 0 if (timeSpan === 0) return 0
return (this.eventTimestamps.length / timeSpan) * 1000 return (this.eventTimestamps.length / timeSpan) * 1000
@@ -633,8 +635,11 @@ export const makeEventBuffer = (
for (const update of chatUpdates) { for (const update of chatUpdates) {
if (update.conditional) { if (update.conditional) {
conditionalChatUpdatesLeft += 1 conditionalChatUpdatesLeft += 1
newData.chatUpdates[update.id!] = update const updateId = update.id
delete data.chatUpdates[update.id!] if (updateId) {
newData.chatUpdates[updateId] = update
delete data.chatUpdates[updateId]
}
} }
} }
@@ -723,7 +728,7 @@ export const makeEventBuffer = (
const { type } = evData as BaileysEventMap['messages.upsert'] const { type } = evData as BaileysEventMap['messages.upsert']
const existingUpserts = Object.values(data.messageUpserts) const existingUpserts = Object.values(data.messageUpserts)
if (existingUpserts.length > 0) { if (existingUpserts.length > 0) {
const bufferedType = existingUpserts[0]!.type const bufferedType = existingUpserts[0]?.type
if (bufferedType !== type) { if (bufferedType !== type) {
logger.debug({ bufferedType, newType: type }, 'messages.upsert type mismatch, emitting buffered messages') logger.debug({ bufferedType, newType: type }, 'messages.upsert type mismatch, emitting buffered messages')
// Emit the buffered messages with their correct type // Emit the buffered messages with their correct type
@@ -973,7 +978,8 @@ function append<E extends BufferableEvent>(
break break
case 'chats.update': case 'chats.update':
for (const update of eventData as ChatUpdate[]) { for (const update of eventData as ChatUpdate[]) {
const chatId = update.id! const chatId = update.id
if (!chatId) continue
const conditionMatches = update.conditional ? update.conditional(data) : true const conditionMatches = update.conditional ? update.conditional(data) : true
if (conditionMatches) { if (conditionMatches) {
delete update.conditional delete update.conditional
@@ -1039,8 +1045,9 @@ function append<E extends BufferableEvent>(
data.contactUpserts[contact.id] = upsert data.contactUpserts[contact.id] = upsert
} }
if (data.contactUpdates[contact.id]) { const existingContactUpdate = data.contactUpdates[contact.id]
upsert = Object.assign(data.contactUpdates[contact.id]!, trimUndefined(contact)) as Contact if (existingContactUpdate) {
upsert = Object.assign(existingContactUpdate, trimUndefined(contact)) as Contact
delete data.contactUpdates[contact.id] delete data.contactUpdates[contact.id]
} }
} }
@@ -1049,7 +1056,8 @@ function append<E extends BufferableEvent>(
case 'contacts.update': case 'contacts.update':
const contactUpdates = eventData as BaileysEventMap['contacts.update'] const contactUpdates = eventData as BaileysEventMap['contacts.update']
for (const update of contactUpdates) { for (const update of contactUpdates) {
const id = update.id! const id = update.id
if (!id) continue
// merge into prior upsert // merge into prior upsert
const upsert = data.historySets.contacts[id] || data.contactUpserts[id] const upsert = data.historySets.contacts[id] || data.contactUpserts[id]
if (upsert) { if (upsert) {
@@ -1170,7 +1178,8 @@ function append<E extends BufferableEvent>(
case 'groups.update': case 'groups.update':
const groupUpdates = eventData as BaileysEventMap['groups.update'] const groupUpdates = eventData as BaileysEventMap['groups.update']
for (const update of groupUpdates) { for (const update of groupUpdates) {
const id = update.id! const id = update.id
if (!id) continue
const groupUpdate = data.groupUpdates[id] || {} const groupUpdate = data.groupUpdates[id] || {}
if (!data.groupUpdates[id]) { if (!data.groupUpdates[id]) {
data.groupUpdates[id] = Object.assign(groupUpdate, update) data.groupUpdates[id] = Object.assign(groupUpdate, update)
@@ -1202,7 +1211,8 @@ function append<E extends BufferableEvent>(
function decrementChatReadCounterIfMsgDidUnread(message: WAMessage) { function decrementChatReadCounterIfMsgDidUnread(message: WAMessage) {
// decrement chat unread counter // decrement chat unread counter
// if the message has already been marked read by us // if the message has already been marked read by us
const chatId = message.key.remoteJid! const chatId = message.key.remoteJid
if (!chatId) return
const chat = data.chatUpdates[chatId] || data.chatUpserts[chatId] const chat = data.chatUpdates[chatId] || data.chatUpserts[chatId]
if ( if (
isRealMessage(message) && isRealMessage(message) &&
@@ -1255,7 +1265,7 @@ function consolidateEvents(data: BufferedEventData) {
const messageUpsertList = Object.values(data.messageUpserts) const messageUpsertList = Object.values(data.messageUpserts)
if (messageUpsertList.length) { if (messageUpsertList.length) {
const type = messageUpsertList[0]!.type const type = messageUpsertList[0]?.type
map['messages.upsert'] = { map['messages.upsert'] = {
messages: messageUpsertList.map(m => m.message), messages: messageUpsertList.map(m => m.message),
type type
@@ -1311,7 +1321,7 @@ function consolidateEvents(data: BufferedEventData) {
function concatChats<C extends Partial<Chat>>(a: C, b: Partial<Chat>) { function concatChats<C extends Partial<Chat>>(a: C, b: Partial<Chat>) {
if ( if (
b.unreadCount === null && // neutralize unread counter b.unreadCount === null && // neutralize unread counter
a.unreadCount! < 0 (a.unreadCount ?? 0) < 0
) { ) {
a.unreadCount = undefined a.unreadCount = undefined
b.unreadCount = undefined b.unreadCount = undefined
@@ -1319,8 +1329,8 @@ function concatChats<C extends Partial<Chat>>(a: C, b: Partial<Chat>) {
if (typeof a.unreadCount === 'number' && typeof b.unreadCount === 'number') { if (typeof a.unreadCount === 'number' && typeof b.unreadCount === 'number') {
b = { ...b } b = { ...b }
if (b.unreadCount! >= 0) { if ((b.unreadCount ?? 0) >= 0) {
b.unreadCount = Math.max(b.unreadCount!, 0) + Math.max(a.unreadCount, 0) b.unreadCount = Math.max(b.unreadCount ?? 0, 0) + Math.max(a.unreadCount, 0)
} }
} }
+28 -13
View File
@@ -317,7 +317,7 @@ export const getStream = async (item: WAMediaUpload, opts?: RequestInit & { maxC
const urlStr = item.url.toString() const urlStr = item.url.toString()
if (urlStr.startsWith('data:')) { if (urlStr.startsWith('data:')) {
const buffer = Buffer.from(urlStr.split(',')[1]!, 'base64') const buffer = Buffer.from(urlStr.split(',')[1] ?? '', 'base64')
return { stream: toReadable(buffer), type: 'buffer' } as const return { stream: toReadable(buffer), type: 'buffer' } as const
} }
@@ -414,7 +414,11 @@ export const encryptedStream = async (
let fileLength = 0 let fileLength = 0
const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv) const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv)
const hmac = Crypto.createHmac('sha256', macKey!).update(iv) if (!macKey) {
throw new Boom('Failed to derive media mac key')
}
const hmac = Crypto.createHmac('sha256', macKey).update(iv)
const sha256Plain = Crypto.createHash('sha256') const sha256Plain = Crypto.createHash('sha256')
const sha256Enc = Crypto.createHash('sha256') const sha256Enc = Crypto.createHash('sha256')
@@ -528,7 +532,7 @@ export const downloadContentFromMessage = async (
opts: MediaDownloadOptions = {} opts: MediaDownloadOptions = {}
) => { ) => {
const isValidMediaUrl = url?.startsWith('https://mmg.whatsapp.net/') const isValidMediaUrl = url?.startsWith('https://mmg.whatsapp.net/')
const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath!) const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath ?? '')
if (!downloadUrl) { if (!downloadUrl) {
throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 }) throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 })
} }
@@ -591,8 +595,8 @@ export const downloadEncryptedContent = async (
const pushBytes = (bytes: Buffer, push: (bytes: Buffer) => void) => { const pushBytes = (bytes: Buffer, push: (bytes: Buffer) => void) => {
if (startByte || endByte) { if (startByte || endByte) {
const start = bytesFetched >= startByte! ? undefined : Math.max(startByte! - bytesFetched, 0) const start = bytesFetched >= (startByte ?? 0) ? undefined : Math.max((startByte ?? 0) - bytesFetched, 0)
const end = bytesFetched + bytes.length < endByte! ? undefined : Math.max(endByte! - bytesFetched, 0) const end = bytesFetched + bytes.length < (endByte ?? 0) ? undefined : Math.max((endByte ?? 0) - bytesFetched, 0)
push(bytes.slice(start, end)) push(bytes.slice(start, end))
@@ -652,7 +656,7 @@ export function extensionForMediaMessage(message: WAMessageContent) {
extension = '.jpeg' extension = '.jpeg'
} else { } else {
const messageContent = message[type] as WAGenericMediaMessage const messageContent = message[type] as WAGenericMediaMessage
extension = getExtension(messageContent.mimetype!)! extension = getExtension(messageContent.mimetype ?? '') ?? ''
} }
return extension return extension
@@ -860,8 +864,8 @@ export const getWAUploadToServer = (
if (result?.url || result?.direct_path) { if (result?.url || result?.direct_path) {
urls = { urls = {
mediaUrl: result.url!, mediaUrl: result.url ?? '',
directPath: result.direct_path!, directPath: result.direct_path ?? '',
meta_hmac: result.meta_hmac, meta_hmac: result.meta_hmac,
fbid: result.fbid, fbid: result.fbid,
ts: result.ts ts: result.ts
@@ -896,17 +900,25 @@ const getMediaRetryKey = (mediaKey: Buffer | Uint8Array) => {
* Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL * Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
*/ */
export const encryptMediaRetryRequest = (key: WAMessageKey, mediaKey: Buffer | Uint8Array, meId: string) => { export const encryptMediaRetryRequest = (key: WAMessageKey, mediaKey: Buffer | Uint8Array, meId: string) => {
if (!key.id) {
throw new Boom('Missing message ID for media retry request')
}
if (!key.remoteJid) {
throw new Boom('Missing remote JID for media retry request')
}
const recp: proto.IServerErrorReceipt = { stanzaId: key.id } const recp: proto.IServerErrorReceipt = { stanzaId: key.id }
const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish() const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
const iv = Crypto.randomBytes(12) const iv = Crypto.randomBytes(12)
const retryKey = getMediaRetryKey(mediaKey) const retryKey = getMediaRetryKey(mediaKey)
const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id!)) const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id))
const req: BinaryNode = { const req: BinaryNode = {
tag: 'receipt', tag: 'receipt',
attrs: { attrs: {
id: key.id!, id: key.id,
to: jidNormalizedUser(meId), to: jidNormalizedUser(meId),
type: 'server-error' type: 'server-error'
}, },
@@ -925,7 +937,7 @@ export const encryptMediaRetryRequest = (key: WAMessageKey, mediaKey: Buffer | U
{ {
tag: 'rmr', tag: 'rmr',
attrs: { attrs: {
jid: key.remoteJid!, jid: key.remoteJid,
from_me: (!!key.fromMe).toString(), from_me: (!!key.fromMe).toString(),
// @ts-ignore // @ts-ignore
participant: key.participant || undefined participant: key.participant || undefined
@@ -938,7 +950,10 @@ export const encryptMediaRetryRequest = (key: WAMessageKey, mediaKey: Buffer | U
} }
export const decodeMediaRetryNode = (node: BinaryNode) => { export const decodeMediaRetryNode = (node: BinaryNode) => {
const rmrNode = getBinaryNodeChild(node, 'rmr')! const rmrNode = getBinaryNodeChild(node, 'rmr')
if (!rmrNode) {
throw new Boom('Missing rmr node in media retry response')
}
const event: BaileysEventMap['messages.media-update'][number] = { const event: BaileysEventMap['messages.media-update'][number] = {
key: { key: {
@@ -951,7 +966,7 @@ export const decodeMediaRetryNode = (node: BinaryNode) => {
const errorNode = getBinaryNodeChild(node, 'error') const errorNode = getBinaryNodeChild(node, 'error')
if (errorNode) { if (errorNode) {
const errorCode = +errorNode.attrs.code! const errorCode = +(errorNode.attrs.code ?? '0')
event.error = new Boom(`Failed to re-upload media (${errorCode})`, { event.error = new Boom(`Failed to re-upload media (${errorCode})`, {
data: errorNode.attrs, data: errorNode.attrs,
statusCode: getStatusCodeForMediaRetry(errorCode) statusCode: getStatusCodeForMediaRetry(errorCode)
+31 -17
View File
@@ -88,14 +88,18 @@ export const xmppPreKey = (pair: KeyPair, id: number): BinaryNode => ({
}) })
export const parseAndInjectE2ESessions = async (node: BinaryNode, repository: SignalRepositoryWithLIDStore) => { export const parseAndInjectE2ESessions = async (node: BinaryNode, repository: SignalRepositoryWithLIDStore) => {
const extractKey = (key: BinaryNode) => const extractKey = (key: BinaryNode) => {
key if (!key) return undefined
? { const keyId = getBinaryNodeChildUInt(key, 'id', 3)
keyId: getBinaryNodeChildUInt(key, 'id', 3)!, const publicKeyBuf = getBinaryNodeChildBuffer(key, 'value')
publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')!), const signature = getBinaryNodeChildBuffer(key, 'signature')
signature: getBinaryNodeChildBuffer(key, 'signature')! if (keyId === undefined || !publicKeyBuf || !signature) return undefined
return {
keyId,
publicKey: generateSignalPubKey(publicKeyBuf),
signature
}
} }
: undefined
const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user') const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user')
for (const node of nodes) { for (const node of nodes) {
assertNodeErrorFree(node) assertNodeErrorFree(node)
@@ -111,20 +115,26 @@ export const parseAndInjectE2ESessions = async (node: BinaryNode, repository: Si
for (const nodesChunk of chunks) { for (const nodesChunk of chunks) {
for (const node of nodesChunk) { for (const node of nodesChunk) {
const signedKey = getBinaryNodeChild(node, 'skey')! const signedKey = getBinaryNodeChild(node, 'skey')
const key = getBinaryNodeChild(node, 'key')! const key = getBinaryNodeChild(node, 'key')
const identity = getBinaryNodeChildBuffer(node, 'identity')! const identity = getBinaryNodeChildBuffer(node, 'identity')
const jid = node.attrs.jid! const jid = node.attrs.jid
if (!signedKey || !key || !identity || !jid) continue
const registrationId = getBinaryNodeChildUInt(node, 'registration', 4) const registrationId = getBinaryNodeChildUInt(node, 'registration', 4)
if (registrationId === undefined) continue
const signedPreKey = extractKey(signedKey)
const preKey = extractKey(key)
if (!signedPreKey || !preKey) continue
await repository.injectE2ESession({ await repository.injectE2ESession({
jid, jid,
session: { session: {
registrationId: registrationId!, registrationId,
identityKey: generateSignalPubKey(identity), identityKey: generateSignalPubKey(identity),
signedPreKey: extractKey(signedKey)!, signedPreKey,
preKey: extractKey(key)! preKey
} }
}) })
} }
@@ -137,14 +147,18 @@ export const extractDeviceJids = (
myLid: string, myLid: string,
excludeZeroDevices: boolean excludeZeroDevices: boolean
) => { ) => {
const { user: myUser, device: myDevice } = jidDecode(myJid)! const myJidDecoded = jidDecode(myJid)
if (!myJidDecoded) return []
const { user: myUser, device: myDevice } = myJidDecoded
const extracted: FullJid[] = [] const extracted: FullJid[] = []
for (const userResult of result) { for (const userResult of result) {
const { devices, id } = userResult as { devices: ParsedDeviceInfo; id: string } const { devices, id } = userResult as { devices: ParsedDeviceInfo; id: string }
const decoded = jidDecode(id)!, const decoded = jidDecode(id)
{ user, server } = decoded if (!decoded) continue
const { user, server } = decoded
let { domainType } = decoded let { domainType } = decoded
const deviceList = devices?.deviceList as DeviceListData[] const deviceList = devices?.deviceList as DeviceListData[]
if (!Array.isArray(deviceList)) continue if (!Array.isArray(deviceList)) continue
+1 -1
View File
@@ -575,7 +575,7 @@ export const prepareStickerPackMessage = async (
duplicateCount++ duplicateCount++
// Merge emojis (deduplicate) // Merge emojis (deduplicate)
const mergedEmojis = Array.from(new Set([...existingMetadata.emojis!, ...emojis])) const mergedEmojis = Array.from(new Set([...(existingMetadata.emojis ?? []), ...emojis]))
existingMetadata.emojis = mergedEmojis existingMetadata.emojis = mergedEmojis
// Merge accessibility labels (concatenate with separator if both exist) // Merge accessibility labels (concatenate with separator if both exist)
+6 -6
View File
@@ -165,19 +165,19 @@ export async function getCachedVersion(
} = config } = config
// 1. Check memory cache first (fastest) // 1. Check memory cache first (fastest)
if (isCacheValid(memoryCache, cacheTtlMs)) { if (isCacheValid(memoryCache, cacheTtlMs) && memoryCache) {
const age = Date.now() - memoryCache!.fetchedAt const age = Date.now() - memoryCache.fetchedAt
logger?.debug({ age: Math.round(age / 1000) + 's' }, 'Using memory cached version') logger?.debug({ age: Math.round(age / 1000) + 's' }, 'Using memory cached version')
return { version: memoryCache!.version, fromCache: true, age, source: 'memory' } return { version: memoryCache.version, fromCache: true, age, source: 'memory' }
} }
// 2. Check file cache (survives restarts) // 2. Check file cache (survives restarts)
const fileCache = await readCacheFile(cacheFilePath) const fileCache = await readCacheFile(cacheFilePath)
if (isCacheValid(fileCache, cacheTtlMs)) { if (isCacheValid(fileCache, cacheTtlMs) && fileCache) {
memoryCache = fileCache // Update memory cache memoryCache = fileCache // Update memory cache
const age = Date.now() - fileCache!.fetchedAt const age = Date.now() - fileCache.fetchedAt
logger?.debug({ age: Math.round(age / 1000) + 's' }, 'Using file cached version') logger?.debug({ age: Math.round(age / 1000) + 's' }, 'Using file cached version')
return { version: fileCache!.version, fromCache: true, age, source: 'file' } return { version: fileCache.version, fromCache: true, age, source: 'file' }
} }
// 3. Need to fetch - but deduplicate concurrent requests // 3. Need to fetch - but deduplicate concurrent requests
+12 -7
View File
@@ -55,15 +55,15 @@ export const jidEncode = (user: string | number | null, server: JidServer, devic
export const jidDecode = (jid: string | undefined): FullJid | undefined => { export const jidDecode = (jid: string | undefined): FullJid | undefined => {
// todo: investigate how to implement hosted ids in this case // todo: investigate how to implement hosted ids in this case
const sepIdx = typeof jid === 'string' ? jid.indexOf('@') : -1 const sepIdx = typeof jid === 'string' ? jid.indexOf('@') : -1
if (sepIdx < 0) { if (sepIdx < 0 || !jid) {
return undefined return undefined
} }
const server = jid!.slice(sepIdx + 1) const server = jid.slice(sepIdx + 1)
const userCombined = jid!.slice(0, sepIdx) const userCombined = jid.slice(0, sepIdx)
const [userAgent, device] = userCombined.split(':') const [userAgent, device] = userCombined.split(':')
const [user, agent] = userAgent!.split('_') const [user, agent] = (userAgent ?? '').split('_')
let domainType = WAJIDDomains.WHATSAPP let domainType = WAJIDDomains.WHATSAPP
if (server === 'lid') { if (server === 'lid') {
@@ -78,7 +78,7 @@ export const jidDecode = (jid: string | undefined): FullJid | undefined => {
return { return {
server: server as JidServer, server: server as JidServer,
user: user!, user: user ?? '',
domainType, domainType,
device: device ? +device : undefined device: device ? +device : undefined
} }
@@ -114,7 +114,7 @@ export const isAnyPnUser = (jid: string | undefined) =>
const botRegexp = /^1313555\d{4}$|^131655500\d{2}$/ const botRegexp = /^1313555\d{4}$|^131655500\d{2}$/
export const isJidBot = (jid: string | undefined) => jid && botRegexp.test(jid.split('@')[0]!) && jid.endsWith('@c.us') export const isJidBot = (jid: string | undefined) => jid && botRegexp.test(jid.split('@')[0] ?? '') && jid.endsWith('@c.us')
export const jidNormalizedUser = (jid: string | undefined) => { export const jidNormalizedUser = (jid: string | undefined) => {
const result = jidDecode(jid) const result = jidDecode(jid)
@@ -129,6 +129,11 @@ export const jidNormalizedUser = (jid: string | undefined) => {
export const transferDevice = (fromJid: string, toJid: string) => { export const transferDevice = (fromJid: string, toJid: string) => {
const fromDecoded = jidDecode(fromJid) const fromDecoded = jidDecode(fromJid)
const deviceId = fromDecoded?.device || 0 const deviceId = fromDecoded?.device || 0
const { server, user } = jidDecode(toJid)! const toDecoded = jidDecode(toJid)
if (!toDecoded) {
return ''
}
const { server, user } = toDecoded
return jidEncode(user, server, deviceId) return jidEncode(user, server, deviceId)
} }
+5 -1
View File
@@ -78,7 +78,11 @@ function encodeEvents(binaryInfo: BinaryInfo) {
} }
const fieldFlag = extended ? FLAG_EVENT : FLAG_FIELD | FLAG_EXTENDED const fieldFlag = extended ? FLAG_EVENT : FLAG_FIELD | FLAG_EXTENDED
binaryInfo.buffer.push(serializeData(id!, value, fieldFlag)) if (id == null) {
continue
}
binaryInfo.buffer.push(serializeData(id, value, fieldFlag))
} }
} }
} }
+14 -6
View File
@@ -194,9 +194,13 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
case proto.HistorySync.HistorySyncType.RECENT: case proto.HistorySync.HistorySyncType.RECENT:
case proto.HistorySync.HistorySyncType.FULL: case proto.HistorySync.HistorySyncType.FULL:
case proto.HistorySync.HistorySyncType.ON_DEMAND: case proto.HistorySync.HistorySyncType.ON_DEMAND:
for (const chat of item.conversations! as Chat[]) { for (const chat of (item.conversations ?? []) as Chat[]) {
if (!chat.id) {
continue
}
contacts.push({ contacts.push({
id: chat.id!, id: chat.id,
name: chat.name || undefined, name: chat.name || undefined,
lid: chat.lidJid || undefined, lid: chat.lidJid || undefined,
phoneNumber: chat.pnJid || undefined phoneNumber: chat.pnJid || undefined
@@ -204,7 +208,7 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
// Source 2: Extract from conversation metadata // Source 2: Extract from conversation metadata
addLidPnMapping( addLidPnMapping(
extractLidPnFromConversation(chat.id!, chat.lidJid, chat.pnJid) extractLidPnFromConversation(chat.id, chat.lidJid, chat.pnJid)
) )
const msgs = chat.messages || [] const msgs = chat.messages || []
@@ -242,7 +246,7 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
message.messageStubParameters?.[0] message.messageStubParameters?.[0]
) { ) {
contacts.push({ contacts.push({
id: message.key.participant || message.key.remoteJid!, id: message.key.participant || message.key.remoteJid || '',
verifiedName: message.messageStubParameters?.[0] verifiedName: message.messageStubParameters?.[0]
}) })
} }
@@ -253,8 +257,12 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
break break
case proto.HistorySync.HistorySyncType.PUSH_NAME: case proto.HistorySync.HistorySyncType.PUSH_NAME:
for (const c of item.pushnames!) { for (const c of item.pushnames ?? []) {
contacts.push({ id: c.id!, notify: c.pushname! }) if (!c.id) {
continue
}
contacts.push({ id: c.id, notify: c.pushname ?? '' })
} }
break break