fix(types): replace all non-null assertions with proper null guards

fix(types): replace all non-null assertions with proper null guards
This commit is contained in:
Renato Alcara
2026-02-08 21:43:52 -03:00
committed by GitHub
27 changed files with 848 additions and 395 deletions
+3 -3
View File
@@ -149,11 +149,11 @@ const startSock = async() => {
// go to an old chat and send this
if (text == "onDemandHistSync") {
const messageId = await sock.fetchMessageHistory(50, msg.key, msg.messageTimestamp!)
const messageId = await sock.fetchMessageHistory(50, msg.key, msg.messageTimestamp ?? 0)
logger.debug({ id: messageId }, 'requested on-demand history resync')
}
if (!msg.key.fromMe && doReplies && !isJidNewsletter(msg.key?.remoteJid!)) {
if (!msg.key.fromMe && doReplies && !isJidNewsletter(msg.key?.remoteJid ?? '')) {
const id = generateMessageIDV2(sock.user?.id)
logger.debug({id, orig_id: msg.key.id }, 'replying to message')
await sock.sendMessage(msg.key.remoteJid!, { text: 'pong '+msg.key.id }, {messageId: id })
@@ -208,7 +208,7 @@ const startSock = async() => {
if(typeof contact.imgUrl !== 'undefined') {
const newUrl = contact.imgUrl === null
? null
: await sock!.profilePictureUrl(contact.id!).catch(() => null)
: await sock.profilePictureUrl(contact.id ?? '').catch(() => null)
logger.debug({id: contact.id, newUrl}, `contact has a new profile pic` )
}
}
+11 -7
View File
@@ -27,7 +27,7 @@ export class SenderKeyMessage extends CiphertextMessage {
super()
if (serialized) {
const version = serialized[0]!
const version = serialized[0] ?? 0
const message = serialized.slice(1, serialized.length - this.SIGNATURE_LENGTH)
const signature = serialized.slice(-1 * this.SIGNATURE_LENGTH)
const senderKeyMessage = proto.SenderKeyMessage.decode(message).toJSON() as SenderKeyMessageStructure
@@ -42,22 +42,26 @@ export class SenderKeyMessage extends CiphertextMessage {
: senderKeyMessage.ciphertext
this.signature = signature
} else {
if (ciphertext == null || keyId == null || iteration == null || signatureKey == null) {
throw new Error('Missing required parameters for SenderKeyMessage construction')
}
const version = (((this.CURRENT_VERSION << 4) | this.CURRENT_VERSION) & 0xff) % 256
const ciphertextBuffer = Buffer.from(ciphertext!)
const ciphertextBuffer = Buffer.from(ciphertext)
const message = proto.SenderKeyMessage.encode(
proto.SenderKeyMessage.create({
id: keyId!,
iteration: iteration!,
id: keyId,
iteration: iteration,
ciphertext: ciphertextBuffer
})
).finish()
const signature = this.getSignature(signatureKey!, Buffer.concat([Buffer.from([version]), message]))
const signature = this.getSignature(signatureKey, Buffer.concat([Buffer.from([version]), message]))
this.serialized = Buffer.concat([Buffer.from([version]), message, Buffer.from(signature)])
this.messageVersion = this.CURRENT_VERSION
this.keyId = keyId!
this.iteration = iteration!
this.keyId = keyId
this.iteration = iteration
this.ciphertext = ciphertextBuffer
this.signature = signature
}
+22 -7
View File
@@ -493,7 +493,12 @@ export function makeLibSignalRepository(
return { migrated: 0, skipped: 0, total: 1 }
}
const { user } = jidDecode(fromJid)!
const decoded1 = jidDecode(fromJid)
if (!decoded1) {
return { migrated: 0, skipped: 0, total: 0 }
}
const { user } = decoded1
logger.debug({ fromJid }, 'bulk device migration - loading all user devices')
@@ -503,7 +508,7 @@ export function makeLibSignalRepository(
return { migrated: 0, skipped: 0, total: 0 }
}
const { device: fromDevice } = jidDecode(fromJid)!
const { device: fromDevice } = decoded1
const fromDeviceStr = fromDevice?.toString() || '0'
if (!userDevices.includes(fromDeviceStr)) {
userDevices.push(fromDeviceStr)
@@ -562,8 +567,11 @@ export function makeLibSignalRepository(
const migrationOps: MigrationOp[] = deviceJids.map(jid => {
const lidWithDevice = transferDevice(jid, toJid)
const fromDecoded = jidDecode(jid)!
const toDecoded = jidDecode(lidWithDevice)!
const fromDecoded = jidDecode(jid)
const toDecoded = jidDecode(lidWithDevice)
if (!fromDecoded || !toDecoded) {
throw new Error(`Failed to decode JID during migration: ${jid} -> ${lidWithDevice}`)
}
return {
fromJid: jid,
@@ -630,7 +638,10 @@ export function makeLibSignalRepository(
}
const jidToSignalProtocolAddress = (jid: string): libsignal.ProtocolAddress => {
const decoded = jidDecode(jid)!
const decoded = jidDecode(jid)
if (!decoded) {
throw new Error(`Failed to decode JID: "${jid}"`)
}
const { user, device, server, domainType } = decoded
if (!user) {
@@ -688,12 +699,16 @@ function signalStorage(
const resolveLIDSignalAddress = async (id: string): Promise<string> => {
if (id.includes('.')) {
const [deviceId, device] = id.split('.')
const [user, domainType_] = deviceId!.split('_')
if (!deviceId) {
throw new Error('Missing device ID')
}
const [user, domainType_] = deviceId.split('_')
const domainType = parseInt(domainType_ || '0')
if (domainType === WAJIDDomains.LID || domainType === WAJIDDomains.HOSTED_LID) return id
const pnJid = `${user!}${device !== '0' ? `:${device}` : ''}@${domainType === WAJIDDomains.HOSTED ? 'hosted' : 's.whatsapp.net'}`
const pnJid = `${user ?? ''}${device !== '0' ? `:${device}` : ''}@${domainType === WAJIDDomains.HOSTED ? 'hosted' : 's.whatsapp.net'}`
const lidForPN = await lidMapping.getLIDForPN(pnJid)
if (lidForPN) {
+16 -4
View File
@@ -118,14 +118,18 @@ export const makeBusinessSocket = (config: SocketConfig) => {
content: [
{
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) => {
@@ -332,7 +336,11 @@ export const makeBusinessSocket = (config: SocketConfig) => {
const productCatalogEditNode = getBinaryNodeChild(result, 'product_catalog_edit')
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) => {
@@ -372,7 +380,11 @@ export const makeBusinessSocket = (config: SocketConfig) => {
const productCatalogAddNode = getBinaryNodeChild(result, 'product_catalog_add')
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[]) => {
+37 -19
View File
@@ -272,9 +272,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
for (const section of getBinaryNodeChildren(botNode, 'section')) {
if (section.attrs.type === 'all') {
for (const bot of getBinaryNodeChildren(section, 'bot')) {
const jid = bot.attrs.jid
const personaId = bot.attrs['persona_id']
if (!jid || !personaId) continue
botList.push({
jid: bot.attrs.jid!,
personaId: bot.attrs['persona_id']!
jid,
personaId
})
}
}
@@ -322,7 +325,9 @@ export const makeChatsSocket = (config: SocketConfig) => {
)
}
if (jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me!.id)) {
const me = authState.creds.me
if (!me) throw new Boom('Not authenticated', { statusCode: 401 })
if (jidNormalizedUser(jid) !== jidNormalizedUser(me.id)) {
targetJid = jidNormalizedUser(jid) // in case it is someone other than us
} else {
targetJid = undefined
@@ -356,7 +361,9 @@ export const makeChatsSocket = (config: SocketConfig) => {
)
}
if (jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me!.id)) {
const me = authState.creds.me
if (!me) throw new Boom('Not authenticated', { statusCode: 401 })
if (jidNormalizedUser(jid) !== jidNormalizedUser(me.id)) {
targetJid = jidNormalizedUser(jid) // in case it is someone other than us
} else {
targetJid = undefined
@@ -505,10 +512,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
const newAppStateChunkHandler = (isInitialSync: boolean) => {
return {
onMutation(mutation: ChatMutation) {
const me = authState.creds.me
if (!me) throw new Boom('Not authenticated', { statusCode: 401 })
processSyncAction(
mutation,
ev,
authState.creds.me!,
me,
isInitialSync ? { accountSettings: authState.creds.accountSettings } : undefined,
logger
)
@@ -630,7 +639,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
// if retry attempts overshoot
// or key not found
const isIrrecoverableError =
attemptsMap[name]! >= MAX_SYNC_ATTEMPTS ||
(attemptsMap[name] || 0) >= MAX_SYNC_ATTEMPTS ||
error.output?.statusCode === 404 ||
error.name === 'TypeError'
logger.info(
@@ -652,7 +661,9 @@ export const makeChatsSocket = (config: SocketConfig) => {
const { onMutation } = newAppStateChunkHandler(isInitialSync)
for (const key in globalMutationMap) {
onMutation(globalMutationMap[key]!)
const mutation = globalMutationMap[key]
if (!mutation) continue
onMutation(mutation)
}
}
)
@@ -708,7 +719,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
}
const sendPresenceUpdate = async (type: WAPresence, toJid?: string) => {
const me = authState.creds.me!
const me = authState.creds.me
if (!me) throw new Boom('Not authenticated', { statusCode: 401 })
if (type === 'available' || type === 'unavailable') {
if (!me.name) {
logger.warn('no name present, ignoring presence update request...')
@@ -733,14 +745,17 @@ export const makeChatsSocket = (config: SocketConfig) => {
})
}
} else {
const { server } = jidDecode(toJid)!
if (!toJid) return
const decoded = jidDecode(toJid)
if (!decoded) return
const { server } = decoded
const isLid = server === 'lid'
await sendNode({
tag: 'chatstate',
attrs: {
from: isLid ? me.lid! : me.id,
to: toJid!
from: isLid ? (me.lid || me.id) : me.id,
to: toJid
},
content: [
{
@@ -774,8 +789,9 @@ export const makeChatsSocket = (config: SocketConfig) => {
let presence: PresenceData | undefined
const jid = attrs.from
const participant = attrs.participant || attrs.from
if (!jid) return
if (shouldIgnoreJid(jid!) && jid !== S_WHATSAPP_NET) {
if (shouldIgnoreJid(jid) && jid !== S_WHATSAPP_NET) {
return
}
@@ -786,12 +802,13 @@ export const makeChatsSocket = (config: SocketConfig) => {
}
} else if (Array.isArray(content)) {
const [firstChild] = content
let type = firstChild!.tag as WAPresence
if (!firstChild) return
let type = firstChild.tag as WAPresence
if (type === 'paused') {
type = 'available'
}
if (firstChild!.attrs?.media === 'audio') {
if (firstChild.attrs?.media === 'audio') {
type = 'recording'
}
@@ -801,7 +818,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
}
if (presence) {
ev.emit('presence.update', { id: jid!, presences: { [participant!]: presence } })
if (!participant) return
ev.emit('presence.update', { id: jid, presences: { [participant]: presence } })
}
}
@@ -868,8 +886,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
const { onMutation } = newAppStateChunkHandler(false)
const { mutationMap } = await decodePatches(
name,
[{ ...encodeResult!.patch, version: { version: encodeResult!.state.version } }],
initial!,
[{ ...encodeResult?.patch, version: { version: encodeResult?.state?.version } }],
initial ?? { version: 0, hash: Buffer.alloc(0), indexValueMap: {} },
getAppStateSyncKey,
config.options,
undefined,
@@ -1089,8 +1107,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
ev.emit('messages.upsert', { messages: [msg], type })
if (!!msg.pushName) {
let jid = msg.key.fromMe ? authState.creds.me!.id : msg.key.participant || msg.key.remoteJid
jid = jidNormalizedUser(jid!)
let jid = msg.key.fromMe ? (authState.creds.me?.id ?? '') : msg.key.participant || msg.key.remoteJid
jid = jidNormalizedUser(jid ?? '')
if (!msg.key.fromMe) {
ev.emit('contacts.update', [{ id: jid, notify: msg.pushName, verifiedName: msg.verifiedBizName! }])
+17 -8
View File
@@ -100,7 +100,12 @@ export const makeCommunitiesSocket = (config: SocketConfig) => {
}
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') {
return
}
@@ -353,13 +358,13 @@ export const makeCommunitiesSocket = (config: SocketConfig) => {
communityAcceptInviteV4: ev.createBufferedFunction(
async (key: string | WAMessageKey, inviteMessage: proto.Message.IGroupInviteMessage) => {
key = typeof key === 'string' ? { remoteJid: key } : key
const results = await communityQuery(inviteMessage.groupJid!, 'set', [
const results = await communityQuery(inviteMessage.groupJid ?? '', 'set', [
{
tag: 'accept',
attrs: {
code: inviteMessage.inviteCode!,
expiration: inviteMessage.inviteExpiration!.toString(),
admin: key.remoteJid!
code: inviteMessage.inviteCode ?? '',
expiration: (inviteMessage.inviteExpiration ?? 0).toString(),
admin: key.remoteJid ?? ''
}
}
])
@@ -432,7 +437,11 @@ export const makeCommunitiesSocket = (config: SocketConfig) => {
}
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')
let desc: string | undefined
let descId: string | undefined
@@ -466,12 +475,12 @@ export const extractCommunityMetadata = (result: BinaryNode) => {
participants: getBinaryNodeChildren(community, 'participant').map(({ attrs }) => {
return {
// TODO: IMPLEMENT THE PN/LID FIELDS HERE!!
id: attrs.jid!,
id: attrs.jid ?? '',
admin: (attrs.type || null) as GroupParticipant['admin']
}
}),
ephemeralDuration: eph ? +eph : undefined,
addressingMode: getBinaryNodeChildString(community, 'addressing_mode')! as GroupMetadata['addressingMode']
addressingMode: getBinaryNodeChildString(community, 'addressing_mode') as GroupMetadata['addressingMode']
}
return metadata
}
+9 -9
View File
@@ -225,12 +225,12 @@ export const makeGroupsSocket = (config: SocketConfig) => {
groupAcceptInviteV4: ev.createBufferedFunction(
async (key: string | WAMessageKey, inviteMessage: proto.Message.IGroupInviteMessage) => {
key = typeof key === 'string' ? { remoteJid: key } : key
const results = await groupQuery(inviteMessage.groupJid!, 'set', [
const results = await groupQuery(inviteMessage.groupJid ?? '', 'set', [
{
tag: 'accept',
attrs: {
code: inviteMessage.inviteCode!,
expiration: inviteMessage.inviteExpiration!.toString(),
code: inviteMessage.inviteCode ?? '',
expiration: String(inviteMessage.inviteExpiration ?? ''),
admin: key.remoteJid!
}
}
@@ -316,19 +316,19 @@ export const extractGroupMetadata = (result: BinaryNode) => {
descId = descChild.attrs.id
}
const groupId = group.attrs.id!.includes('@') ? group.attrs.id : jidEncode(group.attrs.id!, 'g.us')
const groupId = (group.attrs.id ?? '').includes('@') ? group.attrs.id : jidEncode(group.attrs.id ?? '', 'g.us')
const eph = getBinaryNodeChild(group, 'ephemeral')?.attrs.expiration
const memberAddMode = getBinaryNodeChildString(group, 'member_add_mode') === 'all_member_add'
const metadata: GroupMetadata = {
id: groupId!,
id: groupId,
notify: group.attrs.notify,
addressingMode: group.attrs.addressing_mode === 'lid' ? WAMessageAddressingMode.LID : WAMessageAddressingMode.PN,
subject: group.attrs.subject!,
subject: group.attrs.subject ?? '',
subjectOwner: group.attrs.s_o,
subjectOwnerPn: group.attrs.s_o_pn,
subjectTime: +group.attrs.s_t!,
subjectTime: +group.attrs.s_t ?? '0',
size: group.attrs.size ? +group.attrs.size : getBinaryNodeChildren(group, 'participant').length,
creation: +group.attrs.creation!,
creation: +group.attrs.creation ?? '0',
owner: group.attrs.creator ? jidNormalizedUser(group.attrs.creator) : undefined,
ownerPn: group.attrs.creator_pn ? jidNormalizedUser(group.attrs.creator_pn) : undefined,
owner_country_code: group.attrs.creator_country_code,
@@ -347,7 +347,7 @@ export const extractGroupMetadata = (result: BinaryNode) => {
participants: getBinaryNodeChildren(group, 'participant').map(({ attrs }) => {
// TODO: Store LID MAPPINGS
return {
id: attrs.jid!,
id: attrs.jid ?? '',
phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
admin: (attrs.type || null) as GroupParticipant['admin']
+73 -53
View File
@@ -156,16 +156,16 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
throw new Boom('Not authenticated')
}
if (await placeholderResendCache.get(messageKey?.id!)) {
if (await placeholderResendCache.get(messageKey?.id)) {
logger.debug({ messageKey }, 'already requested resend')
return
} else {
await placeholderResendCache.set(messageKey?.id!, true)
await placeholderResendCache.set(messageKey?.id, true)
}
await delay(2000)
if (!(await placeholderResendCache.get(messageKey?.id!))) {
if (!(await placeholderResendCache.get(messageKey?.id))) {
logger.debug({ messageKey }, 'message received while resend requested')
return 'RESOLVED'
}
@@ -180,9 +180,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
setTimeout(async () => {
if (await placeholderResendCache.get(messageKey?.id!)) {
if (await placeholderResendCache.get(messageKey?.id)) {
logger.debug({ messageKey }, 'PDO message without response after 8 seconds. Phone possibly offline')
await placeholderResendCache.del(messageKey?.id!)
await placeholderResendCache.del(messageKey?.id)
}
}, 8_000)
@@ -233,7 +233,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
if (update.jid && update.user) {
ev.emit('newsletter-participants.update', {
id: update.jid,
author: node.attrs.from!,
author: node.attrs.from ?? '',
user: update.user,
new_role: 'ADMIN',
action: 'promote'
@@ -251,9 +251,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// Handles newsletter notifications
const handleNewsletterNotification = async (node: BinaryNode) => {
const from = node.attrs.from!
const child = getAllBinaryNodeChildren(node)[0]!
const author = node.attrs.participant!
const from = node.attrs.from ?? ''
const child = getAllBinaryNodeChildren(node)[0]
if (!child) return
const author = node.attrs.participant ?? ''
logger.info({ from, child }, 'got newsletter notification')
@@ -261,7 +262,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
case 'reaction':
const reactionUpdate = {
id: from,
server_id: child.attrs.message_id!,
server_id: child.attrs.message_id ?? '',
reaction: {
code: getBinaryNodeChildString(child, 'reaction'),
count: 1
@@ -273,7 +274,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
case 'view':
const viewUpdate = {
id: from,
server_id: child.attrs.message_id!,
server_id: child.attrs.message_id ?? '',
count: parseInt(child.content?.toString() || '0', 10)
}
ev.emit('newsletter.view', viewUpdate)
@@ -283,9 +284,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const participantUpdate = {
id: from,
author,
user: child.attrs.jid!,
action: child.attrs.action!,
new_role: child.attrs.role!
user: child.attrs.jid ?? '',
action: child.attrs.action ?? '',
new_role: child.attrs.role ?? ''
}
ev.emit('newsletter-participants.update', participantUpdate)
break
@@ -324,7 +325,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
fromMe: false // TODO: is this really true though
},
message: messageProto,
messageTimestamp: +child.attrs.t!
messageTimestamp: +(child.attrs.t ?? 0)
}).toJSON() as WAMessage
await upsertMessage(fullMessage, 'append')
logger.info('Processed plaintext newsletter message')
@@ -345,8 +346,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const stanza: BinaryNode = {
tag: 'ack',
attrs: {
id: attrs.id!,
to: attrs.from!,
id: attrs.id ?? '',
to: attrs.from ?? '',
class: tag
}
}
@@ -371,7 +372,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
if (tag === 'message' && getBinaryNodeChild({ tag, attrs, content }, 'unavailable')) {
stanza.attrs.from = authState.creds.me!.id
const meId = authState.creds.me?.id
if (!meId) throw new Boom('Not authenticated', { statusCode: 401 })
stanza.attrs.from = meId
}
logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, 'sent ack')
@@ -379,10 +382,12 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
const rejectCall = async (callId: string, callFrom: string) => {
const meId = authState.creds.me?.id
if (!meId) throw new Boom('Not authenticated', { statusCode: 401 })
const stanza: BinaryNode = {
tag: 'call',
attrs: {
from: authState.creds.me!.id,
from: meId,
to: callFrom
},
content: [
@@ -401,9 +406,16 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
const sendRetryRequest = async (node: BinaryNode, forceIncludeKeys = false) => {
const { fullMessage } = decodeMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '')
const meId = authState.creds.me?.id
if (!meId) throw new Boom('Not authenticated', { statusCode: 401 })
const meLid = authState.creds.me?.lid || ''
const { fullMessage } = decodeMessageNode(node, meId, meLid)
const { key: msgKey } = fullMessage
const msgId = msgKey.id!
const msgId = msgKey.id
if (!msgId) {
logger.debug({ node }, 'missing message id in retry request')
return
}
if (messageRetryManager) {
// Check if we've exceeded max retries using the new system
@@ -441,7 +453,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const retryCount = (await msgRetryCache.get<number>(key)) || 1
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
const fromJid = node.attrs.from!
const fromJid = node.attrs.from ?? ''
// Check if we should recreate the session
let shouldRecreateSession = false
@@ -498,22 +510,23 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
}
const deviceIdentity = encodeSignedDeviceIdentity(account!, true)
if (!account) throw new Boom('Account not available', { statusCode: 401 })
const deviceIdentity = encodeSignedDeviceIdentity(account, true)
await authState.keys.transaction(async () => {
const receipt: BinaryNode = {
tag: 'receipt',
attrs: {
id: msgId,
type: 'retry',
to: node.attrs.from!
to: node.attrs.from ?? ''
},
content: [
{
tag: 'retry',
attrs: {
count: retryCount.toString(),
id: node.attrs.id!,
t: node.attrs.t!,
id: node.attrs.id ?? '',
t: node.attrs.t ?? '',
v: '1',
// ADD ERROR FIELD
error: '0'
@@ -539,16 +552,19 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const { update, preKeys } = await getNextPreKeys(authState, 1)
const [keyId] = Object.keys(preKeys)
const key = preKeys[+keyId!]
if (!keyId) throw new Boom('No pre-keys available')
const key = preKeys[+keyId]
if (!key) throw new Boom('Pre-key not found')
const content = receipt.content! as BinaryNode[]
const content = receipt.content as BinaryNode[]
if (!content) throw new Boom('Receipt content missing')
content.push({
tag: 'keys',
attrs: {},
content: [
{ tag: 'type', attrs: {}, content: Buffer.from(KEY_BUNDLE_TYPE) },
{ tag: 'identity', attrs: {}, content: identityKey.public },
xmppPreKey(key!, +keyId!),
xmppPreKey(key, +keyId),
xmppSignedPreKey(signedPreKey),
{ tag: 'device-identity', attrs: {}, content: deviceIdentity }
]
@@ -567,7 +583,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const from = node.attrs.from
if (from === S_WHATSAPP_NET) {
const countChild = getBinaryNodeChild(node, 'count')
const count = +countChild!.attrs.value!
const countVal = countChild?.attrs?.value
if (!countVal) return
const count = +countVal
const shouldUploadMorePreKeys = count < MIN_PREKEY_COUNT
logger.debug({ count, shouldUploadMorePreKeys }, 'recv pre-key count')
@@ -596,8 +614,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const actingParticipantLid = fullNode.attrs.participant
const actingParticipantPn = fullNode.attrs.participant_pn
const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid!
const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn!
const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid || ''
const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn || ''
switch (child?.tag) {
case 'create':
@@ -632,7 +650,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
break
case 'modify':
const oldNumber = getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid!)
const oldNumber = getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid ?? '').filter(Boolean)
msg.messageStubParameters = oldNumber || []
msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER
break
@@ -647,7 +665,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const participants = getBinaryNodeChildren(child, 'participant').map(({ attrs }) => {
// TODO: Store LID MAPPINGS
return {
id: attrs.jid!,
id: attrs.jid ?? '',
phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
admin: (attrs.type || null) as GroupParticipant['admin']
@@ -658,8 +676,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
participants.length === 1 &&
// if recv. "remove" message and sender removed themselves
// mark as left
(areJidsSameUser(participants[0]!.id, actingParticipantLid) ||
areJidsSameUser(participants[0]!.id, actingParticipantPn)) &&
(areJidsSameUser(participants[0]?.id, actingParticipantLid) ||
areJidsSameUser(participants[0]?.id, actingParticipantPn)) &&
child.tag === 'remove'
) {
msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE
@@ -669,7 +687,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
break
case 'subject':
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_SUBJECT
msg.messageStubParameters = [child.attrs.subject!]
msg.messageStubParameters = [child.attrs.subject ?? '']
break
case 'description':
const description = getBinaryNodeChild(child, 'body')?.content?.toString()
@@ -688,7 +706,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
break
case 'invite':
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_INVITE_LINK
msg.messageStubParameters = [child.attrs.code!]
msg.messageStubParameters = [child.attrs.code ?? '']
break
case 'member_add_mode':
const addMode = child.content
@@ -702,7 +720,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const approvalMode = getBinaryNodeChild(child, 'group_join')
if (approvalMode) {
msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE
msg.messageStubParameters = [approvalMode.attrs.state!]
msg.messageStubParameters = [approvalMode.attrs.state ?? '']
}
break
@@ -711,7 +729,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
msg.messageStubParameters = [
JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
'created',
child.attrs.request_method!
child.attrs.request_method ?? ''
]
break
case 'revoked_membership_requests':
@@ -741,7 +759,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
break
case 'w:gp2':
// TODO: HANDLE PARTICIPANT_PN
handleGroupNotification(node, child!, result)
if (!child) break
handleGroupNotification(node, child, result)
break
case 'mediaretry':
const event = decodeMediaRetryNode(node)
@@ -751,10 +770,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
await handleEncryptNotification(node)
break
case 'devices':
if (!child) break
const devices = getBinaryNodeChildren(child, 'device')
if (
areJidsSameUser(child!.attrs.jid, authState.creds.me!.id) ||
areJidsSameUser(child!.attrs.lid, authState.creds.me!.lid)
areJidsSameUser(child.attrs.jid, authState.creds.me?.id) ||
areJidsSameUser(child.attrs.lid, authState.creds.me?.lid)
) {
const deviceData = devices.map(d => ({ id: d.attrs.jid, lid: d.attrs.lid }))
logger.info({ deviceData }, 'my own devices changed')
@@ -800,9 +820,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
break
case 'account_sync':
if (child!.tag === 'disappearing_mode') {
const newDuration = +child!.attrs.duration!
const timestamp = +child!.attrs.t!
if (child?.tag === 'disappearing_mode') {
const newDuration = +(child.attrs.duration ?? '0')
const timestamp = +(child.attrs.t ?? '0')
logger.info({ newDuration }, 'updated account disappearing mode')
@@ -815,11 +835,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
}
})
} else if (child!.tag === 'blocklist') {
} else if (child?.tag === 'blocklist') {
const blocklists = getBinaryNodeChildren(child, 'item')
for (const { attrs } of blocklists) {
const blocklist = [attrs.jid!]
const blocklist = [attrs.jid ?? '']
const type = attrs.action === 'block' ? 'add' : 'remove'
ev.emit('blocklist.update', { blocklist, type })
}
@@ -869,7 +889,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
{
tag: 'link_code_companion_reg',
attrs: {
jid: authState.creds.me!.id,
jid: authState.creds.me?.id ?? '',
stage: 'companion_finish'
},
content: [
@@ -1071,7 +1091,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const handleReceipt = async (node: BinaryNode) => {
const { attrs, content } = node
const isLid = attrs.from!.includes('lid')
const isLid = (attrs.from ?? '').includes('lid')
const isNodeFromMe = areJidsSameUser(
attrs.participant || attrs.from,
isLid ? authState.creds.me?.lid : authState.creds.me?.id
@@ -1177,7 +1197,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
notificationMutex.mutex(async () => {
const msg = await processNotification(node)
if (msg) {
const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me!.id)
const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me?.id ?? '')
const { senderAlt: participantAlt, addressingMode } = extractAddressingContext(node)
msg.key = {
remoteJid,
@@ -1221,7 +1241,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
category,
author,
decrypt
} = decryptMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '', signalRepository, logger)
} = decryptMessageNode(node, authState.creds.me?.id ?? '', authState.creds.me?.lid || '', signalRepository, logger)
const alt = msg.key.participantAlt || msg.key.remoteJidAlt
// Handle LID/PN mappings with hybrid approach:
@@ -1506,7 +1526,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// JID normalization moved BEFORE mutex acquisition (line 1273) to prevent race conditions
// cleanMessage still runs inside mutex to ensure atomic message processing
cleanMessage(msg, authState.creds.me!.id, authState.creds.me!.lid!)
cleanMessage(msg, authState.creds.me?.id ?? '', authState.creds.me?.lid ?? '')
await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify')
+58 -36
View File
@@ -126,15 +126,16 @@ export const makeMessagesSocket = (config: SocketConfig) => {
},
content: [{ tag: 'media_conn', attrs: {} }]
})
const mediaConnNode = getBinaryNodeChild(result, 'media_conn')!
const mediaConnNode = getBinaryNodeChild(result, 'media_conn')
if (!mediaConnNode) throw new Boom('Missing media_conn node')
// TODO: explore full length of data that whatsapp provides
const node: MediaConnInfo = {
hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(({ attrs }) => ({
hostname: attrs.hostname!,
maxContentLengthBytes: +attrs.maxContentLengthBytes!
hostname: attrs.hostname ?? '',
maxContentLengthBytes: +(attrs.maxContentLengthBytes ?? 0)
})),
auth: mediaConnNode.attrs.auth!,
ttl: +mediaConnNode.attrs.ttl!,
auth: mediaConnNode.attrs.auth ?? '',
ttl: +(mediaConnNode.attrs.ttl ?? 0),
fetchDate: new Date()
}
logger.debug('fetched media conn')
@@ -162,7 +163,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const node: BinaryNode = {
tag: 'receipt',
attrs: {
id: messageIds[0]!
id: messageIds[0] ?? ''
}
}
const isReadReceipt = type === 'read' || type === 'read-self'
@@ -171,8 +172,9 @@ export const makeMessagesSocket = (config: SocketConfig) => {
}
if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) {
if (!participant) throw new Boom('Missing participant for sender receipt')
node.attrs.recipient = jid
node.attrs.to = participant!
node.attrs.to = participant
} else {
node.attrs.to = jid
if (participant) {
@@ -266,10 +268,11 @@ export const makeMessagesSocket = (config: SocketConfig) => {
}
for (const { jid, user } of jidsWithUser) {
if (!user) continue
if (useCache) {
const devices =
mgetDevices?.[user!] ||
(userDevicesCache.mget ? undefined : ((await userDevicesCache.get(user!)) as FullJid[]))
mgetDevices?.[user] ||
(userDevicesCache.mget ? undefined : ((await userDevicesCache.get(user)) as FullJid[]))
if (devices) {
const devicesWithJid = devices.map(d => ({
...d,
@@ -324,10 +327,14 @@ export const makeMessagesSocket = (config: SocketConfig) => {
}
}
const meId = authState.creds.me?.id
if (!meId) throw new Boom('Not authenticated', { statusCode: 401 })
const meLid = authState.creds.me?.lid || ''
const extracted = extractDeviceJids(
result?.list,
authState.creds.me!.id,
authState.creds.me!.lid!,
meId,
meLid,
ignoreZeroDevices
)
const deviceMap: { [_: string]: FullJid[] } = {}
@@ -547,7 +554,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
: recipientJids.map(jid => ({ recipientJid: jid, message: patched }))
let shouldIncludeDeviceIdentity = false
const meId = authState.creds.me!.id
const meId = authState.creds.me?.id
if (!meId) throw new Boom('Not authenticated', { statusCode: 401 })
const meLid = authState.creds.me?.lid
const meLidUser = meLid ? jidDecode(meLid)?.user : null
@@ -559,8 +567,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
let msgToEncrypt = patchedMessage
if (dsmMessage) {
const { user: targetUser } = jidDecode(jid)!
const { user: ownPnUser } = jidDecode(meId)!
const targetUser = jidDecode(jid)?.user ?? ''
const ownPnUser = jidDecode(meId)?.user ?? ''
const ownLidUser = meLidUser
const isOwnUser = targetUser === ownPnUser || (ownLidUser && targetUser === ownLidUser)
@@ -816,13 +824,16 @@ export const makeMessagesSocket = (config: SocketConfig) => {
statusJidList
}: MessageRelayOptions
) => {
const meId = authState.creds.me!.id
const meId = authState.creds.me?.id
if (!meId) throw new Boom('Not authenticated', { statusCode: 401 })
const meLid = authState.creds.me?.lid
const isRetryResend = Boolean(participant?.jid)
let shouldIncludeDeviceIdentity = isRetryResend
const statusJid = 'status@broadcast'
const { user, server } = jidDecode(jid)!
const jidDecoded = jidDecode(jid)
if (!jidDecoded) throw new Boom('Invalid JID')
const { user, server } = jidDecoded
const isGroup = server === 'g.us'
const isStatus = jid === statusJid
const isLid = server === 'lid'
@@ -910,7 +921,9 @@ export const makeMessagesSocket = (config: SocketConfig) => {
additionalAttributes = { ...additionalAttributes, device_fanout: 'false' }
}
const { user, device } = jidDecode(participant.jid)!
const participantDecoded = jidDecode(participant.jid)
if (!participantDecoded) throw new Boom('Invalid participant JID')
const { user, device } = participantDecoded
devices.push({
user,
device,
@@ -1068,7 +1081,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
logger.debug({ to: jid, ownId }, 'Using PN identity for @s.whatsapp.net conversation')
}
const { user: ownUser } = jidDecode(ownId)!
const ownUser = jidDecode(ownId)?.user ?? ''
if (!participant) {
const patchedForReporting = await patchMessageBeforeSending(message, [jid])
reportingMessage = Array.isArray(patchedForReporting)
@@ -1086,7 +1099,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
if (user !== ownUser) {
const ownUserServer = isLid ? 'lid' : 's.whatsapp.net'
const ownUserForAddressing = isLid && meLid ? jidDecode(meLid)!.user : jidDecode(meId)!.user
const ownUserForAddressing = isLid && meLid ? (jidDecode(meLid)?.user ?? '') : (jidDecode(meId)?.user ?? '')
devices.push({
user: ownUserForAddressing,
@@ -1102,8 +1115,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
// Use conversation-appropriate sender identity
const senderIdentity =
isLid && meLid
? jidEncode(jidDecode(meLid)?.user!, 'lid', undefined)
: jidEncode(jidDecode(meId)?.user!, 's.whatsapp.net', undefined)
? jidEncode(jidDecode(meLid)?.user ?? '', 'lid', undefined)
: jidEncode(jidDecode(meId)?.user ?? '', 's.whatsapp.net', undefined)
// Enumerate devices for sender and target with consistent addressing
const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
@@ -1122,8 +1135,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const allRecipients: string[] = []
const meRecipients: string[] = []
const otherRecipients: string[] = []
const { user: mePnUser } = jidDecode(meId)!
const { user: meLidUser } = meLid ? jidDecode(meLid)! : { user: null }
const mePnUser = jidDecode(meId)?.user ?? ''
const meLidUser = meLid ? (jidDecode(meLid)?.user ?? null) : null
// Carousel in DSM wrapper causes error 479 on own devices
const isCarouselMsg = isCarouselMessage(message)
@@ -1172,10 +1185,11 @@ export const makeMessagesSocket = (config: SocketConfig) => {
}
if (isRetryResend) {
if (!participant) throw new Boom('Missing participant for retry resend')
// Only check for regular LID users, NOT hosted LID users
// Hosted LID users should use meId for comparison, not meLid
const isParticipantLid = isLidUser(participant!.jid)
const isMe = areJidsSameUser(participant!.jid, isParticipantLid ? meLid : meId)
const isParticipantLid = isLidUser(participant.jid)
const isMe = areJidsSameUser(participant.jid, isParticipantLid ? meLid : meId)
// Skip DSM for carousel - own devices reject it with error 479
const usesDSM = isMe && !isCarouselMessage(message)
@@ -1190,7 +1204,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({
data: encodedMessageToSend,
jid: participant!.jid
jid: participant.jid
})
binaryNodeContent.push({
@@ -1198,7 +1212,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
attrs: {
v: '2',
type,
count: participant!.count.toString()
count: (participant?.count ?? 0).toString()
},
content: encryptedContent
})
@@ -1617,8 +1631,12 @@ export const makeMessagesSocket = (config: SocketConfig) => {
updateMemberLabel,
updateMediaMessage: async (message: WAMessage) => {
const content = assertMediaContent(message.message)
const mediaKey = content.mediaKey!
const meId = authState.creds.me!.id
const mediaKey = content.mediaKey
if (!mediaKey) {
throw new Boom('Missing media key for update', { statusCode: 400 })
}
const meId = authState.creds.me?.id ?? ''
const node = encryptMediaRetryRequest(message.key, mediaKey, meId)
let error: Error | undefined = undefined
@@ -1695,7 +1713,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
options: MiscMessageGenerationOptions = {}
): Promise<AlbumSendResult> => {
const startTime = Date.now()
const userJid = authState.creds.me!.id
const userJid = authState.creds.me?.id ?? ''
const {
medias,
@@ -1836,17 +1854,21 @@ export const makeMessagesSocket = (config: SocketConfig) => {
// Attach to parent album via messageAssociation (correct proto structure)
// Uses AssociationType.MEDIA_ALBUM and parentMessageKey as per WhatsApp protocol
if (!mediaMsg.message!.messageContextInfo) {
mediaMsg.message!.messageContextInfo = {}
if (!mediaMsg.message) {
throw new Boom('Missing message content for album media item')
}
mediaMsg.message!.messageContextInfo.messageAssociation = {
if (!mediaMsg.message.messageContextInfo) {
mediaMsg.message.messageContextInfo = {}
}
mediaMsg.message.messageContextInfo.messageAssociation = {
associationType: proto.MessageAssociation.AssociationType.MEDIA_ALBUM,
parentMessageKey: albumKey
}
// Relay the message
await relayMessage(jid, mediaMsg.message!, {
messageId: mediaMsg.key.id!,
await relayMessage(jid, mediaMsg.message, {
messageId: mediaMsg.key.id ?? '',
useCachedGroupMetadata: options.useCachedGroupMetadata
})
@@ -1975,7 +1997,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
)
}
const userJid = authState.creds.me!.id
const userJid = authState.creds.me?.id ?? ''
// Special path for carousel: call relayMessage directly with plain JS object
// This matches Pastorini's working approach:
+38 -10
View File
@@ -552,7 +552,11 @@ export const makeSocket = (config: SocketConfig) => {
})
if (sendMsg) {
sendRawMessage(sendMsg).catch(onClose!)
if (!onClose) {
throw new Error('onClose handler not initialized')
}
sendRawMessage(sendMsg).catch(onClose)
}
return result
@@ -609,8 +613,12 @@ export const makeSocket = (config: SocketConfig) => {
},
content: [{ tag: 'count', attrs: {} }]
})
const countChild = getBinaryNodeChild(result, 'count')!
return +countChild.attrs.value!
const countChild = getBinaryNodeChild(result, 'count')
if (!countChild) {
return 0
}
return +(countChild.attrs.value ?? 0)
}
// Pre-key upload state management
@@ -885,7 +893,11 @@ export const makeSocket = (config: SocketConfig) => {
return
}
const duration = Date.now() - sessionStartTime!
if (!sessionStartTime) {
return
}
const duration = Date.now() - sessionStartTime
const durationHours = Math.floor(duration / 1000 / 60 / 60)
logger.info(`🕐 Session TTL reached after ${durationHours}h, initiating graceful cleanup`)
@@ -1305,7 +1317,11 @@ export const makeSocket = (config: SocketConfig) => {
async function generatePairingKey() {
const salt = randomBytes(32)
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)
return Buffer.concat([salt, randomIv, ciphered])
}
@@ -1352,7 +1368,7 @@ export const makeSocket = (config: SocketConfig) => {
attrs: {
to: S_WHATSAPP_NET,
type: 'result',
id: stanza.attrs.id!
id: stanza.attrs.id ?? ''
}
}
await sendNode(iq)
@@ -1431,7 +1447,9 @@ export const makeSocket = (config: SocketConfig) => {
logger.info('opened connection to WA')
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' })
@@ -1454,13 +1472,23 @@ export const makeSocket = (config: SocketConfig) => {
const myLID = node.attrs.lid
process.nextTick(async () => {
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
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }])
// 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({
'device-list': {
[user]: [device?.toString() || '0']
@@ -1548,7 +1576,7 @@ export const makeSocket = (config: SocketConfig) => {
logger.debug({ name }, 'updated pushName')
sendNode({
tag: 'presence',
attrs: { name: name! }
attrs: { name: name ?? '' }
}).catch(err => {
logger.warn({ trace: err.stack }, 'error in sending presence update on name change')
})
+164 -36
View File
@@ -154,7 +154,12 @@ export const encodeSyncdPatch = async (
})
const encoded = proto.SyncActionData.encode(dataProto).finish()
const keyValue = mutationKeys(key.keyData!)
const keyData = key.keyData
if (!keyData) {
throw new Boom('Missing keyData for encoding', { statusCode: 500 })
}
const keyValue = mutationKeys(keyData)
const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey)
const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey)
@@ -210,15 +215,34 @@ export const decodeSyncdMutations = async (
// if it's a syncdmutation, get the operation property
// otherwise, if it's only a record -- it'll be a SET mutation
const operation = 'operation' in msgMutation ? msgMutation.operation : proto.SyncdMutation.SyncdOperation.SET
if (operation == null) {
throw new Boom('Missing operation in mutation', { statusCode: 500 })
}
const record =
'record' in msgMutation && !!msgMutation.record ? msgMutation.record : (msgMutation as proto.ISyncdRecord)
const key = await getKey(record.keyId!.id!)
const content = Buffer.from(record.value!.blob!)
const keyIdBuf = record.keyId?.id
if (!keyIdBuf) {
throw new Boom('Missing keyId in record', { statusCode: 500 })
}
const recordBlob = record.value?.blob
if (!recordBlob) {
throw new Boom('Missing record blob', { statusCode: 500 })
}
const indexBlob = record.index?.blob
if (!indexBlob) {
throw new Boom('Missing index blob in record', { statusCode: 500 })
}
const key = await getKey(keyIdBuf)
const content = Buffer.from(recordBlob)
const encContent = content.slice(0, -32)
const ogValueMac = content.slice(-32)
if (validateMacs) {
const contentHmac = generateMac(operation!, encContent, record.keyId!.id!, key.valueMacKey)
const contentHmac = generateMac(operation, encContent, keyIdBuf, key.valueMacKey)
if (Buffer.compare(contentHmac, ogValueMac) !== 0) {
throw new Boom('HMAC content verification failed')
}
@@ -227,20 +251,25 @@ export const decodeSyncdMutations = async (
const result = aesDecrypt(encContent, key.valueEncryptionKey)
const syncAction = proto.SyncActionData.decode(result)
const syncActionIndex = syncAction.index
if (!syncActionIndex) {
throw new Boom('Missing index in sync action data', { statusCode: 500 })
}
if (validateMacs) {
const hmac = hmacSign(syncAction.index!, key.indexKey)
if (Buffer.compare(hmac, record.index!.blob!) !== 0) {
const hmac = hmacSign(syncActionIndex, key.indexKey)
if (Buffer.compare(hmac, indexBlob) !== 0) {
throw new Boom('HMAC index verification failed')
}
}
const indexStr = Buffer.from(syncAction.index!).toString()
const indexStr = Buffer.from(syncActionIndex).toString()
onMutation({ syncAction, index: JSON.parse(indexStr) })
ltGenerator.mix({
indexMac: record.index!.blob!,
indexMac: indexBlob,
valueMac: ogValueMac,
operation: operation!
operation: operation
})
}
@@ -256,7 +285,12 @@ export const decodeSyncdMutations = async (
})
}
return mutationKeys(keyEnc.keyData!)
const keyData = keyEnc.keyData
if (!keyData) {
throw new Boom('Missing keyData in app state sync key', { statusCode: 500 })
}
return mutationKeys(keyData)
}
}
@@ -269,28 +303,71 @@ export const decodeSyncdPatch = async (
validateMacs: boolean
) => {
if (validateMacs) {
const base64Key = Buffer.from(msg.keyId!.id!).toString('base64')
const msgKeyId = msg.keyId?.id
if (!msgKeyId) {
throw new Boom('Missing keyId in patch message', { statusCode: 500 })
}
const base64Key = Buffer.from(msgKeyId).toString('base64')
const mainKeyObj = await getAppStateSyncKey(base64Key)
if (!mainKeyObj) {
throw new Boom(`failed to find key "${base64Key}" to decode patch`, { statusCode: 404, data: { msg } })
}
const mainKey = mutationKeys(mainKeyObj.keyData!)
const mutationmacs = msg.mutations!.map(mutation => mutation.record!.value!.blob!.slice(-32))
const mainKeyData = mainKeyObj.keyData
if (!mainKeyData) {
throw new Boom('Missing keyData in main key object', { statusCode: 500 })
}
const mainKey = mutationKeys(mainKeyData)
const msgMutations = msg.mutations
if (!msgMutations) {
throw new Boom('Missing mutations in patch message', { statusCode: 500 })
}
const mutationmacs = msgMutations.map(mutation => {
const mutBlob = mutation.record?.value?.blob
if (!mutBlob) {
throw new Boom('Missing blob in mutation record', { statusCode: 500 })
}
return mutBlob.slice(-32)
})
const msgSnapshotMac = msg.snapshotMac
if (!msgSnapshotMac) {
throw new Boom('Missing snapshotMac in patch message', { statusCode: 500 })
}
const msgVersion = msg.version?.version
if (msgVersion == null) {
throw new Boom('Missing version in patch message', { statusCode: 500 })
}
const msgPatchMac = msg.patchMac
if (!msgPatchMac) {
throw new Boom('Missing patchMac in patch message', { statusCode: 500 })
}
const patchMac = generatePatchMac(
msg.snapshotMac!,
msgSnapshotMac,
mutationmacs,
toNumber(msg.version!.version),
toNumber(msgVersion),
name,
mainKey.patchMacKey
)
if (Buffer.compare(patchMac, msg.patchMac!) !== 0) {
if (Buffer.compare(patchMac, msgPatchMac) !== 0) {
throw new Boom('Invalid patch mac')
}
}
const result = await decodeSyncdMutations(msg.mutations!, initialState, getAppStateSyncKey, onMutation, validateMacs)
const patchMutations = msg.mutations
if (!patchMutations) {
throw new Boom('Missing mutations in patch message', { statusCode: 500 })
}
const result = await decodeSyncdMutations(patchMutations, initialState, getAppStateSyncKey, onMutation, validateMacs)
return result
}
@@ -332,7 +409,7 @@ export const extractSyncdPatches = async (result: BinaryNode, options: RequestIn
const syncd = proto.SyncdPatch.decode(content as Uint8Array)
if (!syncd.version) {
syncd.version = { version: +collectionNode.attrs.version! + 1 }
syncd.version = { version: +(collectionNode.attrs.version ?? 0) + 1 }
}
syncds.push(syncd)
@@ -370,19 +447,30 @@ export const decodeSyncdSnapshot = async (
validateMacs = true
) => {
const newState = newLTHashState()
newState.version = toNumber(snapshot.version!.version)
const snapshotVersion = snapshot.version?.version
if (snapshotVersion == null) {
throw new Boom('Missing version in snapshot', { statusCode: 500 })
}
newState.version = toNumber(snapshotVersion)
const mutationMap: ChatMutationMap = {}
const areMutationsRequired = typeof minimumVersionNumber === 'undefined' || newState.version > minimumVersionNumber
const snapshotRecords = snapshot.records
if (!snapshotRecords) {
throw new Boom('Missing records in snapshot', { statusCode: 500 })
}
const { hash, indexValueMap } = await decodeSyncdMutations(
snapshot.records!,
snapshotRecords,
newState,
getAppStateSyncKey,
areMutationsRequired
? mutation => {
const index = mutation.syncAction.index?.toString()
mutationMap[index!] = mutation
mutationMap[index ?? ''] = mutation
}
: () => {},
validateMacs
@@ -391,15 +479,31 @@ export const decodeSyncdSnapshot = async (
newState.indexValueMap = indexValueMap
if (validateMacs) {
const base64Key = Buffer.from(snapshot.keyId!.id!).toString('base64')
const snapKeyId = snapshot.keyId?.id
if (!snapKeyId) {
throw new Boom('Missing keyId in snapshot', { statusCode: 500 })
}
const base64Key = Buffer.from(snapKeyId).toString('base64')
const keyEnc = await getAppStateSyncKey(base64Key)
if (!keyEnc) {
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
}
const result = mutationKeys(keyEnc.keyData!)
const snapKeyData = keyEnc.keyData
if (!snapKeyData) {
throw new Boom('Missing keyData in snapshot key', { statusCode: 500 })
}
const result = mutationKeys(snapKeyData)
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
if (Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) {
const snapshotMac = snapshot.mac
if (!snapshotMac) {
throw new Boom('Missing mac in snapshot', { statusCode: 500 })
}
if (Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`)
}
}
@@ -436,7 +540,12 @@ export const decodePatches = async (
syncd.mutations?.push(...ref.mutations)
}
const patchVersion = toNumber(version!.version)
const ver = version?.version
if (ver == null) {
throw new Boom('Missing version in patch', { statusCode: 500 })
}
const patchVersion = toNumber(ver)
newState.version = patchVersion
const shouldMutate = typeof minimumVersionNumber === 'undefined' || patchVersion > minimumVersionNumber
@@ -449,7 +558,7 @@ export const decodePatches = async (
shouldMutate
? mutation => {
const index = mutation.syncAction.index?.toString()
mutationMap[index!] = mutation
mutationMap[index ?? ''] = mutation
}
: () => {},
true
@@ -459,15 +568,30 @@ export const decodePatches = async (
newState.indexValueMap = decodeResult.indexValueMap
if (validateMacs) {
const base64Key = Buffer.from(keyId!.id!).toString('base64')
const patchKeyId = keyId?.id
if (!patchKeyId) {
throw new Boom('Missing keyId in patch', { statusCode: 500 })
}
const base64Key = Buffer.from(patchKeyId).toString('base64')
const keyEnc = await getAppStateSyncKey(base64Key)
if (!keyEnc) {
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
}
const result = mutationKeys(keyEnc.keyData!)
const patchKeyData = keyEnc.keyData
if (!patchKeyData) {
throw new Boom('Missing keyData in patch key', { statusCode: 500 })
}
const result = mutationKeys(patchKeyData)
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
if (Buffer.compare(snapshotMac!, computedSnapshotMac) !== 0) {
if (!snapshotMac) {
throw new Boom('Missing snapshotMac in patch', { statusCode: 500 })
}
if (Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`)
}
}
@@ -565,7 +689,7 @@ export const chatModificationToAppPatch = (mod: ChatModification, jid: string) =
messageTimestamp: timestamp
}
},
index: ['deleteMessageForMe', jid, key.id!, key.fromMe ? '1' : '0', '0'],
index: ['deleteMessageForMe', jid, key.id ?? '', key.fromMe ? '1' : '0', '0'],
type: 'regular_high',
apiVersion: 3,
operation: OP.SET
@@ -615,7 +739,11 @@ export const chatModificationToAppPatch = (mod: ChatModification, jid: string) =
operation: OP.SET
}
} else if ('star' in mod) {
const key = mod.star.messages[0]!
const key = mod.star.messages[0]
if (!key) {
throw new Boom('Missing star message key', { statusCode: 400 })
}
patch = {
syncAction: {
starAction: {
@@ -768,7 +896,7 @@ export const processSyncAction = (
{
id,
muteEndTime: action.muteAction?.muted ? toNumber(action.muteAction.muteEndTimestamp) : null,
conditional: getChatUpdateConditional(id!, undefined)
conditional: getChatUpdateConditional(id ?? '', undefined)
}
])
} else if (action?.archiveChatAction || type === 'archive' || type === 'unarchive') {
@@ -797,7 +925,7 @@ export const processSyncAction = (
{
id,
archived: isArchived,
conditional: getChatUpdateConditional(id!, msgRange)
conditional: getChatUpdateConditional(id ?? '', msgRange)
}
])
} else if (action?.markChatAsReadAction) {
@@ -811,7 +939,7 @@ export const processSyncAction = (
{
id,
unreadCount: isNullUpdate ? null : !!markReadAction?.read ? 0 : -1,
conditional: getChatUpdateConditional(id!, markReadAction?.messageRange)
conditional: getChatUpdateConditional(id ?? '', markReadAction?.messageRange)
}
])
} else if (action?.deleteMessageForMeAction || type === 'deleteMessageForMe') {
@@ -837,7 +965,7 @@ export const processSyncAction = (
{
id,
pinned: action.pinAction?.pinned ? toNumber(action.timestamp) : null,
conditional: getChatUpdateConditional(id!, undefined)
conditional: getChatUpdateConditional(id ?? '', undefined)
}
])
} else if (action?.unarchiveChatsSetting) {
@@ -862,7 +990,7 @@ export const processSyncAction = (
])
} else if (action?.deleteChatAction || type === 'deleteChat') {
if (!isInitialSync) {
ev.emit('chats.delete', [id!])
ev.emit('chats.delete', [id ?? ''])
}
} else if (action?.labelEditAction) {
const { name, color, deleted, predefinedId } = action.labelEditAction
+19 -13
View File
@@ -127,6 +127,10 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin
const msgId = stanza.attrs.id
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 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 (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 })
}
if (isMe(from!) || isMeLid(from!)) {
if (isMe(from) || isMeLid(from)) {
fromMe = true
}
chatId = recipient
} else {
chatId = from!
chatId = from
}
msgType = 'chat'
author = from!
author = from
} else if (isJidGroup(from)) {
if (!participant) {
throw new Boom('No participant in group message')
@@ -163,28 +167,28 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin
msgType = 'group'
author = participant
chatId = from!
chatId = from
} else if (isJidBroadcast(from)) {
if (!participant) {
throw new Boom('No participant in group message')
}
const isParticipantMe = isMe(participant)
if (isJidStatusBroadcast(from!)) {
if (isJidStatusBroadcast(from)) {
msgType = isParticipantMe ? 'direct_peer_status' : 'other_status'
} else {
msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast'
}
fromMe = isParticipantMe
chatId = from!
chatId = from
author = participant
} else if (isJidNewsletter(from)) {
msgType = 'newsletter'
chatId = from!
author = from!
chatId = from
author = from
if (isMe(from!) || isMeLid(from!)) {
if (isMe(from) || isMeLid(from)) {
fromMe = true
}
} else {
@@ -207,7 +211,7 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin
const fullMessage: WAMessage = {
key,
category: stanza.attrs.category,
messageTimestamp: +stanza.attrs.t!,
messageTimestamp: +(stanza.attrs.t ?? 0),
pushName: pushname,
broadcast: isJidBroadcast(from)
}
@@ -241,8 +245,10 @@ export const decryptMessageNode = (
for (const { tag, attrs, content } of stanza.content) {
if (tag === 'verified_name' && content instanceof Uint8Array) {
const cert = proto.VerifiedNameCertificate.decode(content)
const details = proto.VerifiedNameCertificate.Details.decode(cert.details!)
fullMessage.verifiedBizName = details.verifiedName
if (cert.details) {
const details = proto.VerifiedNameCertificate.Details.decode(cert.details)
fullMessage.verifiedBizName = details.verifiedName
}
}
if (tag === 'unavailable' && attrs.type === 'view_once') {
+27 -17
View File
@@ -289,8 +289,9 @@ class AdaptiveTimeoutCalculator {
return // Not enough data
}
const oldest = this.eventTimestamps[0]!
const newest = this.eventTimestamps[this.eventTimestamps.length - 1]!
const oldest = this.eventTimestamps[0]
const newest = this.eventTimestamps[this.eventTimestamps.length - 1]
if (oldest === undefined || newest === undefined) return
const timeSpan = newest - oldest
if (timeSpan === 0) return
@@ -337,8 +338,9 @@ class AdaptiveTimeoutCalculator {
if (this.eventTimestamps.length < 2) {
return 0
}
const oldest = this.eventTimestamps[0]!
const newest = this.eventTimestamps[this.eventTimestamps.length - 1]!
const oldest = this.eventTimestamps[0]
const newest = this.eventTimestamps[this.eventTimestamps.length - 1]
if (oldest === undefined || newest === undefined) return 0
const timeSpan = newest - oldest
if (timeSpan === 0) return 0
return (this.eventTimestamps.length / timeSpan) * 1000
@@ -633,8 +635,11 @@ export const makeEventBuffer = (
for (const update of chatUpdates) {
if (update.conditional) {
conditionalChatUpdatesLeft += 1
newData.chatUpdates[update.id!] = update
delete data.chatUpdates[update.id!]
const updateId = 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 existingUpserts = Object.values(data.messageUpserts)
if (existingUpserts.length > 0) {
const bufferedType = existingUpserts[0]!.type
const bufferedType = existingUpserts[0]?.type
if (bufferedType !== type) {
logger.debug({ bufferedType, newType: type }, 'messages.upsert type mismatch, emitting buffered messages')
// Emit the buffered messages with their correct type
@@ -973,7 +978,8 @@ function append<E extends BufferableEvent>(
break
case 'chats.update':
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
if (conditionMatches) {
delete update.conditional
@@ -1039,8 +1045,9 @@ function append<E extends BufferableEvent>(
data.contactUpserts[contact.id] = upsert
}
if (data.contactUpdates[contact.id]) {
upsert = Object.assign(data.contactUpdates[contact.id]!, trimUndefined(contact)) as Contact
const existingContactUpdate = data.contactUpdates[contact.id]
if (existingContactUpdate) {
upsert = Object.assign(existingContactUpdate, trimUndefined(contact)) as Contact
delete data.contactUpdates[contact.id]
}
}
@@ -1049,7 +1056,8 @@ function append<E extends BufferableEvent>(
case 'contacts.update':
const contactUpdates = eventData as BaileysEventMap['contacts.update']
for (const update of contactUpdates) {
const id = update.id!
const id = update.id
if (!id) continue
// merge into prior upsert
const upsert = data.historySets.contacts[id] || data.contactUpserts[id]
if (upsert) {
@@ -1170,7 +1178,8 @@ function append<E extends BufferableEvent>(
case 'groups.update':
const groupUpdates = eventData as BaileysEventMap['groups.update']
for (const update of groupUpdates) {
const id = update.id!
const id = update.id
if (!id) continue
const groupUpdate = data.groupUpdates[id] || {}
if (!data.groupUpdates[id]) {
data.groupUpdates[id] = Object.assign(groupUpdate, update)
@@ -1202,7 +1211,8 @@ function append<E extends BufferableEvent>(
function decrementChatReadCounterIfMsgDidUnread(message: WAMessage) {
// decrement chat unread counter
// 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]
if (
isRealMessage(message) &&
@@ -1255,7 +1265,7 @@ function consolidateEvents(data: BufferedEventData) {
const messageUpsertList = Object.values(data.messageUpserts)
if (messageUpsertList.length) {
const type = messageUpsertList[0]!.type
const type = messageUpsertList[0]?.type
map['messages.upsert'] = {
messages: messageUpsertList.map(m => m.message),
type
@@ -1311,7 +1321,7 @@ function consolidateEvents(data: BufferedEventData) {
function concatChats<C extends Partial<Chat>>(a: C, b: Partial<Chat>) {
if (
b.unreadCount === null && // neutralize unread counter
a.unreadCount! < 0
(a.unreadCount ?? 0) < 0
) {
a.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') {
b = { ...b }
if (b.unreadCount! >= 0) {
b.unreadCount = Math.max(b.unreadCount!, 0) + Math.max(a.unreadCount, 0)
if ((b.unreadCount ?? 0) >= 0) {
b.unreadCount = Math.max(b.unreadCount ?? 0, 0) + Math.max(a.unreadCount, 0)
}
}
+10 -6
View File
@@ -56,7 +56,7 @@ export const isStringNullOrEmpty = (value: string | null | undefined): value is
export const writeRandomPadMax16 = (msg: Uint8Array) => {
const pad = randomBytes(1)
const padLength = (pad[0]! & 0x0f) + 1
const padLength = ((pad[0] ?? 0) & 0x0f) + 1
return Buffer.concat([msg, Buffer.alloc(padLength, padLength)])
}
@@ -67,7 +67,7 @@ export const unpadRandomMax16 = (e: Uint8Array | Buffer) => {
throw new Error('unpadPkcs7 given empty bytes')
}
var r = t[t.length - 1]!
var r = t[t.length - 1] ?? 0
if (r > t.length) {
throw new Error(`unpad given ${t.length} bytes, but pad is ${r}`)
}
@@ -85,7 +85,7 @@ export const generateParticipantHashV2 = (participants: string[]): string => {
export const encodeWAMessage = (message: proto.IMessage) => writeRandomPadMax16(proto.Message.encode(message).finish())
export const generateRegistrationId = (): number => {
return Uint16Array.from(randomBytes(2))[0]! & 16383
return (Uint16Array.from(randomBytes(2))[0] ?? 0) & 16383
}
export const encodeBigEndian = (e: number, t = 4) => {
@@ -246,10 +246,14 @@ export const fetchLatestBaileysVersion = async (options: RequestInit = {}) => {
// Extract version from line 7 (const version = [...])
const lines = text.split('\n')
const versionLine = lines[6] // Line 7 (0-indexed)
const versionMatch = versionLine!.match(/const version = \[(\d+),\s*(\d+),\s*(\d+)\]/)
if (!versionLine) {
throw new Error('Version line not found')
}
const versionMatch = versionLine.match(/const version = \[(\d+),\s*(\d+),\s*(\d+)\]/)
if (versionMatch) {
const version = [parseInt(versionMatch[1]!), parseInt(versionMatch[2]!), parseInt(versionMatch[3]!)] as WAVersion
const version = [parseInt(versionMatch[1] ?? '0'), parseInt(versionMatch[2] ?? '0'), parseInt(versionMatch[3] ?? '0')] as WAVersion
return {
version,
@@ -339,7 +343,7 @@ const STATUS_MAP: { [_: string]: proto.WebMessageInfo.Status } = {
* @param type type from receipt
*/
export const getStatusFromReceiptType = (type: string | undefined) => {
const status = STATUS_MAP[type!]
const status = STATUS_MAP[type ?? '']
if (typeof type === 'undefined') {
return proto.WebMessageInfo.Status.DELIVERY_ACK
}
+8 -7
View File
@@ -293,8 +293,8 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
case proto.HistorySync.HistorySyncType.RECENT:
case proto.HistorySync.HistorySyncType.FULL:
case proto.HistorySync.HistorySyncType.ON_DEMAND:
for (const chat of item.conversations! as Chat[]) {
const chatId = chat.id!
for (const chat of (item.conversations ?? []) as Chat[]) {
const chatId = chat.id ?? ''
// Source 2: Extract LID-PN mapping from conversation object
// This handles cases where the mapping isn't in phoneNumberToLidMappings
@@ -328,7 +328,8 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
delete chat.messages
for (const item of msgs) {
const message = item.message! as WAMessage
if (!item.message) continue
const message = item.message as WAMessage
messages.push(message)
// Source 3: Extract LID-PN mapping from message's alternative JID fields
@@ -358,7 +359,7 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
message.messageStubParameters?.[0]
) {
contacts.push({
id: message.key.participant || message.key.remoteJid!,
id: message.key.participant || message.key.remoteJid || '',
verifiedName: message.messageStubParameters?.[0]
})
}
@@ -369,8 +370,8 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
break
case proto.HistorySync.HistorySyncType.PUSH_NAME:
for (const c of item.pushnames!) {
contacts.push({ id: c.id!, notify: c.pushname! })
for (const c of (item.pushnames ?? [])) {
contacts.push({ id: c.id ?? '', notify: c.pushname ?? '' })
}
break
@@ -424,7 +425,7 @@ export const downloadAndProcessHistorySyncNotification = async (
*/
export const getHistoryMsg = (message: proto.IMessage) => {
const normalizedContent = !!message ? normalizeMessageContent(message) : undefined
const anyHistoryMsg = normalizedContent?.protocolMessage?.historySyncNotification!
const anyHistoryMsg = normalizedContent?.protocolMessage?.historySyncNotification
return anyHistoryMsg
}
+28 -13
View File
@@ -317,7 +317,7 @@ export const getStream = async (item: WAMediaUpload, opts?: RequestInit & { maxC
const urlStr = item.url.toString()
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
}
@@ -414,7 +414,11 @@ export const encryptedStream = async (
let fileLength = 0
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 sha256Enc = Crypto.createHash('sha256')
@@ -528,7 +532,7 @@ export const downloadContentFromMessage = async (
opts: MediaDownloadOptions = {}
) => {
const isValidMediaUrl = url?.startsWith('https://mmg.whatsapp.net/')
const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath!)
const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath ?? '')
if (!downloadUrl) {
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) => {
if (startByte || endByte) {
const start = bytesFetched >= startByte! ? undefined : Math.max(startByte! - bytesFetched, 0)
const end = bytesFetched + bytes.length < endByte! ? undefined : Math.max(endByte! - bytesFetched, 0)
const start = bytesFetched >= (startByte ?? 0) ? undefined : Math.max((startByte ?? 0) - bytesFetched, 0)
const end = bytesFetched + bytes.length < (endByte ?? 0) ? undefined : Math.max((endByte ?? 0) - bytesFetched, 0)
push(bytes.slice(start, end))
@@ -652,7 +656,7 @@ export function extensionForMediaMessage(message: WAMessageContent) {
extension = '.jpeg'
} else {
const messageContent = message[type] as WAGenericMediaMessage
extension = getExtension(messageContent.mimetype!)!
extension = getExtension(messageContent.mimetype ?? '') ?? ''
}
return extension
@@ -860,8 +864,8 @@ export const getWAUploadToServer = (
if (result?.url || result?.direct_path) {
urls = {
mediaUrl: result.url!,
directPath: result.direct_path!,
mediaUrl: result.url ?? '',
directPath: result.direct_path ?? '',
meta_hmac: result.meta_hmac,
fbid: result.fbid,
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
*/
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 recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
const iv = Crypto.randomBytes(12)
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 = {
tag: 'receipt',
attrs: {
id: key.id!,
id: key.id,
to: jidNormalizedUser(meId),
type: 'server-error'
},
@@ -925,7 +937,7 @@ export const encryptMediaRetryRequest = (key: WAMessageKey, mediaKey: Buffer | U
{
tag: 'rmr',
attrs: {
jid: key.remoteJid!,
jid: key.remoteJid,
from_me: (!!key.fromMe).toString(),
// @ts-ignore
participant: key.participant || undefined
@@ -938,7 +950,10 @@ export const encryptMediaRetryRequest = (key: WAMessageKey, mediaKey: Buffer | U
}
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] = {
key: {
@@ -951,7 +966,7 @@ export const decodeMediaRetryNode = (node: BinaryNode) => {
const errorNode = getBinaryNodeChild(node, 'error')
if (errorNode) {
const errorCode = +errorNode.attrs.code!
const errorCode = +(errorNode.attrs.code ?? '0')
event.error = new Boom(`Failed to re-upload media (${errorCode})`, {
data: errorNode.attrs,
statusCode: getStatusCodeForMediaRetry(errorCode)
+76 -38
View File
@@ -168,14 +168,17 @@ export const prepareWAMessageMedia = async (
}
if (cacheableKey) {
const mediaBuff = await options.mediaCache!.get<Buffer>(cacheableKey)
const mediaBuff = await options.mediaCache?.get<Buffer>(cacheableKey)
if (mediaBuff) {
logger?.debug({ cacheableKey }, 'got media cache hit')
const obj = proto.Message.decode(mediaBuff)
const key = `${mediaType}Message`
Object.assign(obj[key as keyof proto.Message]!, { ...uploadData, media: undefined })
const mediaMsg = obj[key as keyof proto.Message]
if (mediaMsg) {
Object.assign(mediaMsg, { ...uploadData, media: undefined })
}
return obj
}
@@ -222,7 +225,7 @@ export const prepareWAMessageMedia = async (
if (cacheableKey) {
logger?.debug({ cacheableKey }, 'set cache')
await options.mediaCache!.set(cacheableKey, WAProto.Message.encode(obj).finish())
await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish())
}
return obj
@@ -257,9 +260,13 @@ export const prepareWAMessageMedia = async (
})(),
(async () => {
try {
if ((requiresThumbnailComputation || requiresDurationComputation || requiresWaveformProcessing) && !originalFilePath) {
throw new Boom('Missing file path for processing')
}
if (requiresThumbnailComputation) {
const { thumbnail, originalImageDimensions } = await generateThumbnail(
originalFilePath!,
originalFilePath,
mediaType as 'image' | 'video',
options
)
@@ -274,12 +281,12 @@ export const prepareWAMessageMedia = async (
}
if (requiresDurationComputation) {
uploadData.seconds = await getAudioDuration(originalFilePath!)
uploadData.seconds = await getAudioDuration(originalFilePath)
logger?.debug('computed audio duration')
}
if (requiresWaveformProcessing) {
uploadData.waveform = await getAudioWaveform(originalFilePath!, logger)
uploadData.waveform = await getAudioWaveform(originalFilePath, logger)
logger?.debug('processed waveform')
}
@@ -325,7 +332,7 @@ export const prepareWAMessageMedia = async (
if (cacheableKey) {
logger?.debug({ cacheableKey }, 'set cache')
await options.mediaCache!.set(cacheableKey, WAProto.Message.encode(obj).finish())
await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish())
}
return obj
@@ -359,7 +366,11 @@ export const generateForwardMessageContent = (message: WAMessage, forceForward?:
// hacky copy
content = normalizeMessageContent(content)
content = proto.Message.decode(proto.Message.encode(content!).finish())
if (!content) {
throw new Boom('no content in message', { statusCode: 400 })
}
content = proto.Message.decode(proto.Message.encode(content).finish())
let key = Object.keys(content)[0] as keyof proto.IMessage
@@ -591,7 +602,8 @@ export const generateCarouselMessage = async (
// Validate cards
for (let i = 0; i < cards.length; i++) {
const card = cards[i]!
const card = cards[i]
if (!card) continue
// Validate mutual exclusivity of media types
if (card.image && card.video) {
@@ -1042,7 +1054,9 @@ export const generateProductCarouselMessage = (
// Validate each product has a valid productId
for (let i = 0; i < products.length; i++) {
const product = products[i]!
const product = products[i]
if (!product) continue
if (!product.productId || typeof product.productId !== 'string' || product.productId.trim().length === 0) {
throw new Boom(`Product at index ${i} must have a non-empty productId`, { statusCode: 400 })
}
@@ -1261,14 +1275,7 @@ export const generateWAMessageContent = async (
options.logger?.warn('[EXPERIMENTAL] Sending buttonsMessage - this may not work and can cause bans')
} else if (hasNonNullishProperty(message, 'text') && hasNonNullishProperty(message, 'templateButtons')) {
// Process templateButtons
const templateMessage: proto.Message.ITemplateMessage = {
hydratedTemplate: {
hydratedContentText: (message as any).text,
hydratedFooterText: (message as any).footer
}
}
templateMessage.hydratedTemplate!.hydratedButtons = ((message as any).templateButtons as any[]).map((btn: any) => {
const hydratedButtons = ((message as any).templateButtons as any[]).map((btn: any) => {
if (btn.quickReplyButton) {
return { index: btn.index, quickReplyButton: btn.quickReplyButton }
} else if (btn.urlButton) {
@@ -1279,6 +1286,14 @@ export const generateWAMessageContent = async (
return btn
})
const templateMessage: proto.Message.ITemplateMessage = {
hydratedTemplate: {
hydratedContentText: (message as any).text,
hydratedFooterText: (message as any).footer,
hydratedButtons
}
}
m.templateMessage = templateMessage
options.logger?.warn('[EXPERIMENTAL] Sending templateMessage - this may not work and can cause bans')
} else if (hasNonNullishProperty(message, 'sections')) {
@@ -1354,7 +1369,9 @@ export const generateWAMessageContent = async (
let expectedVideoCount = 0
for (let i = 0; i < medias.length; i++) {
const media = medias[i]!
const media = medias[i]
if (!media) continue
if (hasNonNullishProperty(media as AnyMessageContent, 'image')) {
expectedImageCount++
} else if (hasNonNullishProperty(media as AnyMessageContent, 'video')) {
@@ -1653,13 +1670,15 @@ export const generateWAMessageContent = async (
}
if (hasOptionalProperty(message, 'mentions') && message.mentions?.length) {
const messageType = Object.keys(m)[0]! as Extract<keyof proto.IMessage, MessageWithContextInfo>
const key = m[messageType]
if ('contextInfo' in key! && !!key.contextInfo) {
key.contextInfo.mentionedJid = message.mentions
} else if (key!) {
key.contextInfo = {
mentionedJid: message.mentions
const messageType = Object.keys(m)[0] as Extract<keyof proto.IMessage, MessageWithContextInfo>
if (messageType) {
const key = m[messageType]
if (key && 'contextInfo' in key && !!key.contextInfo) {
key.contextInfo.mentionedJid = message.mentions
} else if (key) {
key.contextInfo = {
mentionedJid: message.mentions
}
}
}
}
@@ -1676,12 +1695,14 @@ export const generateWAMessageContent = async (
}
if (hasOptionalProperty(message, 'contextInfo') && !!message.contextInfo) {
const messageType = Object.keys(m)[0]! as Extract<keyof proto.IMessage, MessageWithContextInfo>
const key = m[messageType]
if ('contextInfo' in key! && !!key.contextInfo) {
key.contextInfo = { ...key.contextInfo, ...message.contextInfo }
} else if (key!) {
key.contextInfo = message.contextInfo
const messageType = Object.keys(m)[0] as Extract<keyof proto.IMessage, MessageWithContextInfo>
if (messageType) {
const key = m[messageType]
if (key && 'contextInfo' in key && !!key.contextInfo) {
key.contextInfo = { ...key.contextInfo, ...message.contextInfo }
} else if (key) {
key.contextInfo = message.contextInfo
}
}
}
@@ -1708,8 +1729,16 @@ export const generateWAMessageFromContent = (
options.timestamp = new Date()
}
const innerMessage = normalizeMessageContent(message)!
const key = getContentType(innerMessage)! as Exclude<keyof proto.IMessage, 'conversation'>
const innerMessage = normalizeMessageContent(message)
if (!innerMessage) {
throw new Boom('no content in message', { statusCode: 400 })
}
const key = getContentType(innerMessage)
if (!key) {
throw new Boom('unable to determine message content type', { statusCode: 400 })
}
const timestamp = unixTimestampSeconds(options.timestamp)
const { quoted, userJid } = options
@@ -1718,8 +1747,16 @@ export const generateWAMessageFromContent = (
? userJid // TODO: Add support for LIDs
: quoted.participant || quoted.key.participant || quoted.key.remoteJid
let quotedMsg = normalizeMessageContent(quoted.message)!
const msgType = getContentType(quotedMsg)!
let quotedMsg = normalizeMessageContent(quoted.message)
if (!quotedMsg) {
throw new Boom('no content in quoted message', { statusCode: 400 })
}
const msgType = getContentType(quotedMsg)
if (!msgType) {
throw new Boom('unable to determine quoted message content type', { statusCode: 400 })
}
// strip any redundant properties
quotedMsg = proto.Message.create({ [msgType]: quotedMsg[msgType] })
@@ -1728,9 +1765,10 @@ export const generateWAMessageFromContent = (
delete quotedContent.contextInfo
}
const innerContent = innerMessage[key as Exclude<keyof proto.IMessage, 'conversation'>]
const contextInfo: proto.IContextInfo =
('contextInfo' in innerMessage[key]! && innerMessage[key]?.contextInfo) || {}
contextInfo.participant = jidNormalizedUser(participant!)
(innerContent && 'contextInfo' in innerContent && innerMessage[key as Exclude<keyof proto.IMessage, 'conversation'>]?.contextInfo) || {}
contextInfo.participant = jidNormalizedUser(participant ?? '')
contextInfo.stanzaId = quoted.key.id
contextInfo.quotedMessage = quotedMsg
+23 -7
View File
@@ -150,7 +150,7 @@ export const makeNoiseHandler = ({
while (true) {
if (inBytes.length < 3) return
size = (inBytes[0]! << 16) | (inBytes[1]! << 8) | inBytes[2]!
size = (inBytes[0] << 16) | (inBytes[1] << 8) | inBytes[2]
if (inBytes.length < size + 3) return
@@ -180,13 +180,25 @@ export const makeNoiseHandler = ({
mixIntoKey,
finishInit,
processHandshake: ({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => {
authenticate(serverHello!.ephemeral!)
mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!))
if (!serverHello?.ephemeral) {
throw new Boom('Missing server hello ephemeral', { statusCode: 500 })
}
const decStaticContent = decrypt(serverHello!.static!)
if (!serverHello?.static) {
throw new Boom('Missing server hello static', { statusCode: 500 })
}
if (!serverHello?.payload) {
throw new Boom('Missing server hello payload', { statusCode: 500 })
}
authenticate(serverHello.ephemeral)
mixIntoKey(Curve.sharedKey(privateKey, serverHello.ephemeral))
const decStaticContent = decrypt(serverHello.static)
mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
const certDecoded = decrypt(serverHello!.payload!)
const certDecoded = decrypt(serverHello.payload)
const { intermediate: certIntermediate, leaf } = proto.CertChain.decode(certDecoded)
// leaf
@@ -202,7 +214,11 @@ export const makeNoiseHandler = ({
const { issuerSerial } = details
const verify = Curve.verify(details.key!, leaf.details, leaf.signature)
if (!details.key) {
throw new Boom('Missing certificate key', { statusCode: 500 })
}
const verify = Curve.verify(details.key, leaf.details, leaf.signature)
const verifyIntermediate = Curve.verify(
WA_CERT_DETAILS.PUBLIC_KEY,
@@ -223,7 +239,7 @@ export const makeNoiseHandler = ({
}
const keyEnc = encrypt(noiseKey.public)
mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!))
mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello.ephemeral))
return keyEnc
},
+98 -39
View File
@@ -59,32 +59,40 @@ const REAL_MSG_REQ_ME_STUB_TYPES = new Set([WAMessageStubType.GROUP_PARTICIPANT_
/** Cleans a received message to further processing */
export const cleanMessage = (message: WAMessage, meId: string, meLid: string) => {
// ensure remoteJid and participant doesn't have device or agent in it
if (isHostedPnUser(message.key.remoteJid!) || isHostedLidUser(message.key.remoteJid!)) {
const remoteJid = message.key.remoteJid ?? ''
if (isHostedPnUser(remoteJid) || isHostedLidUser(remoteJid)) {
message.key.remoteJid = jidEncode(
jidDecode(message.key?.remoteJid!)?.user!,
isHostedPnUser(message.key.remoteJid!) ? 's.whatsapp.net' : 'lid'
jidDecode(remoteJid)?.user ?? '',
isHostedPnUser(remoteJid) ? 's.whatsapp.net' : 'lid'
)
} else {
message.key.remoteJid = jidNormalizedUser(message.key.remoteJid!)
message.key.remoteJid = jidNormalizedUser(remoteJid)
}
if (isHostedPnUser(message.key.participant!) || isHostedLidUser(message.key.participant!)) {
const participantJid = message.key.participant ?? ''
if (isHostedPnUser(participantJid) || isHostedLidUser(participantJid)) {
message.key.participant = jidEncode(
jidDecode(message.key.participant!)?.user!,
isHostedPnUser(message.key.participant!) ? 's.whatsapp.net' : 'lid'
jidDecode(participantJid)?.user ?? '',
isHostedPnUser(participantJid) ? 's.whatsapp.net' : 'lid'
)
} else {
message.key.participant = jidNormalizedUser(message.key.participant!)
message.key.participant = jidNormalizedUser(participantJid)
}
const content = normalizeMessageContent(message.message)
// if the message has a reaction, ensure fromMe & remoteJid are from our perspective
if (content?.reactionMessage) {
normaliseKey(content.reactionMessage.key!)
const reactionKey = content.reactionMessage.key
if (reactionKey) {
normaliseKey(reactionKey)
}
}
if (content?.pollUpdateMessage) {
normaliseKey(content.pollUpdateMessage.pollCreationMessageKey!)
const pollCreationKey = content.pollUpdateMessage.pollCreationMessageKey
if (pollCreationKey) {
normaliseKey(pollCreationKey)
}
}
function normaliseKey(msgKey: WAMessageKey) {
@@ -94,8 +102,8 @@ export const cleanMessage = (message: WAMessage, meId: string, meLid: string) =>
// if the sender believed the message being reacted to is not from them
// we've to correct the key to be from them, or some other participant
msgKey.fromMe = !msgKey.fromMe
? areJidsSameUser(msgKey.participant || msgKey.remoteJid!, meId) ||
areJidsSameUser(msgKey.participant || msgKey.remoteJid!, meLid)
? areJidsSameUser(msgKey.participant || (msgKey.remoteJid ?? ''), meId) ||
areJidsSameUser(msgKey.participant || (msgKey.remoteJid ?? ''), meLid)
: // if the message being reacted to, was from them
// fromMe automatically becomes false
false
@@ -150,10 +158,11 @@ export const normalizeMessageJids = async (
export const isRealMessage = (message: WAMessage) => {
const normalizedContent = normalizeMessageContent(message.message)
const hasSomeContent = !!getContentType(normalizedContent)
const stubType = message.messageStubType ?? 0
return (
(!!normalizedContent ||
REAL_MSG_STUB_TYPES.has(message.messageStubType!) ||
REAL_MSG_REQ_ME_STUB_TYPES.has(message.messageStubType!)) &&
REAL_MSG_STUB_TYPES.has(stubType) ||
REAL_MSG_REQ_ME_STUB_TYPES.has(stubType)) &&
hasSomeContent &&
!normalizedContent?.protocolMessage &&
!normalizedContent?.reactionMessage &&
@@ -168,11 +177,12 @@ export const shouldIncrementChatUnread = (message: WAMessage) => !message.key.fr
* Typically -- that'll be the remoteJid, but for broadcasts, it'll be the participant
*/
export const getChatId = ({ remoteJid, participant, fromMe }: WAMessageKey) => {
if (isJidBroadcast(remoteJid!) && !isJidStatusBroadcast(remoteJid!) && !fromMe) {
return participant!
const jid = remoteJid ?? ''
if (isJidBroadcast(jid) && !isJidStatusBroadcast(jid) && !fromMe) {
return participant ?? ''
}
return remoteJid!
return jid
}
type PollContext = {
@@ -219,7 +229,11 @@ export function decryptPollVote(
const decKey = hmacSign(sign, key0, 'sha256')
const aad = toBinary(`${pollMsgId}\u0000${voterJid}`)
const decrypted = aesDecryptGCM(encPayload!, decKey, encIv!, aad)
if (!encPayload || !encIv) {
throw new Error('Missing encPayload or encIv for poll vote decryption')
}
const decrypted = aesDecryptGCM(encPayload, decKey, encIv, aad)
return proto.Message.PollVoteMessage.decode(decrypted)
function toBinary(txt: string) {
@@ -249,7 +263,11 @@ export function decryptEventResponse(
const decKey = hmacSign(sign, key0, 'sha256')
const aad = toBinary(`${eventMsgId}\u0000${responderJid}`)
const decrypted = aesDecryptGCM(encPayload!, decKey, encIv!, aad)
if (!encPayload || !encIv) {
throw new Error('Missing encPayload or encIv for event response decryption')
}
const decrypted = aesDecryptGCM(encPayload, decKey, encIv, aad)
return proto.Message.EventResponseMessage.decode(decrypted)
function toBinary(txt: string) {
@@ -271,7 +289,12 @@ const processMessage = async (
getMessage
}: ProcessMessageContext
) => {
const meId = creds.me!.id
const meUser = creds.me
if (!meUser) {
return
}
const meId = meUser.id
const { accountSettings } = creds
const chat: Partial<Chat> = { id: jidNormalizedUser(getChatId(message.key)) }
@@ -299,7 +322,10 @@ const processMessage = async (
if (protocolMsg) {
switch (protocolMsg.type) {
case proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION:
const histNotification = protocolMsg.historySyncNotification!
const histNotification = protocolMsg.historySyncNotification
if (!histNotification) {
break
}
const process = shouldProcessHistoryMsg
const isLatest = !creds.processedHistoryMessages?.length
@@ -366,16 +392,23 @@ const processMessage = async (
break
case proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE:
const keys = protocolMsg.appStateSyncKeyShare!.keys
const keys = protocolMsg.appStateSyncKeyShare?.keys
if (keys?.length) {
let newAppStateSyncKeyId = ''
await keyStore.transaction(async () => {
const newKeys: string[] = []
for (const { keyData, keyId } of keys) {
const strKeyId = Buffer.from(keyId!.keyId!).toString('base64')
const keyIdValue = keyId?.keyId
if (!keyIdValue) {
continue
}
const strKeyId = Buffer.from(keyIdValue).toString('base64')
newKeys.push(strKeyId)
await keyStore.set({ 'app-state-sync-key': { [strKeyId]: keyData! } })
if (keyData) {
await keyStore.set({ 'app-state-sync-key': { [strKeyId]: keyData } })
}
newAppStateSyncKeyId = strKeyId
}
@@ -394,7 +427,7 @@ const processMessage = async (
{
key: {
...message.key,
id: protocolMsg.key!.id
id: protocolMsg.key?.id
},
update: { message: null, messageStubType: WAMessageStubType.REVOKE, key: message.key }
}
@@ -407,17 +440,27 @@ const processMessage = async (
})
break
case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE:
const response = protocolMsg.peerDataOperationRequestResponseMessage!
const response = protocolMsg.peerDataOperationRequestResponseMessage
if (response) {
await placeholderResendCache?.del(response.stanzaId!)
if (response.stanzaId) {
await placeholderResendCache?.del(response.stanzaId)
}
// TODO: IMPLEMENT HISTORY SYNC ETC (sticker uploads etc.).
const { peerDataOperationResult } = response
if (!peerDataOperationResult) {
break
}
let recoveredCount = 0
for (const result of peerDataOperationResult!) {
for (const result of peerDataOperationResult) {
const { placeholderMessageResendResponse: retryResponse } = result
//eslint-disable-next-line max-depth
if (retryResponse) {
const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes!)
if (!retryResponse.webMessageInfoBytes) {
continue
}
const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes)
// Track CTWA message recovery success
recoveredCount++
@@ -435,7 +478,7 @@ const processMessage = async (
ev.emit('messages.upsert', {
messages: [webMessageInfo as WAMessage],
type: 'notify',
requestId: response.stanzaId!
requestId: response.stanzaId ?? ''
})
}
}
@@ -474,17 +517,21 @@ const processMessage = async (
const labelAssociationMsg = protocolMsg.memberLabel
if (labelAssociationMsg?.label) {
ev.emit('group.member-tag.update', {
groupId: chat.id!,
groupId: chat.id ?? '',
label: labelAssociationMsg.label,
participant: message.key.participant!,
participantAlt: message.key.participantAlt!,
participant: message.key.participant ?? '',
participantAlt: message.key.participantAlt ?? '',
messageTimestamp: Number(message.messageTimestamp)
})
}
break
case proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC:
const encodedPayload = protocolMsg.lidMigrationMappingSyncMessage?.encodedMappingPayload!
const encodedPayload = protocolMsg.lidMigrationMappingSyncMessage?.encodedMappingPayload
if (!encodedPayload) {
break
}
const { pnToLidMappings, chatDbMigrationTimestamp } =
proto.LIDMigrationMappingSyncPayload.decode(encodedPayload)
logger?.debug({ pnToLidMappings, chatDbMigrationTimestamp }, 'got lid mappings and chat db migration timestamp')
@@ -502,6 +549,11 @@ const processMessage = async (
}
}
} else if (content?.reactionMessage) {
const reactionKey = content.reactionMessage.key
if (!reactionKey) {
return
}
const reaction: proto.IReaction = {
...content.reactionMessage,
key: message.key
@@ -509,12 +561,15 @@ const processMessage = async (
ev.emit('messages.reaction', [
{
reaction,
key: content.reactionMessage?.key!
key: reactionKey
}
])
} else if (content?.encEventResponseMessage) {
const encEventResponse = content.encEventResponseMessage
const creationMsgKey = encEventResponse.eventCreationMessageKey!
const creationMsgKey = encEventResponse.eventCreationMessageKey
if (!creationMsgKey) {
return
}
// we need to fetch the event creation message to get the event enc key
const eventMsg = await getMessage(creationMsgKey)
@@ -523,12 +578,16 @@ const processMessage = async (
const meIdNormalised = jidNormalizedUser(meId)
// all jids need to be PN
const eventCreatorKey = creationMsgKey.participant || creationMsgKey.remoteJid!
const eventCreatorKey = creationMsgKey.participant || (creationMsgKey.remoteJid ?? '')
const eventCreatorPn = isLidUser(eventCreatorKey)
? await signalRepository.lidMapping.getPNForLID(eventCreatorKey)
: eventCreatorKey
if (!eventCreatorPn) {
return
}
const eventCreatorJid = getKeyAuthor(
{ remoteJid: jidNormalizedUser(eventCreatorPn!), fromMe: meIdNormalised === eventCreatorPn },
{ remoteJid: jidNormalizedUser(eventCreatorPn), fromMe: meIdNormalised === eventCreatorPn },
meIdNormalised
)
@@ -541,7 +600,7 @@ const processMessage = async (
const responseMsg = decryptEventResponse(encEventResponse, {
eventEncKey,
eventCreatorJid,
eventMsgId: creationMsgKey.id!,
eventMsgId: creationMsgKey.id ?? '',
responderJid
})
+32 -18
View File
@@ -88,14 +88,18 @@ export const xmppPreKey = (pair: KeyPair, id: number): BinaryNode => ({
})
export const parseAndInjectE2ESessions = async (node: BinaryNode, repository: SignalRepositoryWithLIDStore) => {
const extractKey = (key: BinaryNode) =>
key
? {
keyId: getBinaryNodeChildUInt(key, 'id', 3)!,
publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')!),
signature: getBinaryNodeChildBuffer(key, 'signature')!
}
: undefined
const extractKey = (key: BinaryNode) => {
if (!key) return undefined
const keyId = getBinaryNodeChildUInt(key, 'id', 3)
const publicKeyBuf = getBinaryNodeChildBuffer(key, 'value')
const signature = getBinaryNodeChildBuffer(key, 'signature')
if (keyId === undefined || !publicKeyBuf || !signature) return undefined
return {
keyId,
publicKey: generateSignalPubKey(publicKeyBuf),
signature
}
}
const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user')
for (const node of nodes) {
assertNodeErrorFree(node)
@@ -111,20 +115,26 @@ export const parseAndInjectE2ESessions = async (node: BinaryNode, repository: Si
for (const nodesChunk of chunks) {
for (const node of nodesChunk) {
const signedKey = getBinaryNodeChild(node, 'skey')!
const key = getBinaryNodeChild(node, 'key')!
const identity = getBinaryNodeChildBuffer(node, 'identity')!
const jid = node.attrs.jid!
const signedKey = getBinaryNodeChild(node, 'skey')
const key = getBinaryNodeChild(node, 'key')
const identity = getBinaryNodeChildBuffer(node, 'identity')
const jid = node.attrs.jid
if (!signedKey || !key || !identity || !jid) continue
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({
jid,
session: {
registrationId: registrationId!,
registrationId,
identityKey: generateSignalPubKey(identity),
signedPreKey: extractKey(signedKey)!,
preKey: extractKey(key)!
signedPreKey,
preKey
}
})
}
@@ -137,14 +147,18 @@ export const extractDeviceJids = (
myLid: string,
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[] = []
for (const userResult of result) {
const { devices, id } = userResult as { devices: ParsedDeviceInfo; id: string }
const decoded = jidDecode(id)!,
{ user, server } = decoded
const decoded = jidDecode(id)
if (!decoded) continue
const { user, server } = decoded
let { domainType } = decoded
const deviceList = devices?.deviceList as DeviceListData[]
if (!Array.isArray(deviceList)) continue
+1 -1
View File
@@ -575,7 +575,7 @@ export const prepareStickerPackMessage = async (
duplicateCount++
// Merge emojis (deduplicate)
const mergedEmojis = Array.from(new Set([...existingMetadata.emojis!, ...emojis]))
const mergedEmojis = Array.from(new Set([...(existingMetadata.emojis ?? []), ...emojis]))
existingMetadata.emojis = mergedEmojis
// Merge accessibility labels (concatenate with separator if both exist)
+30 -13
View File
@@ -64,7 +64,12 @@ const getClientPayload = (config: SocketConfig) => {
}
export const generateLoginNode = (userJid: string, config: SocketConfig): proto.IClientPayload => {
const { user, device } = jidDecode(userJid)!
const decoded = jidDecode(userJid)
if (!decoded) {
throw new Boom('Invalid user JID', { statusCode: 400 })
}
const { user, device } = decoded
const payload: proto.IClientPayload = {
...getClientPayload(config),
passive: true,
@@ -153,6 +158,9 @@ export const configureSuccessfulPairing = (
}: Pick<AuthenticationCreds, 'advSecretKey' | 'signedIdentityKey' | 'signalIdentities'>
) => {
const msgId = stanza.attrs.id
if (!msgId) {
throw new Boom('Missing message ID', { statusCode: 400 })
}
const pairSuccessNode = getBinaryNodeChild(stanza, 'pair-success')
@@ -168,42 +176,51 @@ export const configureSuccessfulPairing = (
const bizName = businessNode?.attrs.name
const jid = deviceNode.attrs.jid
const lid = deviceNode.attrs.lid
if (!jid || !lid) {
throw new Boom('Missing JID or LID in device node', { statusCode: 400 })
}
const { details, hmac, accountType } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityNode.content as Buffer)
if (!details || !hmac) {
throw new Boom('Missing ADV signature fields', { statusCode: 400 })
}
let hmacPrefix = Buffer.from([])
if (accountType !== undefined && accountType === proto.ADVEncryptionType.HOSTED) {
hmacPrefix = WA_ADV_HOSTED_ACCOUNT_SIG_PREFIX
}
const advSign = hmacSign(Buffer.concat([hmacPrefix, details!]), Buffer.from(advSecretKey, 'base64'))
if (Buffer.compare(hmac!, advSign) !== 0) {
const advSign = hmacSign(Buffer.concat([hmacPrefix, details]), Buffer.from(advSecretKey, 'base64'))
if (Buffer.compare(hmac, advSign) !== 0) {
throw new Boom('Invalid account signature')
}
const account = proto.ADVSignedDeviceIdentity.decode(details!)
const account = proto.ADVSignedDeviceIdentity.decode(details)
const { accountSignatureKey, accountSignature, details: deviceDetails } = account
if (!accountSignatureKey || !accountSignature || !deviceDetails) {
throw new Boom('Missing ADV account fields', { statusCode: 400 })
}
const deviceIdentity = proto.ADVDeviceIdentity.decode(deviceDetails!)
const deviceIdentity = proto.ADVDeviceIdentity.decode(deviceDetails)
const accountSignaturePrefix =
deviceIdentity.deviceType === proto.ADVEncryptionType.HOSTED
? WA_ADV_HOSTED_ACCOUNT_SIG_PREFIX
: WA_ADV_ACCOUNT_SIG_PREFIX
const accountMsg = Buffer.concat([accountSignaturePrefix, deviceDetails!, signedIdentityKey.public])
if (!Curve.verify(accountSignatureKey!, accountMsg, accountSignature!)) {
const accountMsg = Buffer.concat([accountSignaturePrefix, deviceDetails, signedIdentityKey.public])
if (!Curve.verify(accountSignatureKey, accountMsg, accountSignature)) {
throw new Boom('Failed to verify account signature')
}
const deviceMsg = Buffer.concat([
WA_ADV_DEVICE_SIG_PREFIX,
deviceDetails!,
deviceDetails,
signedIdentityKey.public,
accountSignatureKey!
accountSignatureKey
])
account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg)
const identity = createSignalIdentity(lid!, accountSignatureKey!)
const identity = createSignalIdentity(lid, accountSignatureKey)
const accountEnc = encodeSignedDeviceIdentity(account, false)
const reply: BinaryNode = {
@@ -211,7 +228,7 @@ export const configureSuccessfulPairing = (
attrs: {
to: S_WHATSAPP_NET,
type: 'result',
id: msgId!
id: msgId
},
content: [
{
@@ -220,7 +237,7 @@ export const configureSuccessfulPairing = (
content: [
{
tag: 'device-identity',
attrs: { 'key-index': deviceIdentity.keyIndex!.toString() },
attrs: { 'key-index': String(deviceIdentity.keyIndex ?? '') },
content: accountEnc
}
]
@@ -230,7 +247,7 @@ export const configureSuccessfulPairing = (
const authUpdate: Partial<AuthenticationCreds> = {
account,
me: { id: jid!, name: bizName, lid },
me: { id: jid, name: bizName, lid },
signalIdentities: [...(signalIdentities || []), identity],
platform: platformNode?.attrs.name
}
+6 -6
View File
@@ -165,19 +165,19 @@ export async function getCachedVersion(
} = config
// 1. Check memory cache first (fastest)
if (isCacheValid(memoryCache, cacheTtlMs)) {
const age = Date.now() - memoryCache!.fetchedAt
if (isCacheValid(memoryCache, cacheTtlMs) && memoryCache) {
const age = Date.now() - memoryCache.fetchedAt
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)
const fileCache = await readCacheFile(cacheFilePath)
if (isCacheValid(fileCache, cacheTtlMs)) {
if (isCacheValid(fileCache, cacheTtlMs) && fileCache) {
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')
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
+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 => {
// todo: investigate how to implement hosted ids in this case
const sepIdx = typeof jid === 'string' ? jid.indexOf('@') : -1
if (sepIdx < 0) {
if (sepIdx < 0 || !jid) {
return undefined
}
const server = jid!.slice(sepIdx + 1)
const userCombined = jid!.slice(0, sepIdx)
const server = jid.slice(sepIdx + 1)
const userCombined = jid.slice(0, sepIdx)
const [userAgent, device] = userCombined.split(':')
const [user, agent] = userAgent!.split('_')
const [user, agent] = (userAgent ?? '').split('_')
let domainType = WAJIDDomains.WHATSAPP
if (server === 'lid') {
@@ -78,7 +78,7 @@ export const jidDecode = (jid: string | undefined): FullJid | undefined => {
return {
server: server as JidServer,
user: user!,
user: user ?? '',
domainType,
device: device ? +device : undefined
}
@@ -114,7 +114,7 @@ export const isAnyPnUser = (jid: string | undefined) =>
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) => {
const result = jidDecode(jid)
@@ -129,6 +129,11 @@ export const jidNormalizedUser = (jid: string | undefined) => {
export const transferDevice = (fromJid: string, toJid: string) => {
const fromDecoded = jidDecode(fromJid)
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)
}
+5 -1
View File
@@ -78,7 +78,11 @@ function encodeEvents(binaryInfo: BinaryInfo) {
}
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))
}
}
}
@@ -35,7 +35,7 @@ export class USyncBotProfileProtocol implements USyncQueryProtocol {
return {
tag: 'bot',
attrs: {},
content: [{ tag: 'profile', attrs: { persona_id: user.personaId! } }]
content: [{ tag: 'profile', attrs: { persona_id: user.personaId ?? '' } }]
}
}
@@ -51,24 +51,24 @@ export class USyncBotProfileProtocol implements USyncQueryProtocol {
for (const command of getBinaryNodeChildren(commandsNode, 'command')) {
commands.push({
name: getBinaryNodeChildString(command, 'name')!,
description: getBinaryNodeChildString(command, 'description')!
name: getBinaryNodeChildString(command, 'name') ?? '',
description: getBinaryNodeChildString(command, 'description') ?? ''
})
}
for (const prompt of getBinaryNodeChildren(promptsNode, 'prompt')) {
prompts.push(`${getBinaryNodeChildString(prompt, 'emoji')!} ${getBinaryNodeChildString(prompt, 'text')!}`)
prompts.push(`${getBinaryNodeChildString(prompt, 'emoji') ?? ''} ${getBinaryNodeChildString(prompt, 'text') ?? ''}`)
}
return {
isDefault: !!getBinaryNodeChild(profile, 'default'),
jid: node.attrs.jid!,
name: getBinaryNodeChildString(profile, 'name')!,
attributes: getBinaryNodeChildString(profile, 'attributes')!,
description: getBinaryNodeChildString(profile, 'description')!,
category: getBinaryNodeChildString(profile, 'category')!,
personaId: profile!.attrs['persona_id']!,
commandsDescription: getBinaryNodeChildString(commandsNode, 'description')!,
jid: node.attrs.jid ?? '',
name: getBinaryNodeChildString(profile, 'name') ?? '',
attributes: getBinaryNodeChildString(profile, 'attributes') ?? '',
description: getBinaryNodeChildString(profile, 'description') ?? '',
category: getBinaryNodeChildString(profile, 'category') ?? '',
personaId: profile?.attrs['persona_id'] ?? '',
commandsDescription: getBinaryNodeChildString(commandsNode, 'description') ?? '',
commands,
prompts
}
+14 -6
View File
@@ -194,9 +194,13 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
case proto.HistorySync.HistorySyncType.RECENT:
case proto.HistorySync.HistorySyncType.FULL:
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({
id: chat.id!,
id: chat.id,
name: chat.name || undefined,
lid: chat.lidJid || undefined,
phoneNumber: chat.pnJid || undefined
@@ -204,7 +208,7 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
// Source 2: Extract from conversation metadata
addLidPnMapping(
extractLidPnFromConversation(chat.id!, chat.lidJid, chat.pnJid)
extractLidPnFromConversation(chat.id, chat.lidJid, chat.pnJid)
)
const msgs = chat.messages || []
@@ -242,7 +246,7 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
message.messageStubParameters?.[0]
) {
contacts.push({
id: message.key.participant || message.key.remoteJid!,
id: message.key.participant || message.key.remoteJid || '',
verifiedName: message.messageStubParameters?.[0]
})
}
@@ -253,8 +257,12 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
break
case proto.HistorySync.HistorySyncType.PUSH_NAME:
for (const c of item.pushnames!) {
contacts.push({ id: c.id!, notify: c.pushname! })
for (const c of item.pushnames ?? []) {
if (!c.id) {
continue
}
contacts.push({ id: c.id, notify: c.pushname ?? '' })
}
break