chore: add linting

This commit is contained in:
Adhiraj Singh
2022-01-19 15:54:02 +05:30
parent f7f86e69d6
commit 8f11f0be76
49 changed files with 5800 additions and 4314 deletions

6
.eslintignore Normal file
View File

@@ -0,0 +1,6 @@
# Ignore artifacts:
lib
coverage
*.lock
.eslintrc.json
src/WABinary/index.ts

3
.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "@adiwajshing"
}

View File

@@ -31,6 +31,10 @@ const startSock = () => {
await sock.sendMessage(jid, msg) await sock.sendMessage(jid, msg)
} }
sock.ev.on('chats.set', item => console.log(`recv ${item.chats.length} chats (is latest: ${item.isLatest})`))
sock.ev.on('messages.set', item => console.log(`recv ${item.messages.length} messages (is latest: ${item.isLatest})`))
sock.ev.on('contacts.set', item => console.log(`recv ${item.contacts.length} contacts`))
sock.ev.on('messages.upsert', async m => { sock.ev.on('messages.upsert', async m => {
console.log(JSON.stringify(m, undefined, 2)) console.log(JSON.stringify(m, undefined, 2))
@@ -46,7 +50,7 @@ const startSock = () => {
sock.ev.on('messages.update', m => console.log(m)) sock.ev.on('messages.update', m => console.log(m))
sock.ev.on('presence.update', m => console.log(m)) sock.ev.on('presence.update', m => console.log(m))
sock.ev.on('chats.update', m => console.log(m)) sock.ev.on('chats.update', m => console.log(m))
sock.ev.on('contacts.update', m => console.log(m)) sock.ev.on('contacts.upsert', m => console.log(m))
sock.ev.on('connection.update', (update) => { sock.ev.on('connection.update', (update) => {
const { connection, lastDisconnect } = update const { connection, lastDisconnect } = update

View File

@@ -19,14 +19,15 @@
"scripts": { "scripts": {
"test": "jest", "test": "jest",
"prepare": "tsc", "prepare": "tsc",
"lint": "eslint '*/*.ts' --quiet --fix",
"build:all": "tsc && typedoc", "build:all": "tsc && typedoc",
"build:docs": "typedoc", "build:docs": "typedoc",
"build:tsc": "tsc", "build:tsc": "tsc",
"example": "node --inspect -r ts-node/register Example/example.ts", "example": "node --inspect -r ts-node/register Example/example.ts",
"example:legacy": "node --inspect -r ts-node/register Example/example-legacy.ts", "example:legacy": "node --inspect -r ts-node/register Example/example-legacy.ts",
"gen-protobuf": "bash src/BinaryNode/GenerateStatics.sh", "gen-protobuf": "bash src/BinaryNode/GenerateStatics.sh",
"browser-decode": "yarn ts-node src/BrowserMessageDecoding.ts" "browser-decode": "yarn ts-node src/BrowserMessageDecoding.ts",
"lint": "eslint ./src --ext .js,.ts,.jsx,.tsx",
"lint:fix": "eslint ./src --fix --ext .js,.ts,.jsx,.tsx"
}, },
"author": "Adhiraj Singh", "author": "Adhiraj Singh",
"license": "MIT", "license": "MIT",
@@ -56,12 +57,14 @@
"WABinary/*.js" "WABinary/*.js"
], ],
"devDependencies": { "devDependencies": {
"@adiwajshing/eslint-config": "git+https://github.com/adiwajshing/eslint-config",
"@types/got": "^9.6.11", "@types/got": "^9.6.11",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/node": "^14.6.2", "@types/node": "^14.6.2",
"@types/pino": "^6.3.2", "@types/pino": "^6.3.2",
"@types/sharp": "^0.29.4", "@types/sharp": "^0.29.4",
"@types/ws": "^8.0.0", "@types/ws": "^8.0.0",
"eslint": "^7.0.0",
"jest": "^27.0.6", "jest": "^27.0.6",
"jimp": "^0.16.1", "jimp": "^0.16.1",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",

View File

@@ -1,6 +1,6 @@
import P from "pino" import P from 'pino'
import type { MediaType, SocketConfig, LegacySocketConfig, CommonSocketConfig } from "../Types" import type { CommonSocketConfig, LegacySocketConfig, MediaType, SocketConfig } from '../Types'
import { Browsers } from "../Utils" import { Browsers } from '../Utils'
export const UNAUTHORIZED_CODES = [401, 403, 419] export const UNAUTHORIZED_CODES = [401, 403, 419]

View File

@@ -1,8 +1,8 @@
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import EventEmitter from "events" import EventEmitter from 'events'
import { LegacyBaileysEventEmitter, LegacySocketConfig, CurveKeyPair, WAInitResponse, ConnectionState, DisconnectReason } from "../Types" import { ConnectionState, CurveKeyPair, DisconnectReason, LegacyBaileysEventEmitter, LegacySocketConfig, WAInitResponse } from '../Types'
import { newLegacyAuthCreds, bindWaitForConnectionUpdate, computeChallengeResponse, validateNewConnection, Curve, printQRIfNecessaryListener } from "../Utils" import { bindWaitForConnectionUpdate, computeChallengeResponse, Curve, newLegacyAuthCreds, printQRIfNecessaryListener, validateNewConnection } from '../Utils'
import { makeSocket } from "./socket" import { makeSocket } from './socket'
const makeAuthSocket = (config: LegacySocketConfig) => { const makeAuthSocket = (config: LegacySocketConfig) => {
const { const {
@@ -66,6 +66,7 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
tag: 'goodbye' tag: 'goodbye'
}) })
} }
// will call state update to close connection // will call state update to close connection
socket?.end( socket?.end(
new Boom('Logged Out', { statusCode: DisconnectReason.loggedOut }) new Boom('Logged Out', { statusCode: DisconnectReason.loggedOut })
@@ -87,7 +88,9 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
updateState({ qr }) updateState({ qr })
initTimeout = setTimeout(async() => { initTimeout = setTimeout(async() => {
if(state.connection !== 'connecting') return if(state.connection !== 'connecting') {
return
}
logger.debug('regenerating QR') logger.debug('regenerating QR')
try { try {
@@ -101,18 +104,21 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
ttl = newTTL ttl = newTTL
ref = newRef ref = newRef
} catch(error) { } catch(error) {
logger.error({ error }, `error in QR gen`) logger.error({ error }, 'error in QR gen')
if(error.output?.statusCode === 429) { // too many QR requests if(error.output?.statusCode === 429) { // too many QR requests
socket.end(error) socket.end(error)
return return
} }
} }
qrGens += 1 qrGens += 1
qrLoop(ttl) qrLoop(ttl)
}, ttl || 20_000) // default is 20s, on the off-chance ttl is not present }, ttl || 20_000) // default is 20s, on the off-chance ttl is not present
} }
qrLoop(ttl) qrLoop(ttl)
} }
const onOpen = async() => { const onOpen = async() => {
const canDoLogin = canLogin() const canDoLogin = canLogin()
const initQuery = (async() => { const initQuery = (async() => {
@@ -126,7 +132,7 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
if(!canDoLogin) { if(!canDoLogin) {
generateKeysForAuth(ref, ttl) generateKeysForAuth(ref, ttl)
} }
})(); })()
let loginTag: string let loginTag: string
if(canDoLogin) { if(canDoLogin) {
updateEncKeys() updateEncKeys()
@@ -146,6 +152,7 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
logger.warn('Received login timeout req when state=open, ignoring...') logger.warn('Received login timeout req when state=open, ignoring...')
return return
} }
logger.info('sending login request') logger.info('sending login request')
socket.sendNode({ socket.sendNode({
json, json,
@@ -153,8 +160,10 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
}) })
initTimeout = setTimeout(sendLoginReq, 10_000) initTimeout = setTimeout(sendLoginReq, 10_000)
} }
sendLoginReq() sendLoginReq()
} }
await initQuery await initQuery
// wait for response with tag "s1" // wait for response with tag "s1"
@@ -168,8 +177,9 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
initTimeout = undefined initTimeout = undefined
if(response.status && response.status !== 200) { if(response.status && response.status !== 200) {
throw new Boom(`Unexpected error in login`, { data: response, statusCode: response.status }) throw new Boom('Unexpected error in login', { data: response, statusCode: response.status })
} }
// if its a challenge request (we get it when logging in) // if its a challenge request (we get it when logging in)
if(response[1]?.challenge) { if(response[1]?.challenge) {
const json = computeChallengeResponse(response[1].challenge, authInfo) const json = computeChallengeResponse(response[1].challenge, authInfo)
@@ -179,12 +189,15 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
response = await socket.waitForMessage('s2', true).promise response = await socket.waitForMessage('s2', true).promise
} }
if(!response || !response[1]) { if(!response || !response[1]) {
throw new Boom('Received unexpected login response', { data: response }) throw new Boom('Received unexpected login response', { data: response })
} }
if(response[1].type === 'upgrade_md_prod') { if(response[1].type === 'upgrade_md_prod') {
throw new Boom('Require multi-device edition', { statusCode: DisconnectReason.multideviceMismatch }) throw new Boom('Require multi-device edition', { statusCode: DisconnectReason.multideviceMismatch })
} }
// validate the new connection // validate the new connection
const { user, auth } = validateNewConnection(response[1], authInfo, curveKeys)// validate the connection const { user, auth } = validateNewConnection(response[1], authInfo, curveKeys)// validate the connection
const isNewLogin = user.id !== state.legacy!.user?.id const isNewLogin = user.id !== state.legacy!.user?.id
@@ -206,6 +219,7 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
qr: undefined qr: undefined
}) })
} }
ws.once('open', async() => { ws.once('open', async() => {
try { try {
await onOpen() await onOpen()
@@ -235,4 +249,5 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev) waitForConnectionUpdate: bindWaitForConnectionUpdate(ev)
} }
} }
export default makeAuthSocket export default makeAuthSocket

View File

@@ -1,7 +1,7 @@
import { BinaryNode, jidNormalizedUser } from "../WABinary"; import { BaileysEventMap, Chat, ChatModification, Contact, LegacySocketConfig, PresenceData, WABusinessProfile, WAFlag, WAMessageKey, WAMessageUpdate, WAMetric, WAPresence } from '../Types'
import { Chat, Contact, WAPresence, PresenceData, LegacySocketConfig, WAFlag, WAMetric, WABusinessProfile, ChatModification, WAMessageKey, WAMessageUpdate, BaileysEventMap } from "../Types"; import { debouncedTimeout, unixTimestampSeconds } from '../Utils/generics'
import { debouncedTimeout, unixTimestampSeconds } from "../Utils/generics"; import { BinaryNode, jidNormalizedUser } from '../WABinary'
import makeAuthSocket from "./auth"; import makeAuthSocket from './auth'
const makeChatsSocket = (config: LegacySocketConfig) => { const makeChatsSocket = (config: LegacySocketConfig) => {
const { logger } = config const { logger } = config
@@ -56,6 +56,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
} else { } else {
ev.emit('messages.delete', { jid, all: true }) ev.emit('messages.delete', { jid, all: true })
} }
break break
case 'archive': case 'archive':
ev.emit('chats.update', [ { id: jid, archive: true } ]) ev.emit('chats.update', [ { id: jid, archive: true } ])
@@ -87,9 +88,10 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
} else { } else {
ev.emit('chats.update', [{ id: jid, mute: +attributes.mute }]) ev.emit('chats.update', [{ id: jid, mute: +attributes.mute }])
} }
break break
default: default:
logger.warn({ node }, `received unrecognized chat update`) logger.warn({ node }, 'received unrecognized chat update')
break break
} }
} }
@@ -126,7 +128,10 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
} }
ev.on('connection.update', async({ connection }) => { ev.on('connection.update', async({ connection }) => {
if(connection !== 'open') return if(connection !== 'open') {
return
}
try { try {
await Promise.all([ await Promise.all([
sendNode({ sendNode({
@@ -176,6 +181,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
if(attrs.name) { if(attrs.name) {
contacts.push({ id, name: attrs.name }) contacts.push({ id, name: attrs.name })
} }
return { return {
id: jidNormalizedUser(attrs.jid), id: jidNormalizedUser(attrs.jid),
conversationTimestamp: attrs.t ? +attrs.t : undefined, conversationTimestamp: attrs.t ? +attrs.t : undefined,
@@ -232,8 +238,11 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
const update: Partial<Chat> = { const update: Partial<Chat> = {
id: jidNormalizedUser(attrs.jid) id: jidNormalizedUser(attrs.jid)
} }
if(attrs.type === 'false') update.unreadCount = -1 if(attrs.type === 'false') {
else update.unreadCount = 0 update.unreadCount = -1
} else {
update.unreadCount = 0
}
ev.emit('chats.update', [update]) ev.emit('chats.update', [update])
} }
@@ -295,7 +304,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
* @param jid the ID of the person/group you are modifiying * @param jid the ID of the person/group you are modifiying
*/ */
chatModify: async(modification: ChatModification, jid: string, chatInfo: Pick<Chat, 'mute' | 'pin'>, timestampNow?: number) => { chatModify: async(modification: ChatModification, jid: string, chatInfo: Pick<Chat, 'mute' | 'pin'>, timestampNow?: number) => {
let chatAttrs: BinaryNode['attrs'] = { jid: jid } const chatAttrs: BinaryNode['attrs'] = { jid: jid }
let data: BinaryNode[] | undefined = undefined let data: BinaryNode[] | undefined = undefined
timestampNow = timestampNow || unixTimestampSeconds() timestampNow = timestampNow || unixTimestampSeconds()
@@ -356,6 +365,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
// apply it and emit events // apply it and emit events
executeChatModification(node) executeChatModification(node)
} }
return response return response
}, },
/** /**
@@ -427,7 +437,8 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
profile.business_hours.business_config = profile.business_hours.config profile.business_hours.business_config = profile.business_hours.config
delete profile.business_hours.config delete profile.business_hours.config
} }
const json = ['action', "editBusinessProfile", {...profile, v: 2}]
const json = ['action', 'editBusinessProfile', { ...profile, v: 2 }]
await query({ json, expect200: true, requiresPhoneConnection: true }) await query({ json, expect200: true, requiresPhoneConnection: true })
}, },
updateProfileName: async(name: string) => { updateProfileName: async(name: string) => {
@@ -447,6 +458,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
} }) } })
ev.emit('contacts.update', [{ id: user.id, name }]) ev.emit('contacts.update', [{ id: user.id, name }])
} }
return response return response
}, },
/** /**
@@ -480,6 +492,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
} }
}) })
} }
ev.emit('contacts.update', [ { id: jid, imgUrl: eurl } ]) ev.emit('contacts.update', [ { id: jid, imgUrl: eurl } ])
} }
}, },
@@ -513,8 +526,8 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
}] }]
} = await query({ } = await query({
json: [ json: [
"query", "businessProfile", 'query', 'businessProfile',
[ { "wid": jid.replace('@s.whatsapp.net', '@c.us') } ], [ { 'wid': jid.replace('@s.whatsapp.net', '@c.us') } ],
84 84
], ],
expect200: true, expect200: true,
@@ -528,4 +541,5 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
} }
} }
} }
export default makeChatsSocket export default makeChatsSocket

View File

@@ -1,7 +1,7 @@
import { BinaryNode, jidNormalizedUser } from "../WABinary"; import { GroupMetadata, GroupModificationResponse, GroupParticipant, LegacySocketConfig, ParticipantAction, WAFlag, WAGroupCreateResponse, WAMetric } from '../Types'
import { LegacySocketConfig, GroupModificationResponse, ParticipantAction, GroupMetadata, WAFlag, WAMetric, WAGroupCreateResponse, GroupParticipant } from "../Types"; import { generateMessageID, unixTimestampSeconds } from '../Utils/generics'
import { generateMessageID, unixTimestampSeconds } from "../Utils/generics"; import { BinaryNode, jidNormalizedUser } from '../WABinary'
import makeMessagesSocket from "./messages"; import makeMessagesSocket from './messages'
const makeGroupsSocket = (config: LegacySocketConfig) => { const makeGroupsSocket = (config: LegacySocketConfig) => {
const { logger } = config const { logger } = config
@@ -63,6 +63,7 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
return meta return meta
} }
/** Get the metadata (works after you've left the group also) */ /** Get the metadata (works after you've left the group also) */
const groupMetadataMinimal = async(jid: string) => { const groupMetadataMinimal = async(jid: string) => {
const { attrs, content }:BinaryNode = await query({ const { attrs, content }:BinaryNode = await query({
@@ -89,6 +90,7 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
} }
} }
} }
const meta: GroupMetadata = { const meta: GroupMetadata = {
id: jid, id: jid,
owner: attrs?.creator, owner: attrs?.creator,
@@ -129,8 +131,11 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
groupMetadata: async(jid: string, minimal: boolean) => { groupMetadata: async(jid: string, minimal: boolean) => {
let result: GroupMetadata let result: GroupMetadata
if(minimal) result = await groupMetadataMinimal(jid) if(minimal) {
else result = await groupMetadataFull(jid) result = await groupMetadataMinimal(jid)
} else {
result = await groupMetadataFull(jid)
}
return result return result
}, },
@@ -154,6 +159,7 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
metadata = await groupMetadataFull(gid) metadata = await groupMetadataFull(gid)
logger.warn (`group ID switched from ${gid} to ${response.gid}`) logger.warn (`group ID switched from ${gid} to ${response.gid}`)
} }
ev.emit('chats.upsert', [ ev.emit('chats.upsert', [
{ {
id: response.gid!, id: response.gid!,
@@ -247,4 +253,5 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
} }
} }
export default makeGroupsSocket export default makeGroupsSocket

View File

@@ -1,5 +1,5 @@
import { LegacySocketConfig } from '../Types'
import { DEFAULT_LEGACY_CONNECTION_CONFIG } from '../Defaults' import { DEFAULT_LEGACY_CONNECTION_CONFIG } from '../Defaults'
import { LegacySocketConfig } from '../Types'
import _makeLegacySocket from './groups' import _makeLegacySocket from './groups'
// export the last socket layer // export the last socket layer
const makeLegacySocket = (config: Partial<LegacySocketConfig>) => ( const makeLegacySocket = (config: Partial<LegacySocketConfig>) => (

View File

@@ -1,10 +1,10 @@
import { BinaryNode, getBinaryNodeMessages, isJidGroup, jidNormalizedUser, areJidsSameUser } from "../WABinary";
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import { Chat, WAMessageCursor, WAMessage, LegacySocketConfig, WAMessageKey, ParticipantAction, WAMessageStatus, WAMessageStubType, GroupMetadata, AnyMessageContent, MiscMessageGenerationOptions, WAFlag, WAMetric, WAUrlInfo, MediaConnInfo, MessageUpdateType, MessageInfo, MessageInfoUpdate, WAMessageUpdate } from "../Types"; import { proto } from '../../WAProto'
import { toNumber, generateWAMessage, decryptMediaMessageBuffer, extractMessageContent, getWAUploadToServer } from "../Utils"; import { WA_DEFAULT_EPHEMERAL } from '../Defaults'
import makeChatsSocket from "./chats"; import { AnyMessageContent, Chat, GroupMetadata, LegacySocketConfig, MediaConnInfo, MessageInfo, MessageInfoUpdate, MessageUpdateType, MiscMessageGenerationOptions, ParticipantAction, WAFlag, WAMessage, WAMessageCursor, WAMessageKey, WAMessageStatus, WAMessageStubType, WAMessageUpdate, WAMetric, WAUrlInfo } from '../Types'
import { WA_DEFAULT_EPHEMERAL } from "../Defaults"; import { decryptMediaMessageBuffer, extractMessageContent, generateWAMessage, getWAUploadToServer, toNumber } from '../Utils'
import { proto } from "../../WAProto"; import { areJidsSameUser, BinaryNode, getBinaryNodeMessages, isJidGroup, jidNormalizedUser } from '../WABinary'
import makeChatsSocket from './chats'
const STATUS_MAP = { const STATUS_MAP = {
read: WAMessageStatus.READ, read: WAMessageStatus.READ,
@@ -27,7 +27,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
let mediaConn: Promise<MediaConnInfo> let mediaConn: Promise<MediaConnInfo>
const refreshMediaConn = async(forceGet = false) => { const refreshMediaConn = async(forceGet = false) => {
let media = await mediaConn const media = await mediaConn
if(!media || forceGet || (new Date().getTime()-media.fetchDate.getTime()) > media.ttl*1000) { if(!media || forceGet || (new Date().getTime()-media.fetchDate.getTime()) > media.ttl*1000) {
mediaConn = (async() => { mediaConn = (async() => {
const { media_conn } = await query({ const { media_conn } = await query({
@@ -39,6 +39,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
return media_conn as MediaConnInfo return media_conn as MediaConnInfo
})() })()
} }
return mediaConn return mediaConn
} }
@@ -51,6 +52,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
if(cursor) { if(cursor) {
key = 'before' in cursor ? cursor.before : cursor.after key = 'before' in cursor ? cursor.before : cursor.after
} }
const { content }:BinaryNode = await query({ const { content }:BinaryNode = await query({
json: { json: {
tag: 'query', tag: 'query',
@@ -71,15 +73,18 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
if(Array.isArray(content)) { if(Array.isArray(content)) {
return content.map(data => proto.WebMessageInfo.decode(data.content as Buffer)) return content.map(data => proto.WebMessageInfo.decode(data.content as Buffer))
} }
return [] return []
} }
const updateMediaMessage = async(message: WAMessage) => { const updateMediaMessage = async(message: WAMessage) => {
const content = message.message?.audioMessage || message.message?.videoMessage || message.message?.imageMessage || message.message?.stickerMessage || message.message?.documentMessage const content = message.message?.audioMessage || message.message?.videoMessage || message.message?.imageMessage || message.message?.stickerMessage || message.message?.documentMessage
if (!content) throw new Boom( if(!content) {
throw new Boom(
`given message ${message.key.id} is not a media message`, `given message ${message.key.id} is not a media message`,
{ statusCode: 400, data: message } { statusCode: 400, data: message }
) )
}
const response: BinaryNode = await query ({ const response: BinaryNode = await query ({
json: { json: {
@@ -154,6 +159,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
if(isJidGroup(jid)) { if(isJidGroup(jid)) {
emitGroupUpdate({ ephemeralDuration: protocolMessage.ephemeralExpiration || null }) emitGroupUpdate({ ephemeralDuration: protocolMessage.ephemeralExpiration || null })
} }
break break
default: default:
break break
@@ -176,6 +182,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
if(isJidGroup(jid)) { if(isJidGroup(jid)) {
emitGroupUpdate({ ephemeralDuration: +message.messageStubParameters[0] || null }) emitGroupUpdate({ ephemeralDuration: +message.messageStubParameters[0] || null })
} }
break break
case WAMessageStubType.GROUP_PARTICIPANT_LEAVE: case WAMessageStubType.GROUP_PARTICIPANT_LEAVE:
case WAMessageStubType.GROUP_PARTICIPANT_REMOVE: case WAMessageStubType.GROUP_PARTICIPANT_REMOVE:
@@ -185,6 +192,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
if(participants.includes(user.id)) { if(participants.includes(user.id)) {
chatUpdate.readOnly = true chatUpdate.readOnly = true
} }
break break
case WAMessageStubType.GROUP_PARTICIPANT_ADD: case WAMessageStubType.GROUP_PARTICIPANT_ADD:
case WAMessageStubType.GROUP_PARTICIPANT_INVITE: case WAMessageStubType.GROUP_PARTICIPANT_INVITE:
@@ -193,6 +201,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
if(participants.includes(user.id)) { if(participants.includes(user.id)) {
chatUpdate.readOnly = null chatUpdate.readOnly = null
} }
emitParticipantsUpdate('add') emitParticipantsUpdate('add')
break break
case WAMessageStubType.GROUP_CHANGE_ANNOUNCE: case WAMessageStubType.GROUP_CHANGE_ANNOUNCE:
@@ -239,6 +248,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
if(response && response.content) { if(response && response.content) {
urlInfo.jpegThumbnail = response.content as Buffer urlInfo.jpegThumbnail = response.content as Buffer
} }
return urlInfo return urlInfo
} }
@@ -277,10 +287,12 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
message.status = status message.status = status
ev.emit('messages.update', [ { key: message.key, update: { status } } ]) ev.emit('messages.update', [ { key: message.key, update: { status } } ])
} }
promise promise
.then(() => emitUpdate(finalState)) .then(() => emitUpdate(finalState))
.catch(() => emitUpdate(WAMessageStatus.ERROR)) .catch(() => emitUpdate(WAMessageStatus.ERROR))
} }
if(config.emitOwnEvents) { if(config.emitOwnEvents) {
onMessage(message, 'append') onMessage(message, 'append')
} }
@@ -330,14 +342,17 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
logger.warn({ content, key }, 'got unknown status update for message') logger.warn({ content, key }, 'got unknown status update for message')
} }
} }
ev.emit('messages.update', updates) ev.emit('messages.update', updates)
} }
} }
const onMessageInfoUpdate = ([, attributes]: [string, {[_: string]: any}]) => { const onMessageInfoUpdate = ([, attributes]: [string, {[_: string]: any}]) => {
let ids = attributes.id as string[] | string let ids = attributes.id as string[] | string
if(typeof ids === 'string') { if(typeof ids === 'string') {
ids = [ids] ids = [ids]
} }
let updateKey: keyof MessageInfoUpdate['update'] let updateKey: keyof MessageInfoUpdate['update']
switch (attributes.ack.toString()) { switch (attributes.ack.toString()) {
case '2': case '2':
@@ -347,9 +362,10 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
updateKey = 'reads' updateKey = 'reads'
break break
default: default:
logger.warn({ attributes }, `received unknown message info update`) logger.warn({ attributes }, 'received unknown message info update')
return return
} }
const keyPartial = { const keyPartial = {
remoteJid: jidNormalizedUser(attributes.to), remoteJid: jidNormalizedUser(attributes.to),
fromMe: areJidsSameUser(attributes.from, state.legacy?.user?.id || ''), fromMe: areJidsSameUser(attributes.from, state.legacy?.user?.id || ''),
@@ -416,12 +432,15 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
} }
} }
} }
return info return info
}, },
downloadMediaMessage: async(message: WAMessage, type: 'buffer' | 'stream' = 'buffer') => { downloadMediaMessage: async(message: WAMessage, type: 'buffer' | 'stream' = 'buffer') => {
const downloadMediaMessage = async() => { const downloadMediaMessage = async() => {
let mContent = extractMessageContent(message.message) const mContent = extractMessageContent(message.message)
if (!mContent) throw new Boom('No message present', { statusCode: 400, data: message }) if(!mContent) {
throw new Boom('No message present', { statusCode: 400, data: message })
}
const stream = await decryptMediaMessageBuffer(mContent) const stream = await decryptMediaMessageBuffer(mContent)
if(type === 'buffer') { if(type === 'buffer') {
@@ -429,8 +448,10 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
for await (const chunk of stream) { for await (const chunk of stream) {
buffer = Buffer.concat([buffer, chunk]) buffer = Buffer.concat([buffer, chunk])
} }
return buffer return buffer
} }
return stream return stream
} }
@@ -446,6 +467,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
const result = await downloadMediaMessage() const result = await downloadMediaMessage()
return result return result
} }
throw error throw error
} }
}, },
@@ -453,15 +475,15 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
fetchMessagesFromWA, fetchMessagesFromWA,
/** Load a single message specified by the ID */ /** Load a single message specified by the ID */
loadMessageFromWA: async(jid: string, id: string) => { loadMessageFromWA: async(jid: string, id: string) => {
let message: WAMessage
// load the message before the given message // load the message before the given message
let messages = (await fetchMessagesFromWA(jid, 1, { before: { id, fromMe: true } })) let messages = (await fetchMessagesFromWA(jid, 1, { before: { id, fromMe: true } }))
if(!messages[0]) messages = (await fetchMessagesFromWA(jid, 1, { before: {id, fromMe: false} })) if(!messages[0]) {
messages = (await fetchMessagesFromWA(jid, 1, { before: { id, fromMe: false } }))
}
// the message after the loaded message is the message required // the message after the loaded message is the message required
const [actual] = await fetchMessagesFromWA(jid, 1, { after: messages[0] && messages[0].key }) const [actual] = await fetchMessagesFromWA(jid, 1, { after: messages[0] && messages[0].key })
message = actual return actual
return message
}, },
searchMessages: async(txt: string, inJid: string | null, count: number, page: number) => { searchMessages: async(txt: string, inJid: string | null, count: number, page: number) => {
const node: BinaryNode = await query({ const node: BinaryNode = await query({

View File

@@ -1,11 +1,11 @@
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import { STATUS_CODES } from "http" import { STATUS_CODES } from 'http'
import { promisify } from "util" import { promisify } from 'util'
import WebSocket from "ws" import WebSocket from 'ws'
import { BinaryNode, encodeBinaryNodeLegacy } from "../WABinary" import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, DEFAULT_ORIGIN, PHONE_CONNECTION_CB } from '../Defaults'
import { DisconnectReason, LegacySocketConfig, SocketQueryOptions, SocketSendMessageOptions, WAFlag, WAMetric, WATag } from "../Types" import { DisconnectReason, LegacySocketConfig, SocketQueryOptions, SocketSendMessageOptions, WAFlag, WAMetric, WATag } from '../Types'
import { aesEncrypt, hmacSign, promiseTimeout, unixTimestampSeconds, decodeWAMessage } from "../Utils" import { aesEncrypt, decodeWAMessage, hmacSign, promiseTimeout, unixTimestampSeconds } from '../Utils'
import { DEFAULT_ORIGIN, DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, PHONE_CONNECTION_CB } from "../Defaults" import { BinaryNode, encodeBinaryNodeLegacy } from '../WABinary'
/** /**
* Connects to WA servers and performs: * Connects to WA servers and performs:
@@ -57,6 +57,7 @@ export const makeSocket = ({
epoch += 1 // increment message count, it makes the 'epoch' field when sending binary messages epoch += 1 // increment message count, it makes the 'epoch' field when sending binary messages
return tag return tag
} }
const sendRawMessage = (data: Buffer | string) => { const sendRawMessage = (data: Buffer | string) => {
if(ws.readyState !== ws.OPEN) { if(ws.readyState !== ws.OPEN) {
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
@@ -64,6 +65,7 @@ export const makeSocket = ({
return sendPromise.call(ws, data) as Promise<void> return sendPromise.call(ws, data) as Promise<void>
} }
/** /**
* Send a message to the WA servers * Send a message to the WA servers
* @returns the tag attached in the message * @returns the tag attached in the message
@@ -81,9 +83,11 @@ export const makeSocket = ({
if(Array.isArray(json)) { if(Array.isArray(json)) {
throw new Boom('Expected BinaryNode with binary code', { statusCode: 400 }) throw new Boom('Expected BinaryNode with binary code', { statusCode: 400 })
} }
if(!authInfo) { if(!authInfo) {
throw new Boom('No encryption/mac keys to encrypt node with', { statusCode: 400 }) throw new Boom('No encryption/mac keys to encrypt node with', { statusCode: 400 })
} }
const binary = encodeBinaryNodeLegacy(json) // encode the JSON to the WhatsApp binary format const binary = encodeBinaryNodeLegacy(json) // encode the JSON to the WhatsApp binary format
const buff = aesEncrypt(binary, authInfo.encKey) // encrypt it using AES and our encKey const buff = aesEncrypt(binary, authInfo.encKey) // encrypt it using AES and our encKey
@@ -98,9 +102,11 @@ export const makeSocket = ({
} else { } else {
data = `${tag},${JSON.stringify(json)}` data = `${tag},${JSON.stringify(json)}`
} }
await sendRawMessage(data) await sendRawMessage(data)
return tag return tag
} }
const end = (error: Error | undefined) => { const end = (error: Error | undefined) => {
logger.info({ error }, 'connection closed') logger.info({ error }, 'connection closed')
@@ -114,12 +120,15 @@ export const makeSocket = ({
clearPhoneCheckInterval() clearPhoneCheckInterval()
if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) { if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {
try { ws.close() } catch { } try {
ws.close()
} catch{ }
} }
ws.emit('ws-close', error) ws.emit('ws-close', error)
ws.removeAllListeners('ws-close') ws.removeAllListeners('ws-close')
} }
const onMessageRecieved = (message: string | Buffer) => { const onMessageRecieved = (message: string | Buffer) => {
if(message[0] === '!' || message[0] === '!'.charCodeAt(0)) { if(message[0] === '!' || message[0] === '!'.charCodeAt(0)) {
// when the first character in the message is an '!', the server is sending a pong frame // when the first character in the message is an '!', the server is sending a pong frame
@@ -133,7 +142,9 @@ export const makeSocket = ({
const dec = decodeWAMessage(message, authInfo) const dec = decodeWAMessage(message, authInfo)
messageTag = dec[0] messageTag = dec[0]
json = dec[1] json = dec[1]
if (!json) return if(!json) {
return
}
} catch(error) { } catch(error) {
end(error) end(error)
return return
@@ -172,18 +183,20 @@ export const makeSocket = ({
const listener = ([, connected]) => { const listener = ([, connected]) => {
if(connected) { if(connected) {
timeout = setTimeout(() => { timeout = setTimeout(() => {
logger.info({ tag }, `cancelling wait for message as a response is no longer expected from the phone`) logger.info({ tag }, 'cancelling wait for message as a response is no longer expected from the phone')
cancel(new Boom('Not expecting a response', { statusCode: 422 })) cancel(new Boom('Not expecting a response', { statusCode: 422 }))
}, expectResponseTimeout) }, expectResponseTimeout)
ws.off(PHONE_CONNECTION_CB, listener) ws.off(PHONE_CONNECTION_CB, listener)
} }
} }
ws.on(PHONE_CONNECTION_CB, listener) ws.on(PHONE_CONNECTION_CB, listener)
return () => { return () => {
ws.off(PHONE_CONNECTION_CB, listener) ws.off(PHONE_CONNECTION_CB, listener)
timeout && clearTimeout(timeout) timeout && clearTimeout(timeout)
} }
} }
/** interval is started when a query takes too long to respond */ /** interval is started when a query takes too long to respond */
const startPhoneCheckInterval = () => { const startPhoneCheckInterval = () => {
phoneCheckListeners += 1 phoneCheckListeners += 1
@@ -194,6 +207,7 @@ export const makeSocket = ({
logger.warn('phone check called without listeners') logger.warn('phone check called without listeners')
return return
} }
logger.info('checking phone connection...') logger.info('checking phone connection...')
sendAdminTest() sendAdminTest()
@@ -201,6 +215,7 @@ export const makeSocket = ({
}, phoneResponseTimeMs) }, phoneResponseTimeMs)
} }
} }
const clearPhoneCheckInterval = () => { const clearPhoneCheckInterval = () => {
phoneCheckListeners -= 1 phoneCheckListeners -= 1
if(phoneCheckListeners <= 0) { if(phoneCheckListeners <= 0) {
@@ -209,6 +224,7 @@ export const makeSocket = ({
phoneCheckListeners = 0 phoneCheckListeners = 0
} }
} }
/** checks for phone connection */ /** checks for phone connection */
const sendAdminTest = () => sendNode({ json: ['admin', 'test'] }) const sendAdminTest = () => sendNode({ json: ['admin', 'test'] })
/** /**
@@ -236,6 +252,7 @@ export const makeSocket = ({
onErr = err => { onErr = err => {
reject(err || new Boom('Intentional Close', { statusCode: DisconnectReason.connectionClosed })) reject(err || new Boom('Intentional Close', { statusCode: DisconnectReason.connectionClosed }))
} }
cancelToken = () => onErr(new Boom('Cancelled', { statusCode: 500 })) cancelToken = () => onErr(new Boom('Cancelled', { statusCode: 500 }))
if(requiresPhoneConnection) { if(requiresPhoneConnection) {
@@ -256,9 +273,12 @@ export const makeSocket = ({
ws.off('ws-close', onErr) // if the socket closes, you'll never receive the message ws.off('ws-close', onErr) // if the socket closes, you'll never receive the message
} }
})(), })(),
cancelToken: () => { cancelToken() } cancelToken: () => {
cancelToken()
} }
} }
}
/** /**
* Query something from the WhatsApp servers * Query something from the WhatsApp servers
* @param json the query itself * @param json the query itself
@@ -287,6 +307,7 @@ export const makeSocket = ({
if(responseStatusCode === 599) { // the connection has gone bad if(responseStatusCode === 599) { // the connection has gone bad
end(new Boom('WA server overloaded', { statusCode: 599, data: { query: json, response } })) end(new Boom('WA server overloaded', { statusCode: 599, data: { query: json, response } }))
} }
if(expect200 && Math.floor(responseStatusCode/100) !== 2) { if(expect200 && Math.floor(responseStatusCode/100) !== 2) {
const message = STATUS_CODES[responseStatusCode] || 'unknown' const message = STATUS_CODES[responseStatusCode] || 'unknown'
throw new Boom( throw new Boom(
@@ -294,11 +315,16 @@ export const makeSocket = ({
{ data: { query: json, response }, statusCode: response.status } { data: { query: json, response }, statusCode: response.status }
) )
} }
return response return response
} }
const startKeepAliveRequest = () => ( const startKeepAliveRequest = () => (
keepAliveReq = setInterval(() => { keepAliveReq = setInterval(() => {
if (!lastDateRecv) lastDateRecv = new Date() if(!lastDateRecv) {
lastDateRecv = new Date()
}
const diff = Date.now() - lastDateRecv.getTime() const diff = Date.now() - lastDateRecv.getTime()
/* /*
check if it's been a suspicious amount of time since the server responded with our last seen check if it's been a suspicious amount of time since the server responded with our last seen
@@ -315,10 +341,14 @@ export const makeSocket = ({
) )
const waitForSocketOpen = async() => { const waitForSocketOpen = async() => {
if(ws.readyState === ws.OPEN) return if(ws.readyState === ws.OPEN) {
return
}
if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) { if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
throw new Boom('Connection Already Closed', { statusCode: DisconnectReason.connectionClosed }) throw new Boom('Connection Already Closed', { statusCode: DisconnectReason.connectionClosed })
} }
let onOpen: () => void let onOpen: () => void
let onClose: (err: Error) => void let onClose: (err: Error) => void
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
@@ -362,6 +392,7 @@ export const makeSocket = ({
reason = DisconnectReason.connectionLost reason = DisconnectReason.connectionLost
break break
} }
end(new Boom( end(new Boom(
`Connection terminated by server: "${kind || 'unknown'}"`, `Connection terminated by server: "${kind || 'unknown'}"`,
{ statusCode: reason } { statusCode: reason }

View File

@@ -1,10 +1,10 @@
import { SocketConfig, WAPresence, PresenceData, Chat, WAPatchCreate, WAMediaUpload, ChatMutation, WAPatchName, AppStateChunk, LTHashState, ChatModification, Contact, WABusinessProfile, WABusinessHoursConfig } from "../Types"; import { Boom } from '@hapi/boom'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidNormalizedUser, S_WHATSAPP_NET, reduceBinaryNodeToDictionary } from "../WABinary";
import { proto } from '../../WAProto' import { proto } from '../../WAProto'
import { generateProfilePicture, toNumber, encodeSyncdPatch, decodePatches, extractSyncdPatches, chatModificationToAppPatch, decodeSyncdSnapshot, newLTHashState } from "../Utils"; import { AppStateChunk, Chat, ChatModification, ChatMutation, Contact, LTHashState, PresenceData, SocketConfig, WABusinessHoursConfig, WABusinessProfile, WAMediaUpload, WAPatchCreate, WAPatchName, WAPresence } from '../Types'
import { makeMessagesSocket } from "./messages-send"; import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, newLTHashState, toNumber } from '../Utils'
import makeMutex from "../Utils/make-mutex"; import makeMutex from '../Utils/make-mutex'
import { Boom } from "@hapi/boom"; import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary'
import { makeMessagesSocket } from './messages-send'
const MAX_SYNC_ATTEMPTS = 5 const MAX_SYNC_ATTEMPTS = 5
@@ -179,9 +179,13 @@ export const makeChatsSocket = (config: SocketConfig) => {
const profiles = getBinaryNodeChild(getBinaryNodeChild(results, 'business_profile'), 'profile') const profiles = getBinaryNodeChild(getBinaryNodeChild(results, 'business_profile'), 'profile')
if(!profiles) { if(!profiles) {
// if not bussines // if not bussines
if (logger.level == 'trace') logger.trace({ jid }, 'Not bussines') if(logger.level === 'trace') {
logger.trace({ jid }, 'Not bussines')
}
return return
} }
const address = getBinaryNodeChild(profiles, 'address') const address = getBinaryNodeChild(profiles, 'address')
const description = getBinaryNodeChild(profiles, 'description') const description = getBinaryNodeChild(profiles, 'description')
const website = getBinaryNodeChild(profiles, 'website') const website = getBinaryNodeChild(profiles, 'website')
@@ -301,6 +305,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
appStateChunk.totalMutations.push(...mutations) appStateChunk.totalMutations.push(...mutations)
} }
// only process if there are syncd patches // only process if there are syncd patches
if(patches.length) { if(patches.length) {
const { newMutations, state: newState } = await decodePatches(name, patches, states[name], getAppStateSyncKey, initialVersionMap[name]) const { newMutations, state: newState } = await decodePatches(name, patches, states[name], getAppStateSyncKey, initialVersionMap[name])
@@ -314,6 +319,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
appStateChunk.totalMutations.push(...newMutations) appStateChunk.totalMutations.push(...newMutations)
} }
if(hasMorePatches) { if(hasMorePatches) {
logger.info(`${name} has more patches...`) logger.info(`${name} has more patches...`)
} else { // collection is done with sync } else { // collection is done with sync
@@ -321,7 +327,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
} }
} catch(error) { } catch(error) {
logger.info({ name, error: error.stack }, 'failed to sync state from version, removing and trying from scratch') logger.info({ name, error: error.stack }, 'failed to sync state from version, removing and trying from scratch')
await authState.keys.set({ "app-state-sync-version": { [name]: null } }) await authState.keys.set({ 'app-state-sync-version': { [name]: null } })
// increment number of retries // increment number of retries
attemptsMap[name] = (attemptsMap[name] || 0) + 1 attemptsMap[name] = (attemptsMap[name] || 0) + 1
// if retry attempts overshoot // if retry attempts overshoot
@@ -413,10 +419,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
if(type === 'paused') { if(type === 'paused') {
type = 'available' type = 'available'
} }
presence = { lastKnownPresence: type } presence = { lastKnownPresence: type }
} else { } else {
logger.error({ tag, attrs, content }, 'recv invalid presence node') logger.error({ tag, attrs, content }, 'recv invalid presence node')
} }
if(presence) { if(presence) {
ev.emit('presence.update', { id: jid, presences: { [participant]: presence } }) ev.emit('presence.update', { id: jid, presences: { [participant]: presence } })
} }
@@ -492,9 +500,11 @@ export const makeChatsSocket = (config: SocketConfig) => {
if(Object.values(updates).length) { if(Object.values(updates).length) {
ev.emit('chats.update', Object.values(updates)) ev.emit('chats.update', Object.values(updates))
} }
if(Object.values(contactUpdates).length) { if(Object.values(contactUpdates).length) {
ev.emit('contacts.upsert', Object.values(contactUpdates)) ev.emit('contacts.upsert', Object.values(contactUpdates))
} }
if(msgDeletes.length) { if(msgDeletes.length) {
ev.emit('messages.delete', { keys: msgDeletes }) ev.emit('messages.delete', { keys: msgDeletes })
} }
@@ -504,7 +514,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
const name = patchCreate.type const name = patchCreate.type
const myAppStateKeyId = authState.creds.myAppStateKeyId const myAppStateKeyId = authState.creds.myAppStateKeyId
if(!myAppStateKeyId) { if(!myAppStateKeyId) {
throw new Boom(`App state key not present!`, { statusCode: 400 }) throw new Boom('App state key not present!', { statusCode: 400 })
} }
await mutationMutex.mutex( await mutationMutex.mutex(
@@ -562,6 +572,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
} }
) )
} }
/** sending abt props may fix QR scan fail if server expects */ /** sending abt props may fix QR scan fail if server expects */
const fetchAbt = async() => { const fetchAbt = async() => {
const abtNode = await query({ const abtNode = await query({
@@ -583,10 +594,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
if(propsNode) { if(propsNode) {
props = reduceBinaryNodeToDictionary(propsNode, 'prop') props = reduceBinaryNodeToDictionary(propsNode, 'prop')
} }
logger.debug('fetched abt') logger.debug('fetched abt')
return props return props
} }
/** sending non-abt props may fix QR scan fail if server expects */ /** sending non-abt props may fix QR scan fail if server expects */
const fetchProps = async() => { const fetchProps = async() => {
const resultNode = await query({ const resultNode = await query({
@@ -608,10 +621,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
if(propsNode) { if(propsNode) {
props = reduceBinaryNodeToDictionary(propsNode, 'prop') props = reduceBinaryNodeToDictionary(propsNode, 'prop')
} }
logger.debug('fetched props') logger.debug('fetched props')
return props return props
} }
/** /**
* modify a chat -- mark unread, read etc. * modify a chat -- mark unread, read etc.
* lastMessages must be sorted in reverse chronologically * lastMessages must be sorted in reverse chronologically
@@ -634,11 +649,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
if(lastAccountSyncTimestamp) { if(lastAccountSyncTimestamp) {
await updateAccountSyncTimestamp(lastAccountSyncTimestamp) await updateAccountSyncTimestamp(lastAccountSyncTimestamp)
} }
lastAccountSyncTimestamp = +attrs.timestamp lastAccountSyncTimestamp = +attrs.timestamp
ev.emit('creds.update', { lastAccountSyncTimestamp }) ev.emit('creds.update', { lastAccountSyncTimestamp })
break break
default: default:
logger.info({ node }, `received unknown sync`) logger.info({ node }, 'received unknown sync')
break break
} }
}) })
@@ -650,7 +666,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
mutationMutex.mutex( mutationMutex.mutex(
async() => { async() => {
await resyncAppState([name]) await resyncAppState([name])
.catch(err => logger.error({ trace: err.stack, node }, `failed to sync state`)) .catch(err => logger.error({ trace: err.stack, node }, 'failed to sync state'))
} }
) )
} }

View File

@@ -1,7 +1,7 @@
import { generateMessageID } from "../Utils"; import { GroupMetadata, ParticipantAction, SocketConfig } from '../Types'
import { SocketConfig, GroupMetadata, ParticipantAction } from "../Types"; import { generateMessageID } from '../Utils'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidEncode, jidNormalizedUser } from "../WABinary"; import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidEncode, jidNormalizedUser } from '../WABinary'
import { makeSocket } from "./socket"; import { makeSocket } from './socket'
export const makeGroupsSocket = (config: SocketConfig) => { export const makeGroupsSocket = (config: SocketConfig) => {
const sock = makeSocket(config) const sock = makeSocket(config)
@@ -101,8 +101,8 @@ export const makeGroupsSocket = (config: SocketConfig) => {
return participantsAffected.map(p => p.attrs.jid) return participantsAffected.map(p => p.attrs.jid)
}, },
groupUpdateDescription: async(jid: string, description?: string) => { groupUpdateDescription: async(jid: string, description?: string) => {
const metadata = await groupMetadata(jid); const metadata = await groupMetadata(jid)
const prev = metadata.descId ?? null; const prev = metadata.descId ?? null
await groupQuery( await groupQuery(
jid, jid,
@@ -175,6 +175,7 @@ export const makeGroupsSocket = (config: SocketConfig) => {
data[meta.id] = meta data[meta.id] = meta
} }
} }
return data return data
} }
} }
@@ -190,6 +191,7 @@ export const extractGroupMetadata = (result: BinaryNode) => {
desc = getBinaryNodeChild(descChild, 'body')?.content as string desc = getBinaryNodeChild(descChild, 'body')?.content as string
descId = descChild.attrs.id 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 eph = getBinaryNodeChild(group, 'ephemeral')?.attrs.expiration
const metadata: GroupMetadata = { const metadata: GroupMetadata = {

View File

@@ -1,5 +1,5 @@
import { SocketConfig } from '../Types'
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults' import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
import { SocketConfig } from '../Types'
import { makeMessagesRecvSocket as _makeSocket } from './messages-recv' import { makeMessagesRecvSocket as _makeSocket } from './messages-recv'
// export the last socket layer // export the last socket layer

View File

@@ -1,11 +1,11 @@
import { SocketConfig, WAMessageStubType, ParticipantAction, Chat, GroupMetadata } from "../Types" import { proto } from '../../WAProto'
import { decodeMessageStanza, encodeBigEndian, toNumber, downloadAndProcessHistorySyncNotification, generateSignalPubKey, xmppPreKey, xmppSignedPreKey } from "../Utils" import { KEY_BUNDLE_TYPE } from '../Defaults'
import { BinaryNode, jidDecode, jidEncode, areJidsSameUser, getBinaryNodeChildren, jidNormalizedUser, getAllBinaryNodeChildren, BinaryNodeAttributes, isJidGroup } from '../WABinary' import { Chat, GroupMetadata, ParticipantAction, SocketConfig, WAMessageStubType } from '../Types'
import { proto } from "../../WAProto" import { decodeMessageStanza, downloadAndProcessHistorySyncNotification, encodeBigEndian, generateSignalPubKey, toNumber, xmppPreKey, xmppSignedPreKey } from '../Utils'
import { KEY_BUNDLE_TYPE } from "../Defaults" import { areJidsSameUser, BinaryNode, BinaryNodeAttributes, getAllBinaryNodeChildren, getBinaryNodeChildren, isJidGroup, jidDecode, jidEncode, jidNormalizedUser } from '../WABinary'
import { makeChatsSocket } from "./chats" import { makeChatsSocket } from './chats'
import { extractGroupMetadata } from "./groups" import { extractGroupMetadata } from './groups'
const STATUS_MAP: { [_: string]: proto.WebMessageInfo.WebMessageInfoStatus } = { const STATUS_MAP: { [_: string]: proto.WebMessageInfo.WebMessageInfoStatus } = {
'played': proto.WebMessageInfo.WebMessageInfoStatus.PLAYED, 'played': proto.WebMessageInfo.WebMessageInfoStatus.PLAYED,
@@ -18,6 +18,7 @@ const getStatusFromReceiptType = (type: string | undefined) => {
if(typeof type === 'undefined') { if(typeof type === 'undefined') {
return proto.WebMessageInfo.WebMessageInfoStatus.DELIVERY_ACK return proto.WebMessageInfo.WebMessageInfoStatus.DELIVERY_ACK
} }
return status return status
} }
@@ -52,6 +53,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
if(!!attrs.participant) { if(!!attrs.participant) {
stanza.attrs.participant = attrs.participant stanza.attrs.participant = attrs.participant
} }
logger.debug({ recv: attrs, sent: stanza.attrs }, `sent "${tag}" ack`) logger.debug({ recv: attrs, sent: stanza.attrs }, `sent "${tag}" ack`)
await sendNode(stanza) await sendNode(stanza)
} }
@@ -64,6 +66,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
delete msgRetryMap[msgId] delete msgRetryMap[msgId]
return return
} }
msgRetryMap[msgId] = retryCount+1 msgRetryMap[msgId] = retryCount+1
const isGroup = !!node.attrs.participant const isGroup = !!node.attrs.participant
@@ -102,9 +105,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
if(node.attrs.recipient) { if(node.attrs.recipient) {
receipt.attrs.recipient = node.attrs.recipient receipt.attrs.recipient = node.attrs.recipient
} }
if(node.attrs.participant) { if(node.attrs.participant) {
receipt.attrs.participant = node.attrs.participant receipt.attrs.participant = node.attrs.participant
} }
if(retryCount > 1) { if(retryCount > 1) {
const exec = generateSignalPubKey(Buffer.from(KEY_BUNDLE_TYPE)).slice(0, 1); const exec = generateSignalPubKey(Buffer.from(KEY_BUNDLE_TYPE)).slice(0, 1);
@@ -120,6 +125,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
] ]
}) })
} }
await sendNode(receipt) await sendNode(receipt)
logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt') logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt')
@@ -146,9 +152,17 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
} }
}) })
if(chats.length) ev.emit('chats.set', { chats, isLatest }) if(chats.length) {
if(messages.length) ev.emit('messages.set', { messages, isLatest }) ev.emit('chats.set', { chats, isLatest })
if(contacts.length) ev.emit('contacts.set', { contacts }) }
if(messages.length) {
ev.emit('messages.set', { messages, isLatest })
}
if(contacts.length) {
ev.emit('contacts.set', { contacts })
}
break break
case proto.ProtocolMessage.ProtocolMessageType.APP_STATE_SYNC_KEY_SHARE: case proto.ProtocolMessage.ProtocolMessageType.APP_STATE_SYNC_KEY_SHARE:
@@ -167,9 +181,12 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId }) ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId })
resyncMainAppState() resyncMainAppState()
} else [ } else {
[
logger.info({ protocolMsg }, 'recv app state sync with 0 keys') logger.info({ protocolMsg }, 'recv app state sync with 0 keys')
] ]
}
break break
case proto.ProtocolMessage.ProtocolMessageType.REVOKE: case proto.ProtocolMessage.ProtocolMessageType.REVOKE:
ev.emit('messages.update', [ ev.emit('messages.update', [
@@ -208,6 +225,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
if(participants.includes(meJid)) { if(participants.includes(meJid)) {
chatUpdate.readOnly = true chatUpdate.readOnly = true
} }
break break
case WAMessageStubType.GROUP_PARTICIPANT_ADD: case WAMessageStubType.GROUP_PARTICIPANT_ADD:
case WAMessageStubType.GROUP_PARTICIPANT_INVITE: case WAMessageStubType.GROUP_PARTICIPANT_INVITE:
@@ -216,6 +234,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
if(participants.includes(meJid)) { if(participants.includes(meJid)) {
chatUpdate.readOnly = false chatUpdate.readOnly = false
} }
emitParticipantsUpdate('add') emitParticipantsUpdate('add')
break break
case WAMessageStubType.GROUP_CHANGE_ANNOUNCE: case WAMessageStubType.GROUP_CHANGE_ANNOUNCE:
@@ -281,6 +300,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
) { ) {
result.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE result.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE
} }
result.messageStubParameters = participants result.messageStubParameters = participants
break break
case 'subject': case 'subject':
@@ -307,13 +327,16 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const deviceJids = devices.map(d => d.attrs.jid) const deviceJids = devices.map(d => d.attrs.jid)
logger.info({ deviceJids }, 'got my own devices') logger.info({ deviceJids }, 'got my own devices')
} }
break break
} }
} }
if(Object.keys(result).length) { if(Object.keys(result).length) {
return result return result
} }
} }
// recv a message // recv a message
ws.on('CB:message', async(stanza: BinaryNode) => { ws.on('CB:message', async(stanza: BinaryNode) => {
const msg = await decodeMessageStanza(stanza, authState) const msg = await decodeMessageStanza(stanza, authState)
@@ -487,9 +510,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
} }
} }
} }
if(Object.keys(chat).length > 1) { if(Object.keys(chat).length > 1) {
ev.emit('chats.update', [ chat ]) ev.emit('chats.update', [ chat ])
} }
if(Object.keys(contactNameUpdates).length) { if(Object.keys(contactNameUpdates).length) {
ev.emit('contacts.update', Object.keys(contactNameUpdates).map( ev.emit('contacts.update', Object.keys(contactNameUpdates).map(
id => ({ id, notify: contactNameUpdates[id] }) id => ({ id, notify: contactNameUpdates[id] })

View File

@@ -1,11 +1,11 @@
import { SocketConfig, MediaConnInfo, AnyMessageContent, MiscMessageGenerationOptions, WAMediaUploadFunction, MessageRelayOptions } from "../Types" import NodeCache from 'node-cache'
import { encodeWAMessage, generateMessageID, generateWAMessage, encryptSenderKeyMsgSignalProto, encryptSignalProto, extractDeviceJids, jidToSignalProtocolAddress, parseAndInjectE2ESessions, getWAUploadToServer } from "../Utils" import { proto } from '../../WAProto'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET, BinaryNodeAttributes, JidWithDevice, reduceBinaryNodeToDictionary } from '../WABinary' import { WA_DEFAULT_EPHEMERAL } from '../Defaults'
import { proto } from "../../WAProto" import { AnyMessageContent, MediaConnInfo, MessageRelayOptions, MiscMessageGenerationOptions, SocketConfig } from '../Types'
import { WA_DEFAULT_EPHEMERAL } from "../Defaults" import { encodeWAMessage, encryptSenderKeyMsgSignalProto, encryptSignalProto, extractDeviceJids, generateMessageID, generateWAMessage, getWAUploadToServer, jidToSignalProtocolAddress, parseAndInjectE2ESessions } from '../Utils'
import { makeGroupsSocket } from "./groups" import { BinaryNode, BinaryNodeAttributes, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidDecode, jidEncode, jidNormalizedUser, JidWithDevice, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary'
import NodeCache from "node-cache" import { makeGroupsSocket } from './groups'
export const makeMessagesSocket = (config: SocketConfig) => { export const makeMessagesSocket = (config: SocketConfig) => {
const { logger } = config const { logger } = config
@@ -41,12 +41,13 @@ export const makeMessagesSocket = (config: SocketConfig) => {
}) })
privacySettings = reduceBinaryNodeToDictionary(content[0] as BinaryNode, 'category') privacySettings = reduceBinaryNodeToDictionary(content[0] as BinaryNode, 'category')
} }
return privacySettings return privacySettings
} }
let mediaConn: Promise<MediaConnInfo> let mediaConn: Promise<MediaConnInfo>
const refreshMediaConn = async(forceGet = false) => { const refreshMediaConn = async(forceGet = false) => {
let media = await mediaConn const media = await mediaConn
if(!media || forceGet || (new Date().getTime()-media.fetchDate.getTime()) > media.ttl*1000) { if(!media || forceGet || (new Date().getTime()-media.fetchDate.getTime()) > media.ttl*1000) {
mediaConn = (async() => { mediaConn = (async() => {
const result = await query({ const result = await query({
@@ -71,8 +72,10 @@ export const makeMessagesSocket = (config: SocketConfig) => {
return node return node
})() })()
} }
return mediaConn return mediaConn
} }
/** /**
* generic send receipt function * generic send receipt function
* used for receipts of phone call, read, delivery etc. * used for receipts of phone call, read, delivery etc.
@@ -89,9 +92,11 @@ export const makeMessagesSocket = (config: SocketConfig) => {
if(type) { if(type) {
node.attrs.type = type node.attrs.type = type
} }
if(participant) { if(participant) {
node.attrs.participant = participant node.attrs.participant = participant
} }
const remainingMessageIds = messageIds.slice(1) const remainingMessageIds = messageIds.slice(1)
if(remainingMessageIds.length) { if(remainingMessageIds.length) {
node.content = [ node.content = [
@@ -202,7 +207,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
} }
if(jidsRequiringFetch.length) { if(jidsRequiringFetch.length) {
logger.debug({ jidsRequiringFetch }, `fetching sessions`) logger.debug({ jidsRequiringFetch }, 'fetching sessions')
const result = await query({ const result = await query({
tag: 'iq', tag: 'iq',
attrs: { attrs: {
@@ -226,6 +231,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
await parseAndInjectE2ESessions(result, authState) await parseAndInjectE2ESessions(result, authState)
return true return true
} }
return false return false
} }
@@ -291,7 +297,10 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const [groupData, senderKeyMap] = await Promise.all([ const [groupData, senderKeyMap] = await Promise.all([
(async() => { (async() => {
let groupData = cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined let groupData = cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
if(!groupData) groupData = await groupMetadata(jid) if(!groupData) {
groupData = await groupMetadata(jid)
}
return groupData return groupData
})(), })(),
(async() => { (async() => {
@@ -316,6 +325,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
senderKeyMap[jid] = true senderKeyMap[jid] = true
} }
} }
// if there are some participants with whom the session has not been established // if there are some participants with whom the session has not been established
// if there are, we re-send the senderkey // if there are, we re-send the senderkey
if(senderKeyJids.length) { if(senderKeyJids.length) {
@@ -363,8 +373,11 @@ export const makeMessagesSocket = (config: SocketConfig) => {
for(const { user, device } of devices) { for(const { user, device } of devices) {
const jid = jidEncode(user, 's.whatsapp.net', device) const jid = jidEncode(user, 's.whatsapp.net', device)
const isMe = user === meUser const isMe = user === meUser
if(isMe) meJids.push(jid) if(isMe) {
else otherJids.push(jid) meJids.push(jid)
} else {
otherJids.push(jid)
}
} }
const [meNodes, otherNodes] = await Promise.all([ const [meNodes, otherNodes] = await Promise.all([
@@ -472,6 +485,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
ev.emit('messages.upsert', { messages: [fullMsg], type: 'append' }) ev.emit('messages.upsert', { messages: [fullMsg], type: 'append' })
}) })
} }
return fullMsg return fullMsg
} }
} }

View File

@@ -1,13 +1,13 @@
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import EventEmitter from 'events'
import { promisify } from "util"
import WebSocket from "ws"
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
import EventEmitter from 'events'
import { promisify } from 'util'
import WebSocket from 'ws'
import { proto } from '../../WAProto' import { proto } from '../../WAProto'
import { DisconnectReason, SocketConfig, BaileysEventEmitter, AuthenticationCreds } from "../Types" import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, DEFAULT_ORIGIN, KEY_BUNDLE_TYPE } from '../Defaults'
import { Curve, generateRegistrationNode, configureSuccessfulPairing, generateLoginNode, encodeBigEndian, promiseTimeout, generateOrGetPreKeys, xmppSignedPreKey, xmppPreKey, getPreKeys, makeNoiseHandler, useSingleFileAuthState, addTransactionCapability, bindWaitForConnectionUpdate, printQRIfNecessaryListener } from "../Utils" import { AuthenticationCreds, BaileysEventEmitter, DisconnectReason, SocketConfig } from '../Types'
import { DEFAULT_ORIGIN, DEF_TAG_PREFIX, DEF_CALLBACK_PREFIX, KEY_BUNDLE_TYPE } from "../Defaults" import { addTransactionCapability, bindWaitForConnectionUpdate, configureSuccessfulPairing, Curve, encodeBigEndian, generateLoginNode, generateOrGetPreKeys, generateRegistrationNode, getPreKeys, makeNoiseHandler, printQRIfNecessaryListener, promiseTimeout, useSingleFileAuthState, xmppPreKey, xmppSignedPreKey } from '../Utils'
import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, S_WHATSAPP_NET, getBinaryNodeChild } from '../WABinary' import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, getBinaryNodeChild, S_WHATSAPP_NET } from '../WABinary'
/** /**
* Connects to WA servers and performs: * Connects to WA servers and performs:
@@ -56,6 +56,7 @@ export const makeSocket = ({
Please pass the credentials in the config itself Please pass the credentials in the config itself
`) `)
} }
const { creds } = authState const { creds } = authState
let lastDateRecv: Date let lastDateRecv: Date
@@ -72,19 +73,23 @@ export const makeSocket = ({
if(ws.readyState !== ws.OPEN) { if(ws.readyState !== ws.OPEN) {
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
} }
const bytes = noise.encodeFrame(data) const bytes = noise.encodeFrame(data)
await sendPromise.call(ws, bytes) as Promise<void> await sendPromise.call(ws, bytes) as Promise<void>
} }
/** send a binary node */ /** send a binary node */
const sendNode = (node: BinaryNode) => { const sendNode = (node: BinaryNode) => {
let buff = encodeBinaryNode(node) const buff = encodeBinaryNode(node)
return sendRawMessage(buff) return sendRawMessage(buff)
} }
/** await the next incoming message */ /** await the next incoming message */
const awaitNextMessage = async(sendMsg?: Uint8Array) => { const awaitNextMessage = async(sendMsg?: Uint8Array) => {
if(ws.readyState !== ws.OPEN) { if(ws.readyState !== ws.OPEN) {
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
} }
let onOpen: (data: any) => void let onOpen: (data: any) => void
let onClose: (err: Error) => void let onClose: (err: Error) => void
@@ -137,9 +142,12 @@ export const makeSocket = ({
ws.off('error', onErr) ws.off('error', onErr)
} }
} }
/** send a query, and wait for its response. auto-generates message ID if not provided */ /** send a query, and wait for its response. auto-generates message ID if not provided */
const query = async(node: BinaryNode, timeoutMs?: number) => { const query = async(node: BinaryNode, timeoutMs?: number) => {
if(!node.attrs.id) node.attrs.id = generateMessageTag() if(!node.attrs.id) {
node.attrs.id = generateMessageTag()
}
const msgId = node.attrs.id const msgId = node.attrs.id
const wait = waitForMessage(msgId, timeoutMs) const wait = waitForMessage(msgId, timeoutMs)
@@ -150,8 +158,10 @@ export const makeSocket = ({
if('tag' in result) { if('tag' in result) {
assertNodeErrorFree(result) assertNodeErrorFree(result)
} }
return result return result
} }
/** connection handshake */ /** connection handshake */
const validateConnection = async() => { const validateConnection = async() => {
logger.info('connected to WA Web') logger.info('connected to WA Web')
@@ -176,6 +186,7 @@ export const makeSocket = ({
logger.info('logging in...') logger.info('logging in...')
node = generateLoginNode(creds.me!.id, { version, browser }) node = generateLoginNode(creds.me!.id, { version, browser })
} }
const payloadEnc = noise.encrypt(node) const payloadEnc = noise.encrypt(node)
await sendRawMessage( await sendRawMessage(
proto.HandshakeMessage.encode({ proto.HandshakeMessage.encode({
@@ -188,6 +199,7 @@ export const makeSocket = ({
noise.finishInit() noise.finishInit()
startKeepAliveRequest() startKeepAliveRequest()
} }
/** get some pre-keys and do something with them */ /** get some pre-keys and do something with them */
const assertingPreKeys = async(range: number, execute: (keys: { [_: number]: any }) => Promise<void>) => { const assertingPreKeys = async(range: number, execute: (keys: { [_: number]: any }) => Promise<void>) => {
const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(authState.creds, range) const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(authState.creds, range)
@@ -207,6 +219,7 @@ export const makeSocket = ({
ev.emit('creds.update', update) ev.emit('creds.update', update)
} }
/** generates and uploads a set of pre-keys */ /** generates and uploads a set of pre-keys */
const uploadPreKeys = async() => { const uploadPreKeys = async() => {
await assertingPreKeys(30, async preKeys => { await assertingPreKeys(30, async preKeys => {
@@ -279,7 +292,9 @@ export const makeSocket = ({
ws.removeAllListeners('message') ws.removeAllListeners('message')
if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) { if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {
try { ws.close() } catch { } try {
ws.close()
} catch{ }
} }
ev.emit('connection.update', { ev.emit('connection.update', {
@@ -293,10 +308,14 @@ export const makeSocket = ({
} }
const waitForSocketOpen = async() => { const waitForSocketOpen = async() => {
if(ws.readyState === ws.OPEN) return if(ws.readyState === ws.OPEN) {
return
}
if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) { if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
} }
let onOpen: () => void let onOpen: () => void
let onClose: (err: Error) => void let onClose: (err: Error) => void
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
@@ -315,7 +334,10 @@ export const makeSocket = ({
const startKeepAliveRequest = () => ( const startKeepAliveRequest = () => (
keepAliveReq = setInterval(() => { keepAliveReq = setInterval(() => {
if (!lastDateRecv) lastDateRecv = new Date() if(!lastDateRecv) {
lastDateRecv = new Date()
}
const diff = Date.now() - lastDateRecv.getTime() const diff = Date.now() - lastDateRecv.getTime()
/* /*
check if it's been a suspicious amount of time since the server responded with our last seen check if it's been a suspicious amount of time since the server responded with our last seen
@@ -468,6 +490,7 @@ export const makeSocket = ({
if(!creds.serverHasPreKeys) { if(!creds.serverHasPreKeys) {
await uploadPreKeys() await uploadPreKeys()
} }
await sendPassiveIq('active') await sendPassiveIq('active')
logger.info('opened connection to WA') logger.info('opened connection to WA')
@@ -486,7 +509,7 @@ export const makeSocket = ({
}) })
ws.on('CB:stream:error', (node: BinaryNode) => { ws.on('CB:stream:error', (node: BinaryNode) => {
logger.error({ error: node }, `stream errored out`) logger.error({ error: node }, 'stream errored out')
const statusCode = +(node.attrs.code || DisconnectReason.restartRequired) const statusCode = +(node.attrs.code || DisconnectReason.restartRequired)
end(new Boom('Stream Errored', { statusCode, data: node })) end(new Boom('Stream Errored', { statusCode, data: node }))
@@ -536,4 +559,5 @@ export const makeSocket = ({
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev) waitForConnectionUpdate: bindWaitForConnectionUpdate(ev)
} }
} }
export type Socket = ReturnType<typeof makeSocket> export type Socket = ReturnType<typeof makeSocket>

View File

View File

@@ -1,7 +1,7 @@
import { MediaType, DownloadableMessage } from '../Types'
import { downloadContentFromMessage } from '../Utils'
import { proto } from '../../WAProto'
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import { proto } from '../../WAProto'
import { DownloadableMessage, MediaType } from '../Types'
import { downloadContentFromMessage } from '../Utils'
jest.setTimeout(20_000) jest.setTimeout(20_000)

View File

@@ -1,5 +1,5 @@
import type { Contact } from "./Contact" import type { proto } from '../../WAProto'
import type { proto } from "../../WAProto" import type { Contact } from './Contact'
export type KeyPair = { public: Uint8Array, private: Uint8Array } export type KeyPair = { public: Uint8Array, private: Uint8Array }
export type SignedKeyPair = { keyPair: KeyPair, signature: Uint8Array, keyId: number } export type SignedKeyPair = { keyPair: KeyPair, signature: Uint8Array, keyId: number }

View File

@@ -1,4 +1,4 @@
import type { proto } from "../../WAProto" import type { proto } from '../../WAProto'
/** set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send */ /** set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send */
export type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused' export type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'

View File

@@ -1,12 +1,10 @@
import type EventEmitter from "events" import type EventEmitter from 'events'
import { AuthenticationCreds } from './Auth' import { AuthenticationCreds } from './Auth'
import { Chat, PresenceData } from './Chat' import { Chat, PresenceData } from './Chat'
import { Contact } from './Contact' import { Contact } from './Contact'
import { ConnectionState } from './State'
import { GroupMetadata, ParticipantAction } from './GroupMetadata' import { GroupMetadata, ParticipantAction } from './GroupMetadata'
import { MessageInfoUpdate, MessageUpdateType, WAMessage, WAMessageUpdate, WAMessageKey } from './Message' import { MessageInfoUpdate, MessageUpdateType, WAMessage, WAMessageKey, WAMessageUpdate } from './Message'
import { ConnectionState } from './State'
export type BaileysEventMap<T> = { export type BaileysEventMap<T> = {
/** connection state has been updated -- WS closed, opened, connecting etc. */ /** connection state has been updated -- WS closed, opened, connecting etc. */

View File

@@ -1,4 +1,4 @@
import { Contact } from "./Contact"; import { Contact } from './Contact'
export type GroupParticipant = (Contact & { isAdmin?: boolean; isSuperAdmin?: boolean, admin?: 'admin' | 'superadmin' | null }) export type GroupParticipant = (Contact & { isAdmin?: boolean; isSuperAdmin?: boolean, admin?: 'admin' | 'superadmin' | null })

View File

@@ -1,6 +1,6 @@
import { CommonSocketConfig } from "./Socket" import { BinaryNode } from '../WABinary'
import { CommonBaileysEventEmitter } from "./Events" import { CommonBaileysEventEmitter } from './Events'
import { BinaryNode } from "../WABinary" import { CommonSocketConfig } from './Socket'
export interface LegacyAuthenticationCreds { export interface LegacyAuthenticationCreds {
clientID: string clientID: string

View File

@@ -1,10 +1,9 @@
import type { ReadStream } from "fs" import type NodeCache from 'node-cache'
import type { Logger } from "pino" import type { Logger } from 'pino'
import type { URL } from "url" import type { Readable } from 'stream'
import type NodeCache from "node-cache" import type { URL } from 'url'
import type { GroupMetadata } from "./GroupMetadata"
import type { Readable } from "stream"
import { proto } from '../../WAProto' import { proto } from '../../WAProto'
import type { GroupMetadata } from './GroupMetadata'
// export the WAMessage Prototypes // export the WAMessage Prototypes
export { proto as WAProto } export { proto as WAProto }

View File

@@ -1,9 +1,9 @@
import type { Agent } from "https" import type { Agent } from 'https'
import type { Logger } from "pino"
import type { URL } from "url"
import type NodeCache from 'node-cache' import type NodeCache from 'node-cache'
import { MediaConnInfo } from "./Message" import type { Logger } from 'pino'
import type { URL } from 'url'
import { MediaConnInfo } from './Message'
export type WAVersion = [number, number, number] export type WAVersion = [number, number, number]
export type WABrowserDescription = [string, string, string] export type WABrowserDescription = [string, string, string]

View File

@@ -1,4 +1,4 @@
import { Contact } from "./Contact" import { Contact } from './Contact'
export type WAConnectionState = 'open' | 'connecting' | 'close' export type WAConnectionState = 'open' | 'connecting' | 'close'

View File

@@ -9,9 +9,8 @@ export * from './Socket'
export * from './Events' export * from './Events'
import type NodeCache from 'node-cache' import type NodeCache from 'node-cache'
import { AuthenticationState } from './Auth'
import { proto } from '../../WAProto' import { proto } from '../../WAProto'
import { AuthenticationState } from './Auth'
import { CommonSocketConfig } from './Socket' import { CommonSocketConfig } from './Socket'
export type SocketConfig = CommonSocketConfig<AuthenticationState> & { export type SocketConfig = CommonSocketConfig<AuthenticationState> & {

View File

@@ -2,9 +2,9 @@ import { Boom } from '@hapi/boom'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
import type { Logger } from 'pino' import type { Logger } from 'pino'
import { proto } from '../../WAProto' import { proto } from '../../WAProto'
import type { AuthenticationCreds, AuthenticationState, SignalDataTypeMap, SignalDataSet, SignalKeyStore, SignalKeyStoreWithTransaction } from "../Types" import type { AuthenticationCreds, AuthenticationState, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction } from '../Types'
import { Curve, signedKeyPair } from './crypto' import { Curve, signedKeyPair } from './crypto'
import { generateRegistrationId, BufferJSON } from './generics' import { BufferJSON, generateRegistrationId } from './generics'
const KEY_MAP: { [T in keyof SignalDataTypeMap]: string } = { const KEY_MAP: { [T in keyof SignalDataTypeMap]: string } = {
'pre-key': 'preKeys', 'pre-key': 'preKeys',
@@ -46,6 +46,7 @@ export const addTransactionCapability = (state: SignalKeyStore, logger: Logger):
if(value) { if(value) {
dict[id] = value dict[id] = value
} }
return dict return dict
}, { } }, { }
) )
@@ -55,7 +56,7 @@ export const addTransactionCapability = (state: SignalKeyStore, logger: Logger):
}, },
set: data => { set: data => {
if(inTransaction) { if(inTransaction) {
logger.trace({ types: Object.keys(data) }, `caching in transaction`) logger.trace({ types: Object.keys(data) }, 'caching in transaction')
for(const key in data) { for(const key in data) {
transactionCache[key] = transactionCache[key] || { } transactionCache[key] = transactionCache[key] || { }
Object.assign(transactionCache[key], data[key]) Object.assign(transactionCache[key], data[key])
@@ -69,7 +70,7 @@ export const addTransactionCapability = (state: SignalKeyStore, logger: Logger):
}, },
isInTransaction: () => inTransaction, isInTransaction: () => inTransaction,
prefetch: (type, ids) => { prefetch: (type, ids) => {
logger.trace({ type, ids }, `prefetching`) logger.trace({ type, ids }, 'prefetching')
return prefetch(type, ids) return prefetch(type, ids)
}, },
transaction: async(work) => { transaction: async(work) => {
@@ -153,8 +154,10 @@ export const useSingleFileAuthState = (filename: string, logger?: Logger): { sta
if(type === 'app-state-sync-key') { if(type === 'app-state-sync-key') {
value = proto.AppStateSyncKeyData.fromObject(value) value = proto.AppStateSyncKeyData.fromObject(value)
} }
dict[id] = value dict[id] = value
} }
return dict return dict
}, { } }, { }
) )
@@ -165,6 +168,7 @@ export const useSingleFileAuthState = (filename: string, logger?: Logger): { sta
keys[key] = keys[key] || { } keys[key] = keys[key] || { }
Object.assign(keys[key], data[_key]) Object.assign(keys[key], data[_key])
} }
saveState() saveState()
} }
} }

View File

@@ -1,10 +1,10 @@
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import { aesDecrypt, hmacSign, aesEncrypt, hkdf } from "./crypto"
import { WAPatchCreate, ChatMutation, WAPatchName, LTHashState, ChatModification, LastMessageList } from "../Types"
import { proto } from '../../WAProto' import { proto } from '../../WAProto'
import { LT_HASH_ANTI_TAMPERING } from './lt-hash' import { ChatModification, ChatMutation, LastMessageList, LTHashState, WAPatchCreate, WAPatchName } from '../Types'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren } from '../WABinary' import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren } from '../WABinary'
import { aesDecrypt, aesEncrypt, hkdf, hmacSign } from './crypto'
import { toNumber } from './generics' import { toNumber } from './generics'
import { LT_HASH_ANTI_TAMPERING } from './lt-hash'
import { downloadContentFromMessage, } from './messages-media' import { downloadContentFromMessage, } from './messages-media'
type FetchAppStateSyncKey = (keyId: string) => Promise<proto.IAppStateSyncKeyData> | proto.IAppStateSyncKeyData type FetchAppStateSyncKey = (keyId: string) => Promise<proto.IAppStateSyncKeyData> | proto.IAppStateSyncKeyData
@@ -31,9 +31,11 @@ const generateMac = (operation: proto.SyncdMutation.SyncdMutationSyncdOperation,
r = 0x02 r = 0x02
break break
} }
const buff = Buffer.from([r]) const buff = Buffer.from([r])
return Buffer.concat([ buff, Buffer.from(keyId as any, 'base64') ]) return Buffer.concat([ buff, Buffer.from(keyId as any, 'base64') ])
} }
const keyData = getKeyData() const keyData = getKeyData()
const last = Buffer.alloc(8) // 8 bytes const last = Buffer.alloc(8) // 8 bytes
@@ -45,7 +47,7 @@ const generateMac = (operation: proto.SyncdMutation.SyncdMutationSyncdOperation,
return hmac.slice(0, 32) return hmac.slice(0, 32)
} }
const to64BitNetworkOrder = function(e) { const to64BitNetworkOrder = (e: number) => {
const t = new ArrayBuffer(8) const t = new ArrayBuffer(8)
new DataView(t).setUint32(4, e, !1) new DataView(t).setUint32(4, e, !1)
return Buffer.from(t) return Buffer.from(t)
@@ -66,6 +68,7 @@ const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' |
if(!prevOp) { if(!prevOp) {
throw new Boom('tried remove, but no previous op', { data: { indexMac, valueMac } }) throw new Boom('tried remove, but no previous op', { data: { indexMac, valueMac } })
} }
// remove from index value mac, since this mutation is erased // remove from index value mac, since this mutation is erased
delete indexValueMap[indexMacBase64] delete indexValueMap[indexMacBase64]
} else { } else {
@@ -73,6 +76,7 @@ const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' |
// add this index into the history map // add this index into the history map
indexValueMap[indexMacBase64] = { valueMac } indexValueMap[indexMacBase64] = { valueMac }
} }
if(prevOp) { if(prevOp) {
subBuffs.push(new Uint8Array(prevOp.valueMac).buffer) subBuffs.push(new Uint8Array(prevOp.valueMac).buffer)
} }
@@ -120,6 +124,7 @@ export const encodeSyncdPatch = async(
if(!key) { if(!key) {
throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 }) throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 })
} }
const encKeyId = Buffer.from(myAppStateKeyId, 'base64') const encKeyId = Buffer.from(myAppStateKeyId, 'base64')
state = { ...state, indexValueMap: { ...state.indexValueMap } } state = { ...state, indexValueMap: { ...state.indexValueMap } }
@@ -189,10 +194,12 @@ export const decodeSyncdMutations = async(
if(!keyEnc) { if(!keyEnc) {
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 404, data: { msgMutations } }) throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 404, data: { msgMutations } })
} }
const result = mutationKeys(keyEnc.keyData!) const result = mutationKeys(keyEnc.keyData!)
keyCache[base64Key] = result keyCache[base64Key] = result
key = result key = result
} }
return key return key
} }
@@ -283,13 +290,14 @@ export const extractSyncdPatches = async(result: BinaryNode) => {
const syncds: proto.ISyncdPatch[] = [] const syncds: proto.ISyncdPatch[] = []
const name = collectionNode.attrs.name as WAPatchName const name = collectionNode.attrs.name as WAPatchName
const hasMorePatches = collectionNode.attrs.has_more_patches == 'true' const hasMorePatches = collectionNode.attrs.has_more_patches === 'true'
let snapshot: proto.ISyncdSnapshot | undefined = undefined let snapshot: proto.ISyncdSnapshot | undefined = undefined
if(snapshotNode && !!snapshotNode.content) { if(snapshotNode && !!snapshotNode.content) {
if(!Buffer.isBuffer(snapshotNode)) { if(!Buffer.isBuffer(snapshotNode)) {
snapshotNode.content = Buffer.from(Object.values(snapshotNode.content)) snapshotNode.content = Buffer.from(Object.values(snapshotNode.content))
} }
const blobRef = proto.ExternalBlobReference.decode( const blobRef = proto.ExternalBlobReference.decode(
snapshotNode.content! as Buffer snapshotNode.content! as Buffer
) )
@@ -302,10 +310,12 @@ export const extractSyncdPatches = async(result: BinaryNode) => {
if(!Buffer.isBuffer(content)) { if(!Buffer.isBuffer(content)) {
content = Buffer.from(Object.values(content)) content = Buffer.from(Object.values(content))
} }
const syncd = proto.SyncdPatch.decode(content! as Uint8Array) const syncd = proto.SyncdPatch.decode(content! as Uint8Array)
if(!syncd.version) { if(!syncd.version) {
syncd.version = { version: +collectionNode.attrs.version+1 } syncd.version = { version: +collectionNode.attrs.version+1 }
} }
syncds.push(syncd) syncds.push(syncd)
} }
} }
@@ -325,6 +335,7 @@ export const downloadExternalBlob = async(blob: proto.IExternalBlobReference) =>
for await (const chunk of stream) { for await (const chunk of stream) {
buffer = Buffer.concat([buffer, chunk]) buffer = Buffer.concat([buffer, chunk])
} }
return buffer return buffer
} }
@@ -344,7 +355,7 @@ export const decodeSyncdSnapshot = async(
const newState = newLTHashState() const newState = newLTHashState()
newState.version = toNumber(snapshot.version!.version!) newState.version = toNumber(snapshot.version!.version!)
let { hash, indexValueMap, mutations } = await decodeSyncdMutations(snapshot.records!, newState, getAppStateSyncKey, validateMacs) const { hash, indexValueMap, mutations } = await decodeSyncdMutations(snapshot.records!, newState, getAppStateSyncKey, validateMacs)
newState.hash = hash newState.hash = hash
newState.indexValueMap = indexValueMap newState.indexValueMap = indexValueMap
@@ -354,6 +365,7 @@ export const decodeSyncdSnapshot = async(
if(!keyEnc) { if(!keyEnc) {
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 500 }) throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 500 })
} }
const result = mutationKeys(keyEnc.keyData!) const result = mutationKeys(keyEnc.keyData!)
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey) const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
if(Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) { if(Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) {
@@ -363,7 +375,8 @@ export const decodeSyncdSnapshot = async(
const areMutationsRequired = typeof minimumVersionNumber === 'undefined' || newState.version > minimumVersionNumber const areMutationsRequired = typeof minimumVersionNumber === 'undefined' || newState.version > minimumVersionNumber
if(!areMutationsRequired) { if(!areMutationsRequired) {
mutations = [] // clear array
mutations.splice(0, mutations.length)
} }
return { return {
@@ -412,6 +425,7 @@ export const decodePatches = async(
if(!keyEnc) { if(!keyEnc) {
throw new Boom(`failed to find key "${base64Key}" to decode mutation`) throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
} }
const result = mutationKeys(keyEnc.keyData!) const result = mutationKeys(keyEnc.keyData!)
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey) const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
if(Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) { if(Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {
@@ -419,6 +433,7 @@ export const decodePatches = async(
} }
} }
} }
return { return {
newMutations: successfulMutations, newMutations: successfulMutations,
state: newState state: newState
@@ -434,16 +449,19 @@ export const chatModificationToAppPatch = (
if(!lastMessages?.length) { if(!lastMessages?.length) {
throw new Boom('Expected last message to be not from me', { statusCode: 400 }) throw new Boom('Expected last message to be not from me', { statusCode: 400 })
} }
const lastMsg = lastMessages[lastMessages.length-1] const lastMsg = lastMessages[lastMessages.length-1]
if(lastMsg.key.fromMe) { if(lastMsg.key.fromMe) {
throw new Boom('Expected last message in array to be not from me', { statusCode: 400 }) throw new Boom('Expected last message in array to be not from me', { statusCode: 400 })
} }
const messageRange: proto.ISyncActionMessageRange = { const messageRange: proto.ISyncActionMessageRange = {
lastMessageTimestamp: lastMsg?.messageTimestamp, lastMessageTimestamp: lastMsg?.messageTimestamp,
messages: lastMessages messages: lastMessages
} }
return messageRange return messageRange
} }
let patch: WAPatchCreate let patch: WAPatchCreate
if('mute' in mod) { if('mute' in mod) {
patch = { patch = {

View File

@@ -1,5 +1,5 @@
import * as curveJs from 'curve25519-js'
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto' import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto'
import * as curveJs from 'curve25519-js'
import { KeyPair } from '../Types' import { KeyPair } from '../Types'
export const Curve = { export const Curve = {
@@ -37,29 +37,35 @@ export const signedKeyPair = (keyPair: KeyPair, keyId: number) => {
export function aesDecrypt(buffer: Buffer, key: Buffer) { export function aesDecrypt(buffer: Buffer, key: Buffer) {
return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16)) return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16))
} }
/** decrypt AES 256 CBC */ /** decrypt AES 256 CBC */
export function aesDecryptWithIV(buffer: Buffer, key: Buffer, IV: Buffer) { export function aesDecryptWithIV(buffer: Buffer, key: Buffer, IV: Buffer) {
const aes = createDecipheriv('aes-256-cbc', key, IV) const aes = createDecipheriv('aes-256-cbc', key, IV)
return Buffer.concat([aes.update(buffer), aes.final()]) return Buffer.concat([aes.update(buffer), aes.final()])
} }
// encrypt AES 256 CBC; where a random IV is prefixed to the buffer // encrypt AES 256 CBC; where a random IV is prefixed to the buffer
export function aesEncrypt(buffer: Buffer | Uint8Array, key: Buffer) { export function aesEncrypt(buffer: Buffer | Uint8Array, key: Buffer) {
const IV = randomBytes(16) const IV = randomBytes(16)
const aes = createCipheriv('aes-256-cbc', key, IV) const aes = createCipheriv('aes-256-cbc', key, IV)
return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer
} }
// encrypt AES 256 CBC with a given IV // encrypt AES 256 CBC with a given IV
export function aesEncrypWithIV(buffer: Buffer, key: Buffer, IV: Buffer) { export function aesEncrypWithIV(buffer: Buffer, key: Buffer, IV: Buffer) {
const aes = createCipheriv('aes-256-cbc', key, IV) const aes = createCipheriv('aes-256-cbc', key, IV)
return Buffer.concat([aes.update(buffer), aes.final()]) // prefix IV to the buffer return Buffer.concat([aes.update(buffer), aes.final()]) // prefix IV to the buffer
} }
// sign HMAC using SHA 256 // sign HMAC using SHA 256
export function hmacSign(buffer: Buffer | Uint8Array, key: Buffer | Uint8Array, variant: 'sha256' | 'sha512' = 'sha256') { export function hmacSign(buffer: Buffer | Uint8Array, key: Buffer | Uint8Array, variant: 'sha256' | 'sha512' = 'sha256') {
return createHmac(variant, key).update(buffer).digest() return createHmac(variant, key).update(buffer).digest()
} }
export function sha256(buffer: Buffer) { export function sha256(buffer: Buffer) {
return createHash('sha256').update(buffer).digest() return createHash('sha256').update(buffer).digest()
} }
// HKDF key expansion // HKDF key expansion
// from: https://github.com/benadida/node-hkdf // from: https://github.com/benadida/node-hkdf
export function hkdf(buffer: Uint8Array, expandedLength: number, { info, salt }: { salt?: Buffer, info?: string }) { export function hkdf(buffer: Uint8Array, expandedLength: number, { info, salt }: { salt?: Buffer, info?: string }) {
@@ -82,11 +88,12 @@ export function hkdf(buffer: Uint8Array, expandedLength: number, { info, salt }:
prev, prev,
infoBuff, infoBuff,
Buffer.from(String.fromCharCode(i + 1)) Buffer.from(String.fromCharCode(i + 1))
]); ])
hmac.update(input) hmac.update(input)
prev = hmac.digest() prev = hmac.digest()
buffers.push(prev) buffers.push(prev)
} }
return Buffer.concat(buffers, expandedLength) return Buffer.concat(buffers, expandedLength)
} }

View File

@@ -1,9 +1,9 @@
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import { unpadRandomMax16 } from "./generics"
import { WAMessageKey, AuthenticationState } from '../Types'
import { areJidsSameUser, BinaryNode, isJidBroadcast, isJidGroup, isJidStatusBroadcast, isJidUser, jidNormalizedUser } from '../WABinary'
import { decryptGroupSignalProto, decryptSignalProto, processSenderKeyMessage } from './signal'
import { proto } from '../../WAProto' import { proto } from '../../WAProto'
import { AuthenticationState, WAMessageKey } from '../Types'
import { areJidsSameUser, BinaryNode, isJidBroadcast, isJidGroup, isJidStatusBroadcast, isJidUser } from '../WABinary'
import { unpadRandomMax16 } from './generics'
import { decryptGroupSignalProto, decryptSignalProto, processSenderKeyMessage } from './signal'
type MessageType = 'chat' | 'peer_broadcast' | 'other_broadcast' | 'group' | 'direct_peer_status' | 'other_status' type MessageType = 'chat' | 'peer_broadcast' | 'other_broadcast' | 'group' | 'direct_peer_status' | 'other_status'
@@ -27,16 +27,19 @@ export const decodeMessageStanza = async(stanza: BinaryNode, auth: Authenticatio
if(!isMe(from)) { if(!isMe(from)) {
throw new Boom('') throw new Boom('')
} }
chatId = recipient chatId = recipient
} else { } else {
chatId = from chatId = from
} }
msgType = 'chat' msgType = 'chat'
author = from author = from
} else if(isJidGroup(from)) { } else if(isJidGroup(from)) {
if(!participant) { if(!participant) {
throw new Boom('No participant in group message') throw new Boom('No participant in group message')
} }
msgType = 'group' msgType = 'group'
author = participant author = participant
chatId = from chatId = from
@@ -44,12 +47,14 @@ export const decodeMessageStanza = async(stanza: BinaryNode, auth: Authenticatio
if(!participant) { if(!participant) {
throw new Boom('No participant in group message') throw new Boom('No participant in group message')
} }
const isParticipantMe = isMe(participant) const isParticipantMe = isMe(participant)
if(isJidStatusBroadcast(from)) { if(isJidStatusBroadcast(from)) {
msgType = isParticipantMe ? 'direct_peer_status' : 'other_status' msgType = isParticipantMe ? 'direct_peer_status' : 'other_status'
} else { } else {
msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast' msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast'
} }
chatId = from chatId = from
author = participant author = participant
} }
@@ -78,8 +83,13 @@ export const decodeMessageStanza = async(stanza: BinaryNode, auth: Authenticatio
if(Array.isArray(stanza.content)) { if(Array.isArray(stanza.content)) {
for(const { tag, attrs, content } of stanza.content) { for(const { tag, attrs, content } of stanza.content) {
if(tag !== 'enc') continue if(tag !== 'enc') {
if(!(content instanceof Uint8Array)) continue continue
}
if(!(content instanceof Uint8Array)) {
continue
}
let msgBuffer: Buffer let msgBuffer: Buffer
@@ -95,14 +105,18 @@ export const decodeMessageStanza = async(stanza: BinaryNode, auth: Authenticatio
msgBuffer = await decryptSignalProto(user, e2eType, content as Buffer, auth) msgBuffer = await decryptSignalProto(user, e2eType, content as Buffer, auth)
break break
} }
let msg: proto.IMessage = proto.Message.decode(unpadRandomMax16(msgBuffer)) let msg: proto.IMessage = proto.Message.decode(unpadRandomMax16(msgBuffer))
msg = msg.deviceSentMessage?.message || msg msg = msg.deviceSentMessage?.message || msg
if(msg.senderKeyDistributionMessage) { if(msg.senderKeyDistributionMessage) {
await processSenderKeyMessage(author, msg.senderKeyDistributionMessage, auth) await processSenderKeyMessage(author, msg.senderKeyDistributionMessage, auth)
} }
if(fullMessage.message) Object.assign(fullMessage.message, msg) if(fullMessage.message) {
else fullMessage.message = msg Object.assign(fullMessage.message, msg)
} else {
fullMessage.message = msg
}
} catch(error) { } catch(error) {
fullMessage.messageStubType = proto.WebMessageInfo.WebMessageInfoStubType.CIPHERTEXT fullMessage.messageStubType = proto.WebMessageInfo.WebMessageInfoStubType.CIPHERTEXT
fullMessage.messageStubParameters = [error.message] fullMessage.messageStubParameters = [error.message]

View File

@@ -2,10 +2,10 @@ import { Boom } from '@hapi/boom'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
import { platform, release } from 'os' import { platform, release } from 'os'
import { Logger } from 'pino' import { Logger } from 'pino'
import { ConnectionState } from '..'
import { proto } from '../../WAProto' import { proto } from '../../WAProto'
import { CommonBaileysEventEmitter, DisconnectReason } from '../Types' import { CommonBaileysEventEmitter, DisconnectReason } from '../Types'
import { Binary } from '../WABinary' import { Binary } from '../WABinary'
import { ConnectionState } from '..'
const PLATFORM_MAP = { const PLATFORM_MAP = {
'aix': 'AIX', 'aix': 'AIX',
@@ -27,6 +27,7 @@ export const BufferJSON = {
if(Buffer.isBuffer(value) || value instanceof Uint8Array || value?.type === 'Buffer') { if(Buffer.isBuffer(value) || value instanceof Uint8Array || value?.type === 'Buffer') {
return { type: 'Buffer', data: Buffer.from(value?.data || value).toString('base64') } return { type: 'Buffer', data: Buffer.from(value?.data || value).toString('base64') }
} }
return value return value
}, },
reviver: (_, value: any) => { reviver: (_, value: any) => {
@@ -34,16 +35,18 @@ export const BufferJSON = {
const val = value.data || value.value const val = value.data || value.value
return typeof val === 'string' ? Buffer.from(val, 'base64') : Buffer.from(val) return typeof val === 'string' ? Buffer.from(val, 'base64') : Buffer.from(val)
} }
return value return value
} }
} }
export const writeRandomPadMax16 = function(e: Binary) { export const writeRandomPadMax16 = (e: Binary) => {
function r(e: Binary, t: number) { function r(e: Binary, t: number) {
for (var r = 0; r < t; r++) for(var r = 0; r < t; r++) {
e.writeUint8(t) e.writeUint8(t)
} }
}
var t = randomBytes(1) var t = randomBytes(1)
r(e, 1 + (15 & t[0])) r(e, 1 + (15 & t[0]))
@@ -51,17 +54,17 @@ export const writeRandomPadMax16 = function(e: Binary) {
} }
export const unpadRandomMax16 = (e: Uint8Array | Buffer) => { export const unpadRandomMax16 = (e: Uint8Array | Buffer) => {
const t = new Uint8Array(e); const t = new Uint8Array(e)
if(0 === t.length) { if(0 === t.length) {
throw new Error('unpadPkcs7 given empty bytes'); throw new Error('unpadPkcs7 given empty bytes')
} }
var r = t[t.length - 1]; var r = t[t.length - 1]
if(r > t.length) { if(r > t.length) {
throw new Error(`unpad given ${t.length} bytes, but pad is ${r}`); throw new Error(`unpad given ${t.length} bytes, but pad is ${r}`)
} }
return new Uint8Array(t.buffer, t.byteOffset, t.length - r); return new Uint8Array(t.buffer, t.byteOffset, t.length - r)
} }
export const encodeWAMessage = (message: proto.IMessage) => ( export const encodeWAMessage = (message: proto.IMessage) => (
@@ -81,36 +84,42 @@ export const encodeInt = (e: number, t: number) => {
a[i] = 255 & r a[i] = 255 & r
r >>>= 8 r >>>= 8
} }
return a return a
} }
export const encodeBigEndian = (e: number, t=4) => { export const encodeBigEndian = (e: number, t=4) => {
let r = e; let r = e
let a = new Uint8Array(t); const a = new Uint8Array(t)
for(let i = t - 1; i >= 0; i--) { for(let i = t - 1; i >= 0; i--) {
a[i] = 255 & r a[i] = 255 & r
r >>>= 8 r >>>= 8
} }
return a return a
} }
export const toNumber = (t: Long | number) => ((typeof t === 'object' && 'toNumber' in t) ? t.toNumber() : t) export const toNumber = (t: Long | number) => ((typeof t === 'object' && 'toNumber' in t) ? t.toNumber() : t)
export function shallowChanges <T>(old: T, current: T, { lookForDeletedKeys }: {lookForDeletedKeys: boolean}): Partial<T> { export function shallowChanges <T>(old: T, current: T, { lookForDeletedKeys }: {lookForDeletedKeys: boolean}): Partial<T> {
let changes: Partial<T> = {} const changes: Partial<T> = {}
for (let key in current) { for(const key in current) {
if(old[key] !== current[key]) { if(old[key] !== current[key]) {
changes[key] = current[key] || null changes[key] = current[key] || null
} }
} }
if(lookForDeletedKeys) { if(lookForDeletedKeys) {
for (let key in old) { for(const key in old) {
if(!changes[key] && old[key] !== current[key]) { if(!changes[key] && old[key] !== current[key]) {
changes[key] = current[key] || null changes[key] = current[key] || null
} }
} }
} }
return changes return changes
} }
/** unix timestamp of a date in seconds */ /** unix timestamp of a date in seconds */
export const unixTimestampSeconds = (date: Date = new Date()) => Math.floor(date.getTime()/1000) export const unixTimestampSeconds = (date: Date = new Date()) => Math.floor(date.getTime()/1000)
@@ -154,13 +163,18 @@ export const delayCancellable = (ms: number) => {
}) })
) )
} }
return { delay, cancel } return { delay, cancel }
} }
export async function promiseTimeout<T>(ms: number, promise: (resolve: (v?: T)=>void, reject: (error) => void) => void) { export async function promiseTimeout<T>(ms: number, promise: (resolve: (v?: T)=>void, reject: (error) => void) => void) {
if (!ms) return new Promise (promise) if(!ms) {
return new Promise (promise)
}
const stack = new Error().stack const stack = new Error().stack
// Create a promise that rejects in <ms> milliseconds // Create a promise that rejects in <ms> milliseconds
let {delay, cancel} = delayCancellable (ms) const { delay, cancel } = delayCancellable (ms)
const p = new Promise ((resolve, reject) => { const p = new Promise ((resolve, reject) => {
delay delay
.then(() => reject( .then(() => reject(
@@ -178,6 +192,7 @@ export async function promiseTimeout<T>(ms: number, promise: (resolve: (v?: T)=>
.finally (cancel) .finally (cancel)
return p as Promise<T> return p as Promise<T>
} }
// generate a random ID to attach to a message // generate a random ID to attach to a message
export const generateMessageID = () => 'BAE5' + randomBytes(6).toString('hex').toUpperCase() export const generateMessageID = () => 'BAE5' + randomBytes(6).toString('hex').toUpperCase()
@@ -191,10 +206,11 @@ export const bindWaitForConnectionUpdate = (ev: CommonBaileysEventEmitter<any>)
listener = (update) => { listener = (update) => {
if(check(update)) { if(check(update)) {
resolve() resolve()
} else if(update.connection == 'close') { } else if(update.connection === 'close') {
reject(update.lastDisconnect?.error || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })) reject(update.lastDisconnect?.error || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }))
} }
} }
ev.on('connection.update', listener) ev.on('connection.update', listener)
} }
) )

View File

@@ -1,8 +1,8 @@
import { downloadContentFromMessage } from "./messages-media"
import { proto } from "../../WAProto"
import { promisify } from 'util' import { promisify } from 'util'
import { inflate } from "zlib" import { inflate } from 'zlib'
import { Chat, Contact } from "../Types" import { proto } from '../../WAProto'
import { Chat, Contact } from '../Types'
import { downloadContentFromMessage } from './messages-media'
const inflatePromise = promisify(inflate) const inflatePromise = promisify(inflate)
@@ -12,6 +12,7 @@ export const downloadHistory = async(msg: proto.IHistorySyncNotification) => {
for await (const chunk of stream) { for await (const chunk of stream) {
buffer = Buffer.concat([buffer, chunk]) buffer = Buffer.concat([buffer, chunk])
} }
// decompress buffer // decompress buffer
buffer = await inflatePromise(buffer) buffer = await inflatePromise(buffer)
@@ -51,6 +52,7 @@ export const processHistoryMessage = (item: proto.IHistorySync, historyCache: Se
historyCache.add(chat.id) historyCache.add(chat.id)
} }
} }
break break
case proto.HistorySync.HistorySyncHistorySyncType.PUSH_NAME: case proto.HistorySync.HistorySyncHistorySyncType.PUSH_NAME:
for(const c of item.pushnames) { for(const c of item.pushnames) {
@@ -60,6 +62,7 @@ export const processHistoryMessage = (item: proto.IHistorySync, historyCache: Se
historyCache.add(contactId) historyCache.add(contactId)
} }
} }
break break
case proto.HistorySync.HistorySyncHistorySyncType.INITIAL_STATUS_V3: case proto.HistorySync.HistorySyncHistorySyncType.INITIAL_STATUS_V3:
// TODO // TODO

View File

@@ -1,9 +1,9 @@
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
import { decodeBinaryNodeLegacy, jidNormalizedUser } from "../WABinary" import { AuthenticationCreds, Contact, CurveKeyPair, DisconnectReason, LegacyAuthenticationCreds, WATag } from '../Types'
import { aesDecrypt, hmacSign, hkdf, Curve } from "./crypto" import { decodeBinaryNodeLegacy, jidNormalizedUser } from '../WABinary'
import { aesDecrypt, Curve, hkdf, hmacSign } from './crypto'
import { BufferJSON } from './generics' import { BufferJSON } from './generics'
import { DisconnectReason, WATag, LegacyAuthenticationCreds, AuthenticationCreds, CurveKeyPair, Contact } from "../Types"
export const newLegacyAuthCreds = () => ({ export const newLegacyAuthCreds = () => ({
clientID: randomBytes(16).toString('base64') clientID: randomBytes(16).toString('base64')
@@ -15,9 +15,14 @@ export const decodeWAMessage = (
fromMe: boolean=false fromMe: boolean=false
) => { ) => {
let commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message let commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message
if (commaIndex < 0) throw new Boom('invalid message', { data: message }) // if there was no comma, then this message must be not be valid if(commaIndex < 0) {
throw new Boom('invalid message', { data: message })
} // if there was no comma, then this message must be not be valid
if(message[commaIndex+1] === ',') {
commaIndex += 1
}
if (message[commaIndex+1] === ',') commaIndex += 1
let data = message.slice(commaIndex+1, message.length) let data = message.slice(commaIndex+1, message.length)
// get the message tag. // get the message tag.
@@ -37,6 +42,7 @@ export const decodeWAMessage = (
if(!macKey || !encKey) { if(!macKey || !encKey) {
throw new Boom('recieved encrypted buffer when auth creds unavailable', { data: message, statusCode: DisconnectReason.badSession }) throw new Boom('recieved encrypted buffer when auth creds unavailable', { data: message, statusCode: DisconnectReason.badSession })
} }
/* /*
If the data recieved was not a JSON, then it must be an encrypted message. If the data recieved was not a JSON, then it must be an encrypted message.
Such a message can only be decrypted if we're connected successfully to the servers & have encryption keys Such a message can only be decrypted if we're connected successfully to the servers & have encryption keys
@@ -69,6 +75,7 @@ export const decodeWAMessage = (
} }
} }
} }
return [messageTag, json, tags] as const return [messageTag, json, tags] as const
} }
@@ -90,16 +97,20 @@ export const validateNewConnection = (
} }
return { user, auth, phone: json.phone } return { user, auth, phone: json.phone }
} }
if(!json.secret) { if(!json.secret) {
// if we didn't get a secret, we don't need it, we're validated // if we didn't get a secret, we don't need it, we're validated
if(json.clientToken && json.clientToken !== auth.clientToken) { if(json.clientToken && json.clientToken !== auth.clientToken) {
auth = { ...auth, clientToken: json.clientToken } auth = { ...auth, clientToken: json.clientToken }
} }
if(json.serverToken && json.serverToken !== auth.serverToken) { if(json.serverToken && json.serverToken !== auth.serverToken) {
auth = { ...auth, serverToken: json.serverToken } auth = { ...auth, serverToken: json.serverToken }
} }
return onValidationSuccess() return onValidationSuccess()
} }
const secret = Buffer.from(json.secret, 'base64') const secret = Buffer.from(json.secret, 'base64')
if(secret.length !== 144) { if(secret.length !== 144) {
throw new Error ('incorrect secret length received: ' + secret.length) throw new Error ('incorrect secret length received: ' + secret.length)
@@ -159,6 +170,7 @@ export const useSingleFileLegacyAuthState = (file: string) => {
if(typeof state.encKey === 'string') { if(typeof state.encKey === 'string') {
state.encKey = Buffer.from(state.encKey, 'base64') state.encKey = Buffer.from(state.encKey, 'base64')
} }
if(typeof state.macKey === 'string') { if(typeof state.macKey === 'string') {
state.macKey = Buffer.from(state.macKey, 'base64') state.macKey = Buffer.from(state.macKey, 'base64')
} }
@@ -179,6 +191,7 @@ export const getAuthenticationCredsType = (creds: LegacyAuthenticationCreds | Au
if('clientID' in creds && !!creds.clientID) { if('clientID' in creds && !!creds.clientID) {
return 'legacy' return 'legacy'
} }
if('noiseKey' in creds && !!creds.noiseKey) { if('noiseKey' in creds && !!creds.noiseKey) {
return 'md' return 'md'
} }

View File

@@ -6,7 +6,7 @@ import { hkdf } from './crypto'
* if the same series of mutations was made sequentially. * if the same series of mutations was made sequentially.
*/ */
const o = 128; const o = 128
class d { class d {
@@ -16,41 +16,45 @@ class d {
this.salt = e this.salt = e
} }
add(e, t) { add(e, t) {
var r = this; var r = this
for(const item of t) { for(const item of t) {
e = r._addSingle(e, item) e = r._addSingle(e, item)
} }
return e return e
} }
subtract(e, t) { subtract(e, t) {
var r = this; var r = this
for(const item of t) { for(const item of t) {
e = r._subtractSingle(e, item) e = r._subtractSingle(e, item)
} }
return e return e
} }
subtractThenAdd(e, t, r) { subtractThenAdd(e, t, r) {
var n = this; var n = this
return n.add(n.subtract(e, r), t) return n.add(n.subtract(e, r), t)
} }
_addSingle(e, t) { _addSingle(e, t) {
var r = this; var r = this
const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer; const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer
return r.performPointwiseWithOverflow(e, n, ((e, t) => e + t)) return r.performPointwiseWithOverflow(e, n, ((e, t) => e + t))
} }
_subtractSingle(e, t) { _subtractSingle(e, t) {
var r = this; var r = this
const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer; const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer
return r.performPointwiseWithOverflow(e, n, ((e, t) => e - t)) return r.performPointwiseWithOverflow(e, n, ((e, t) => e - t))
} }
performPointwiseWithOverflow(e, t, r) { performPointwiseWithOverflow(e, t, r) {
const n = new DataView(e) const n = new DataView(e)
, i = new DataView(t) , i = new DataView(t)
, a = new ArrayBuffer(n.byteLength) , a = new ArrayBuffer(n.byteLength)
, s = new DataView(a); , s = new DataView(a)
for (let e = 0; e < n.byteLength; e += 2) for(let e = 0; e < n.byteLength; e += 2) {
s.setUint16(e, r(n.getUint16(e, !0), i.getUint16(e, !0)), !0); s.setUint16(e, r(n.getUint16(e, !0), i.getUint16(e, !0)), !0)
}
return a return a
} }
} }

View File

@@ -6,7 +6,10 @@ export default () => {
task = (async() => { task = (async() => {
// wait for the previous task to complete // wait for the previous task to complete
// if there is an error, we swallow so as to not block the queue // if there is an error, we swallow so as to not block the queue
try { await task } catch { } try {
await task
} catch{ }
// execute the current task // execute the current task
return code() return code()
})() })()

View File

@@ -1,19 +1,19 @@
import type { Logger } from 'pino'
import type { IAudioMetadata } from 'music-metadata'
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import * as Crypto from 'crypto'
import { Readable, Transform } from 'stream'
import { createReadStream, createWriteStream, promises as fs, WriteStream } from 'fs'
import { exec } from 'child_process'
import { tmpdir } from 'os'
import { URL } from 'url'
import { join } from 'path'
import { once } from 'events'
import { MessageType, WAMessageContent, WAProto, WAGenericMediaMessage, WAMediaUpload, MediaType, DownloadableMessage, CommonSocketConfig, WAMediaUploadFunction, MediaConnInfo } from '../Types'
import { generateMessageID } from './generics'
import { hkdf } from './crypto'
import { DEFAULT_ORIGIN, MEDIA_PATH_MAP } from '../Defaults'
import { AxiosRequestConfig } from 'axios' import { AxiosRequestConfig } from 'axios'
import { exec } from 'child_process'
import * as Crypto from 'crypto'
import { once } from 'events'
import { createReadStream, createWriteStream, promises as fs, WriteStream } from 'fs'
import type { IAudioMetadata } from 'music-metadata'
import { tmpdir } from 'os'
import { join } from 'path'
import type { Logger } from 'pino'
import { Readable, Transform } from 'stream'
import { URL } from 'url'
import { DEFAULT_ORIGIN, MEDIA_PATH_MAP } from '../Defaults'
import { CommonSocketConfig, DownloadableMessage, MediaConnInfo, MediaType, MessageType, WAGenericMediaMessage, WAMediaUpload, WAMediaUploadFunction, WAMessageContent, WAProto } from '../Types'
import { hkdf } from './crypto'
import { generateMessageID } from './generics'
const getTmpFilesDirectory = () => tmpdir() const getTmpFilesDirectory = () => tmpdir()
@@ -34,25 +34,37 @@ const getImageProcessingLibrary = async() => {
return sharp return sharp
})() })()
]) ])
if(sharp) return { sharp } if(sharp) {
if(jimp) return { jimp } return { sharp }
}
if(jimp) {
return { jimp }
}
throw new Boom('No image processing library available') throw new Boom('No image processing library available')
} }
export const hkdfInfoKey = (type: MediaType) => { export const hkdfInfoKey = (type: MediaType) => {
let str: string = type let str: string = type
if(type === 'sticker') str = 'image' if(type === 'sticker') {
if(type === 'md-app-state') str = 'App State' str = 'image'
}
let hkdfInfo = str[0].toUpperCase() + str.slice(1) if(type === 'md-app-state') {
str = 'App State'
}
const hkdfInfo = str[0].toUpperCase() + str.slice(1)
return `WhatsApp ${hkdfInfo} Keys` return `WhatsApp ${hkdfInfo} Keys`
} }
/** generates all the keys required to encrypt/decrypt & sign a media message */ /** generates all the keys required to encrypt/decrypt & sign a media message */
export function getMediaKeys(buffer, mediaType: MediaType) { export function getMediaKeys(buffer, mediaType: MediaType) {
if(typeof buffer === 'string') { if(typeof buffer === 'string') {
buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64') buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64')
} }
// expand using HKDF to 112 bytes, also pass in the relevant app info // expand using HKDF to 112 bytes, also pass in the relevant app info
const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) }) const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) })
return { return {
@@ -61,18 +73,21 @@ export function getMediaKeys(buffer, mediaType: MediaType) {
macKey: expandedMediaKey.slice(48, 80), macKey: expandedMediaKey.slice(48, 80),
} }
} }
/** Extracts video thumb using FFMPEG */ /** Extracts video thumb using FFMPEG */
const extractVideoThumb = async( const extractVideoThumb = async(
path: string, path: string,
destPath: string, destPath: string,
time: string, time: string,
size: { width: number; height: number }, size: { width: number; height: number },
) => ) => new Promise((resolve, reject) => {
new Promise((resolve, reject) => {
const cmd = `ffmpeg -ss ${time} -i ${path} -y -s ${size.width}x${size.height} -vframes 1 -f image2 ${destPath}` const cmd = `ffmpeg -ss ${time} -i ${path} -y -s ${size.width}x${size.height} -vframes 1 -f image2 ${destPath}`
exec(cmd, (err) => { exec(cmd, (err) => {
if (err) reject(err) if(err) {
else resolve() reject(err)
} else {
resolve()
}
}) })
}) as Promise<void> }) as Promise<void>
@@ -80,6 +95,7 @@ export const extractImageThumb = async (bufferOrFilePath: Readable | Buffer | st
if(bufferOrFilePath instanceof Readable) { if(bufferOrFilePath instanceof Readable) {
bufferOrFilePath = await toBuffer(bufferOrFilePath) bufferOrFilePath = await toBuffer(bufferOrFilePath)
} }
const lib = await getImageProcessingLibrary() const lib = await getImageProcessingLibrary()
if('sharp' in lib) { if('sharp' in lib) {
const result = await lib.sharp!.default(bufferOrFilePath) const result = await lib.sharp!.default(bufferOrFilePath)
@@ -98,6 +114,7 @@ export const extractImageThumb = async (bufferOrFilePath: Readable | Buffer | st
return result return result
} }
} }
export const generateProfilePicture = async(mediaUpload: WAMediaUpload) => { export const generateProfilePicture = async(mediaUpload: WAMediaUpload) => {
let bufferOrFilePath: Buffer | string let bufferOrFilePath: Buffer | string
if(Buffer.isBuffer(mediaUpload)) { if(Buffer.isBuffer(mediaUpload)) {
@@ -133,11 +150,13 @@ export const generateProfilePicture = async (mediaUpload: WAMediaUpload) => {
img: await img, img: await img,
} }
} }
/** gets the SHA256 of the given media message */ /** gets the SHA256 of the given media message */
export const mediaMessageSHA256B64 = (message: WAMessageContent) => { export const mediaMessageSHA256B64 = (message: WAMessageContent) => {
const media = Object.values(message)[0] as WAGenericMediaMessage const media = Object.values(message)[0] as WAGenericMediaMessage
return media?.fileSha256 && Buffer.from(media.fileSha256).toString ('base64') return media?.fileSha256 && Buffer.from(media.fileSha256).toString ('base64')
} }
export async function getAudioDuration(buffer: Buffer | string | Readable) { export async function getAudioDuration(buffer: Buffer | string | Readable) {
const musicMetadata = await import('music-metadata') const musicMetadata = await import('music-metadata')
let metadata: IAudioMetadata let metadata: IAudioMetadata
@@ -150,29 +169,42 @@ export async function getAudioDuration (buffer: Buffer | string | Readable) {
} else { } else {
metadata = await musicMetadata.parseStream(buffer, null, { duration: true }) metadata = await musicMetadata.parseStream(buffer, null, { duration: true })
} }
return metadata.format.duration;
return metadata.format.duration
} }
export const toReadable = (buffer: Buffer) => { export const toReadable = (buffer: Buffer) => {
const readable = new Readable({ read: () => {} }) const readable = new Readable({ read: () => {} })
readable.push(buffer) readable.push(buffer)
readable.push(null) readable.push(null)
return readable return readable
} }
export const toBuffer = async(stream: Readable) => { export const toBuffer = async(stream: Readable) => {
let buff = Buffer.alloc(0) let buff = Buffer.alloc(0)
for await (const chunk of stream) { for await (const chunk of stream) {
buff = Buffer.concat([ buff, chunk ]) buff = Buffer.concat([ buff, chunk ])
} }
return buff return buff
} }
export const getStream = async(item: WAMediaUpload) => { export const getStream = async(item: WAMediaUpload) => {
if(Buffer.isBuffer(item)) return { stream: toReadable(item), type: 'buffer' } if(Buffer.isBuffer(item)) {
if('stream' in item) return { stream: item.stream, type: 'readable' } return { stream: toReadable(item), type: 'buffer' }
}
if('stream' in item) {
return { stream: item.stream, type: 'readable' }
}
if(item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) { if(item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {
return { stream: await getHttpStream(item.url), type: 'remote' } return { stream: await getHttpStream(item.url), type: 'remote' }
} }
return { stream: createReadStream(item.url), type: 'file' } return { stream: createReadStream(item.url), type: 'file' }
} }
/** generates a thumbnail for a given media, if required */ /** generates a thumbnail for a given media, if required */
export async function generateThumbnail( export async function generateThumbnail(
file: string, file: string,
@@ -200,11 +232,13 @@ export async function generateThumbnail(
return thumbnail return thumbnail
} }
export const getHttpStream = async(url: string | URL, options: AxiosRequestConfig & { isStream?: true } = {}) => { export const getHttpStream = async(url: string | URL, options: AxiosRequestConfig & { isStream?: true } = {}) => {
const { default: axios } = await import('axios') const { default: axios } = await import('axios')
const fetched = await axios.get(url.toString(), { ...options, responseType: 'stream' }) const fetched = await axios.get(url.toString(), { ...options, responseType: 'stream' })
return fetched.data as Readable return fetched.data as Readable
} }
export const encryptedStream = async( export const encryptedStream = async(
media: WAMediaUpload, media: WAMediaUpload,
mediaType: MediaType, mediaType: MediaType,
@@ -250,10 +284,14 @@ export const encryptedStream = async(
fileLength += data.length fileLength += data.length
sha256Plain = sha256Plain.update(data) sha256Plain = sha256Plain.update(data)
if(writeStream) { if(writeStream) {
if(!writeStream.write(data)) await once(writeStream, 'drain') if(!writeStream.write(data)) {
await once(writeStream, 'drain')
} }
}
onChunk(aes.update(data)) onChunk(aes.update(data))
} }
onChunk(aes.final()) onChunk(aes.final())
const mac = hmac.digest().slice(0, 10) const mac = hmac.digest().slice(0, 10)
@@ -324,6 +362,7 @@ export const downloadContentFromMessage = async(
firstBlockIsIV = true firstBlockIsIV = true
} }
} }
const endChunk = endByte ? toSmallestChunkSize(endByte || 0)+AES_CHUNK_SIZE : undefined const endChunk = endByte ? toSmallestChunkSize(endByte || 0)+AES_CHUNK_SIZE : undefined
const headers: { [_: string]: string } = { const headers: { [_: string]: string } = {
@@ -331,7 +370,9 @@ export const downloadContentFromMessage = async(
} }
if(startChunk || endChunk) { if(startChunk || endChunk) {
headers.Range = `bytes=${startChunk}-` headers.Range = `bytes=${startChunk}-`
if(endChunk) headers.Range += endChunk if(endChunk) {
headers.Range += endChunk
}
} }
// download the message // download the message
@@ -377,7 +418,7 @@ export const downloadContentFromMessage = async(
data = data.slice(AES_CHUNK_SIZE) data = data.slice(AES_CHUNK_SIZE)
} }
aes = Crypto.createDecipheriv("aes-256-cbc", cipherKey, ivValue) aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue)
// if an end byte that is not EOF is specified // if an end byte that is not EOF is specified
// stop auto padding (PKCS7) -- otherwise throws an error for decryption // stop auto padding (PKCS7) -- otherwise throws an error for decryption
if(endByte) { if(endByte) {
@@ -422,6 +463,7 @@ export async function decryptMediaMessageBuffer(message: WAMessageContent): Prom
) { ) {
throw new Boom(`no media message for "${type}"`, { statusCode: 400 }) throw new Boom(`no media message for "${type}"`, { statusCode: 400 })
} }
if(type === 'locationMessage' || type === 'liveLocationMessage') { if(type === 'locationMessage' || type === 'liveLocationMessage') {
const buffer = Buffer.from(message[type].jpegThumbnail) const buffer = Buffer.from(message[type].jpegThumbnail)
const readable = new Readable({ read: () => {} }) const readable = new Readable({ read: () => {} })
@@ -429,16 +471,22 @@ export async function decryptMediaMessageBuffer(message: WAMessageContent): Prom
readable.push(null) readable.push(null)
return readable return readable
} }
let messageContent: WAGenericMediaMessage let messageContent: WAGenericMediaMessage
if(message.productMessage) { if(message.productMessage) {
const product = message.productMessage.product?.productImage const product = message.productMessage.product?.productImage
if (!product) throw new Boom('product has no image', { statusCode: 400 }) if(!product) {
throw new Boom('product has no image', { statusCode: 400 })
}
messageContent = product messageContent = product
} else { } else {
messageContent = message[type] messageContent = message[type]
} }
return downloadContentFromMessage(messageContent, type.replace('Message', '') as MediaType) return downloadContentFromMessage(messageContent, type.replace('Message', '') as MediaType)
} }
export function extensionForMediaMessage(message: WAMessageContent) { export function extensionForMediaMessage(message: WAMessageContent) {
const getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1] const getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1]
const type = Object.keys(message)[0] as MessageType const type = Object.keys(message)[0] as MessageType
@@ -457,6 +505,7 @@ export function extensionForMediaMessage(message: WAMessageContent) {
| WAProto.DocumentMessage | WAProto.DocumentMessage
extension = getExtension (messageContent.mimetype) extension = getExtension (messageContent.mimetype)
} }
return extension return extension
} }
@@ -469,14 +518,14 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger }: C
let urls: { mediaUrl: string, directPath: string } let urls: { mediaUrl: string, directPath: string }
const hosts = [ ...customUploadHosts, ...uploadInfo.hosts ] const hosts = [ ...customUploadHosts, ...uploadInfo.hosts ]
let chunks: Buffer[] = [] const chunks: Buffer[] = []
for await (const chunk of stream) { for await (const chunk of stream) {
chunks.push(chunk) chunks.push(chunk)
} }
let reqBody = Buffer.concat(chunks) let reqBody = Buffer.concat(chunks)
for (let { hostname, maxContentLengthBytes } of hosts) { for(const { hostname, maxContentLengthBytes } of hosts) {
logger.debug(`uploading to "${hostname}"`) logger.debug(`uploading to "${hostname}"`)
const auth = encodeURIComponent(uploadInfo.auth) // the auth token const auth = encodeURIComponent(uploadInfo.auth) // the auth token
@@ -523,6 +572,7 @@ export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger }: C
logger.warn({ trace: error.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`) logger.warn({ trace: error.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`)
} }
} }
// clear buffer just to be sure we're releasing the memory // clear buffer just to be sure we're releasing the memory
reqBody = undefined reqBody = undefined

View File

@@ -1,11 +1,12 @@
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import { promises as fs } from "fs" import { promises as fs } from 'fs'
import { proto } from '../../WAProto' import { proto } from '../../WAProto'
import { MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from "../Defaults" import { MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults'
import { import {
AnyMediaMessageContent, AnyMediaMessageContent,
AnyMessageContent, AnyMessageContent,
MediaGenerationOptions, MediaGenerationOptions,
MediaType,
MessageContentGenerationOptions, MessageContentGenerationOptions,
MessageGenerationOptions, MessageGenerationOptions,
MessageGenerationOptionsFromContent, MessageGenerationOptionsFromContent,
@@ -13,13 +14,11 @@ import {
WAMediaUpload, WAMediaUpload,
WAMessage, WAMessage,
WAMessageContent, WAMessageContent,
WAMessageStatus,
WAProto, WAProto,
WATextMessage, WATextMessage } from '../Types'
MediaType, import { generateMessageID, unixTimestampSeconds } from './generics'
WAMessageStatus import { encryptedStream, generateThumbnail, getAudioDuration } from './messages-media'
} from "../Types"
import { generateMessageID, unixTimestampSeconds } from "./generics"
import { encryptedStream, generateThumbnail, getAudioDuration } from "./messages-media"
type MediaUploadData = { type MediaUploadData = {
media: WAMediaUpload media: WAMediaUpload
@@ -39,7 +38,7 @@ const MIMETYPE_MAP: { [T in MediaType]: string } = {
audio: 'audio/ogg; codecs=opus', audio: 'audio/ogg; codecs=opus',
sticker: 'image/webp', sticker: 'image/webp',
history: 'application/x-protobuf', history: 'application/x-protobuf',
"md-app-state": 'application/x-protobuf', 'md-app-state': 'application/x-protobuf',
} }
const MessageTypeProto = { const MessageTypeProto = {
@@ -64,6 +63,7 @@ export const prepareWAMessageMedia = async(
mediaType = key mediaType = key
} }
} }
const uploadData: MediaUploadData = { const uploadData: MediaUploadData = {
...message, ...message,
media: message[mediaType] media: message[mediaType]
@@ -81,6 +81,7 @@ export const prepareWAMessageMedia = async(
if(mediaType === 'document' && !uploadData.fileName) { if(mediaType === 'document' && !uploadData.fileName) {
uploadData.fileName = 'file' uploadData.fileName = 'file'
} }
if(!uploadData.mimetype) { if(!uploadData.mimetype) {
uploadData.mimetype = MIMETYPE_MAP[mediaType] uploadData.mimetype = MIMETYPE_MAP[mediaType]
} }
@@ -89,7 +90,7 @@ export const prepareWAMessageMedia = async(
if(cacheableKey) { if(cacheableKey) {
const mediaBuff: Buffer = options.mediaCache!.get(cacheableKey) const mediaBuff: Buffer = options.mediaCache!.get(cacheableKey)
if(mediaBuff) { if(mediaBuff) {
logger?.debug({ cacheableKey }, `got media cache hit`) logger?.debug({ cacheableKey }, 'got media cache hit')
const obj = WAProto.Message.decode(mediaBuff) const obj = WAProto.Message.decode(mediaBuff)
const key = `${mediaType}Message` const key = `${mediaType}Message`
@@ -128,18 +129,19 @@ export const prepareWAMessageMedia = async(
encWriteStream, encWriteStream,
{ fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs } { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs }
) )
logger?.debug(`uploaded media`) logger?.debug('uploaded media')
return result return result
})(), })(),
(async() => { (async() => {
try { try {
if(requiresThumbnailComputation) { if(requiresThumbnailComputation) {
uploadData.jpegThumbnail = await generateThumbnail(bodyPath, mediaType as any, options) uploadData.jpegThumbnail = await generateThumbnail(bodyPath, mediaType as any, options)
logger?.debug(`generated thumbnail`) logger?.debug('generated thumbnail')
} }
if(requiresDurationComputation) { if(requiresDurationComputation) {
uploadData.seconds = await getAudioDuration(bodyPath) uploadData.seconds = await getAudioDuration(bodyPath)
logger?.debug(`computed audio duration`) logger?.debug('computed audio duration')
} }
} catch(error) { } catch(error) {
logger?.warn({ trace: error.stack }, 'failed to obtain extra info') logger?.warn({ trace: error.stack }, 'failed to obtain extra info')
@@ -175,12 +177,13 @@ export const prepareWAMessageMedia = async(
}) })
if(cacheableKey) { if(cacheableKey) {
logger.debug({ cacheableKey }, `set cache`) logger.debug({ cacheableKey }, 'set cache')
options.mediaCache!.set(cacheableKey, WAProto.Message.encode(obj).finish()) options.mediaCache!.set(cacheableKey, WAProto.Message.encode(obj).finish())
} }
return obj return obj
} }
export const prepareDisappearingMessageSettingContent = (ephemeralExpiration?: number) => { export const prepareDisappearingMessageSettingContent = (ephemeralExpiration?: number) => {
ephemeralExpiration = ephemeralExpiration || 0 ephemeralExpiration = ephemeralExpiration || 0
const content: WAMessageContent = { const content: WAMessageContent = {
@@ -195,6 +198,7 @@ export const prepareDisappearingMessageSettingContent = (ephemeralExpiration?: n
} }
return WAProto.Message.fromObject(content) return WAProto.Message.fromObject(content)
} }
/** /**
* Generate forwarded message content like WA does * Generate forwarded message content like WA does
* @param message the message to forward * @param message the message to forward
@@ -205,7 +209,10 @@ export const generateForwardMessageContent = (
forceForward?: boolean forceForward?: boolean
) => { ) => {
let content = message.message let content = message.message
if (!content) throw new Boom('no content in message', { statusCode: 400 }) if(!content) {
throw new Boom('no content in message', { statusCode: 400 })
}
// hacky copy // hacky copy
content = proto.Message.decode(proto.Message.encode(message.message).finish()) content = proto.Message.decode(proto.Message.encode(message.message).finish())
@@ -219,11 +226,16 @@ export const generateForwardMessageContent = (
key = 'extendedTextMessage' key = 'extendedTextMessage'
} }
if (score > 0) content[key].contextInfo = { forwardingScore: score, isForwarded: true }
else content[key].contextInfo = {} if(score > 0) {
content[key].contextInfo = { forwardingScore: score, isForwarded: true }
} else {
content[key].contextInfo = {}
}
return content return content
} }
export const generateWAMessageContent = async( export const generateWAMessageContent = async(
message: AnyMessageContent, message: AnyMessageContent,
options: MessageContentGenerationOptions options: MessageContentGenerationOptions
@@ -244,12 +256,14 @@ export const generateWAMessageContent = async(
options.logger?.warn({ trace: error.stack }, 'url generation failed') options.logger?.warn({ trace: error.stack }, 'url generation failed')
} }
} }
m.extendedTextMessage = extContent m.extendedTextMessage = extContent
} else if('contacts' in message) { } else if('contacts' in message) {
const contactLen = message.contacts.contacts.length const contactLen = message.contacts.contacts.length
if(!contactLen) { if(!contactLen) {
throw new Boom('require atleast 1 contact', { statusCode: 400 }) throw new Boom('require atleast 1 contact', { statusCode: 400 })
} }
if(contactLen === 1) { if(contactLen === 1) {
m.contactMessage = WAProto.ContactMessage.fromObject(message.contacts.contacts[0]) m.contactMessage = WAProto.ContactMessage.fromObject(message.contacts.contacts[0])
} else { } else {
@@ -278,6 +292,7 @@ export const generateWAMessageContent = async(
options options
) )
} }
if('buttons' in message && !!message.buttons) { if('buttons' in message && !!message.buttons) {
const buttonsMessage: proto.IButtonsMessage = { const buttonsMessage: proto.IButtonsMessage = {
buttons: message.buttons!.map(b => ({ ...b, type: proto.Button.ButtonType.RESPONSE })) buttons: message.buttons!.map(b => ({ ...b, type: proto.Button.ButtonType.RESPONSE }))
@@ -289,6 +304,7 @@ export const generateWAMessageContent = async(
if('caption' in message) { if('caption' in message) {
buttonsMessage.contentText = message.caption buttonsMessage.contentText = message.caption
} }
const type = Object.keys(m)[0].replace('Message', '').toUpperCase() const type = Object.keys(m)[0].replace('Message', '').toUpperCase()
buttonsMessage.headerType = ButtonType[type] buttonsMessage.headerType = ButtonType[type]
@@ -341,19 +357,24 @@ export const generateWAMessageContent = async(
if('viewOnce' in message && !!message.viewOnce) { if('viewOnce' in message && !!message.viewOnce) {
m = { viewOnceMessage: { message: m } } m = { viewOnceMessage: { message: m } }
} }
if('mentions' in message && message.mentions?.length) { if('mentions' in message && message.mentions?.length) {
const [messageType] = Object.keys(m) const [messageType] = Object.keys(m)
m[messageType].contextInfo = m[messageType] || { } m[messageType].contextInfo = m[messageType] || { }
m[messageType].contextInfo.mentionedJid = message.mentions m[messageType].contextInfo.mentionedJid = message.mentions
} }
return WAProto.Message.fromObject(m) return WAProto.Message.fromObject(m)
} }
export const generateWAMessageFromContent = ( export const generateWAMessageFromContent = (
jid: string, jid: string,
message: WAMessageContent, message: WAMessageContent,
options: MessageGenerationOptionsFromContent options: MessageGenerationOptionsFromContent
) => { ) => {
if(!options.timestamp) options.timestamp = new Date() // set timestamp to now if(!options.timestamp) {
options.timestamp = new Date()
} // set timestamp to now
const key = Object.keys(message)[0] const key = Object.keys(message)[0]
const timestamp = unixTimestampSeconds(options.timestamp) const timestamp = unixTimestampSeconds(options.timestamp)
@@ -373,6 +394,7 @@ export const generateWAMessageFromContent = (
message[key].contextInfo.remoteJid = quoted.key.remoteJid message[key].contextInfo.remoteJid = quoted.key.remoteJid
} }
} }
if( if(
// if we want to send a disappearing message // if we want to send a disappearing message
!!options?.ephemeralExpiration && !!options?.ephemeralExpiration &&
@@ -409,6 +431,7 @@ export const generateWAMessageFromContent = (
} }
return WAProto.WebMessageInfo.fromObject(messageJSON) return WAProto.WebMessageInfo.fromObject(messageJSON)
} }
export const generateWAMessage = async( export const generateWAMessage = async(
jid: string, jid: string,
content: AnyMessageContent, content: AnyMessageContent,
@@ -434,6 +457,7 @@ export const getContentType = (content: WAProto.IMessage | undefined) => {
return key as keyof typeof content return key as keyof typeof content
} }
} }
/** /**
* Extract the true message content from a message * Extract the true message content from a message
* Eg. extracts the inner message from a disappearing message/view once message * Eg. extracts the inner message from a disappearing message/view once message
@@ -458,6 +482,7 @@ export const extractMessageContent = (content: WAMessageContent | undefined | nu
return { conversation: buttonsMessage.contentText } return { conversation: buttonsMessage.contentText }
} }
} }
return content return content
} }
@@ -465,6 +490,6 @@ export const extractMessageContent = (content: WAMessageContent | undefined | nu
* Returns the device predicted by message ID * Returns the device predicted by message ID
*/ */
export const getDevice = (id: string) => { export const getDevice = (id: string) => {
const deviceType = id.length > 21 ? 'android' : id.substring(0, 2) == '3A' ? 'ios' : 'web' const deviceType = id.length > 21 ? 'android' : id.substring(0, 2) === '3A' ? 'ios' : 'web'
return deviceType return deviceType
} }

View File

@@ -1,15 +1,15 @@
import { sha256, Curve, hkdf } from "./crypto"; import { Boom } from '@hapi/boom'
import { Binary } from "../WABinary"; import { createCipheriv, createDecipheriv } from 'crypto'
import { createCipheriv, createDecipheriv } from "crypto";
import { NOISE_MODE, NOISE_WA_HEADER } from "../Defaults";
import { KeyPair } from "../Types";
import { BinaryNode, decodeBinaryNode } from "../WABinary";
import { Boom } from "@hapi/boom";
import { proto } from '../../WAProto' import { proto } from '../../WAProto'
import { NOISE_MODE, NOISE_WA_HEADER } from '../Defaults'
import { KeyPair } from '../Types'
import { Binary } from '../WABinary'
import { BinaryNode, decodeBinaryNode } from '../WABinary'
import { Curve, hkdf, sha256 } from './crypto'
const generateIV = (counter: number) => { const generateIV = (counter: number) => {
const iv = new ArrayBuffer(12); const iv = new ArrayBuffer(12)
new DataView(iv).setUint32(8, counter); new DataView(iv).setUint32(8, counter)
return new Uint8Array(iv) return new Uint8Array(iv)
} }
@@ -21,6 +21,7 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
hash = sha256(Buffer.from(Binary.build(hash, data).readByteArray())) hash = sha256(Buffer.from(Binary.build(hash, data).readByteArray()))
} }
} }
const encrypt = (plaintext: Uint8Array) => { const encrypt = (plaintext: Uint8Array) => {
const authTagLength = 128 >> 3 const authTagLength = 128 >> 3
const cipher = createCipheriv('aes-256-gcm', encKey, generateIV(writeCounter), { authTagLength }) const cipher = createCipheriv('aes-256-gcm', encKey, generateIV(writeCounter), { authTagLength })
@@ -33,6 +34,7 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
authenticate(result) authenticate(result)
return result return result
} }
const decrypt = (ciphertext: Uint8Array) => { const decrypt = (ciphertext: Uint8Array) => {
// before the handshake is finished, we use the same counter // before the handshake is finished, we use the same counter
// after handshake, the counters are different // after handshake, the counters are different
@@ -48,16 +50,21 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
const result = Buffer.concat([cipher.update(enc), cipher.final()]) const result = Buffer.concat([cipher.update(enc), cipher.final()])
if(isFinished) readCounter += 1 if(isFinished) {
else writeCounter += 1 readCounter += 1
} else {
writeCounter += 1
}
authenticate(ciphertext) authenticate(ciphertext)
return result return result
} }
const localHKDF = (data: Uint8Array) => { const localHKDF = (data: Uint8Array) => {
const key = hkdf(Buffer.from(data), 64, { salt, info: '' }) const key = hkdf(Buffer.from(data), 64, { salt, info: '' })
return [key.slice(0, 32), key.slice(32)] return [key.slice(0, 32), key.slice(32)]
} }
const mixIntoKey = (data: Uint8Array) => { const mixIntoKey = (data: Uint8Array) => {
const [write, read] = localHKDF(data) const [write, read] = localHKDF(data)
salt = write salt = write
@@ -66,6 +73,7 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
readCounter = 0 readCounter = 0
writeCounter = 0 writeCounter = 0
} }
const finishInit = () => { const finishInit = () => {
const [write, read] = localHKDF(new Uint8Array(0)) const [write, read] = localHKDF(new Uint8Array(0))
encKey = write encKey = write
@@ -123,6 +131,7 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
if(isFinished) { if(isFinished) {
data = encrypt(data) data = encrypt(data)
} }
const introSize = sentIntro ? 0 : NOISE_WA_HEADER.length const introSize = sentIntro ? 0 : NOISE_WA_HEADER.length
outBinary.ensureAdditionalCapacity(introSize + 3 + data.byteLength) outBinary.ensureAdditionalCapacity(introSize + 3 + data.byteLength)
@@ -146,6 +155,7 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
const getBytesSize = () => { const getBytesSize = () => {
return (inBinary.readUint8() << 16) | inBinary.readUint16() return (inBinary.readUint8() << 16) | inBinary.readUint16()
} }
const peekSize = () => { const peekSize = () => {
return !(inBinary.size() < 3) && getBytesSize() <= inBinary.size() return !(inBinary.size() < 3) && getBytesSize() <= inBinary.size()
} }
@@ -159,8 +169,10 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
const unpacked = new Binary(result).decompressed() const unpacked = new Binary(result).decompressed()
frame = decodeBinaryNode(unpacked) frame = decodeBinaryNode(unpacked)
} }
onFrame(frame) onFrame(frame)
} }
inBinary.peek(peekSize) inBinary.peek(peekSize)
} }
} }

View File

@@ -1,10 +1,10 @@
import * as libsignal from 'libsignal' import * as libsignal from 'libsignal'
import { encodeBigEndian } from "./generics" import { proto } from '../../WAProto'
import { Curve } from "./crypto" import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage, SenderKeyName, SenderKeyRecord } from '../../WASignalGroup'
import { SenderKeyDistributionMessage, GroupSessionBuilder, SenderKeyRecord, SenderKeyName, GroupCipher } from '../../WASignalGroup' import { AuthenticationCreds, KeyPair, SignalAuthState, SignalIdentity, SignalKeyStore, SignedKeyPair } from '../Types/Auth'
import { SignalIdentity, SignalKeyStore, SignedKeyPair, KeyPair, SignalAuthState, AuthenticationCreds } from "../Types/Auth" import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildUInt, jidDecode, JidWithDevice } from '../WABinary'
import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildUInt, jidDecode, JidWithDevice, getBinaryNodeChildren } from "../WABinary" import { Curve } from './crypto'
import { proto } from "../../WAProto" import { encodeBigEndian } from './generics'
export const generateSignalPubKey = (pubKey: Uint8Array | Buffer) => { export const generateSignalPubKey = (pubKey: Uint8Array | Buffer) => {
const newPub = Buffer.alloc(33) const newPub = Buffer.alloc(33)
@@ -38,6 +38,7 @@ export const getPreKeys = async({ get }: SignalKeyStore, min: number, limit: num
for(let id = min; id < limit;id++) { for(let id = min; id < limit;id++) {
idList.push(id.toString()) idList.push(id.toString())
} }
return get('pre-key', idList) return get('pre-key', idList)
} }
@@ -51,6 +52,7 @@ export const generateOrGetPreKeys = (creds: AuthenticationCreds, range: number)
newPreKeys[i] = Curve.generateKeyPair() newPreKeys[i] = Curve.generateKeyPair()
} }
} }
return { return {
newPreKeys, newPreKeys,
lastPreKeyId, lastPreKeyId,
@@ -115,7 +117,9 @@ export const signalStorage = ({ creds, keys }: SignalAuthState) => ({
}, },
loadSenderKey: async(keyId: string) => { loadSenderKey: async(keyId: string) => {
const { [keyId]: key } = await keys.get('sender-key', [keyId]) const { [keyId]: key } = await keys.get('sender-key', [keyId])
if(key) return new SenderKeyRecord(key) if(key) {
return new SenderKeyRecord(key)
}
}, },
storeSenderKey: async(keyId, key) => { storeSenderKey: async(keyId, key) => {
await keys.set({ 'sender-key': { [keyId]: key.serialize() } }) await keys.set({ 'sender-key': { [keyId]: key.serialize() } })
@@ -153,6 +157,7 @@ export const processSenderKeyMessage = async(
const record = new SenderKeyRecord() const record = new SenderKeyRecord()
await auth.keys.set({ 'sender-key': { [senderName]: record } }) await auth.keys.set({ 'sender-key': { [senderName]: record } })
} }
await builder.process(senderName, senderMsg) await builder.process(senderName, senderMsg)
} }
@@ -168,6 +173,7 @@ export const decryptSignalProto = async(user: string, type: 'pkmsg' | 'msg', msg
result = await session.decryptWhisperMessage(msg) result = await session.decryptWhisperMessage(msg)
break break
} }
return result return result
} }
@@ -216,6 +222,7 @@ export const parseAndInjectE2ESessions = async(node: BinaryNode, auth: SignalAut
for(const node of nodes) { for(const node of nodes) {
assertNodeErrorFree(node) assertNodeErrorFree(node)
} }
await Promise.all( await Promise.all(
nodes.map( nodes.map(
async node => { async node => {
@@ -264,5 +271,6 @@ export const extractDeviceJids = (result: BinaryNode, myJid: string, excludeZero
} }
} }
} }
return extracted return extracted
} }

View File

@@ -1,9 +1,9 @@
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import { proto } from '../../WAProto' import { proto } from '../../WAProto'
import type { SocketConfig, AuthenticationCreds, SignalCreds } from "../Types" import type { AuthenticationCreds, SignalCreds, SocketConfig } from '../Types'
import { Binary, BinaryNode, getAllBinaryNodeChildren, jidDecode, S_WHATSAPP_NET } from '../WABinary'
import { Curve, hmacSign } from './crypto' import { Curve, hmacSign } from './crypto'
import { encodeInt } from './generics' import { encodeInt } from './generics'
import { BinaryNode, S_WHATSAPP_NET, jidDecode, Binary, getAllBinaryNodeChildren } from '../WABinary'
import { createSignalIdentity } from './signal' import { createSignalIdentity } from './signal'
const ENCODED_VERSION = 'S9Kdc4pc4EJryo21snc5cg==' const ENCODED_VERSION = 'S9Kdc4pc4EJryo21snc5cg=='
@@ -15,12 +15,12 @@ const getUserAgent = ({ version, browser }: Pick<SocketConfig, 'version' | 'brow
}, },
platform: 14, platform: 14,
releaseChannel: 0, releaseChannel: 0,
mcc: "000", mcc: '000',
mnc: "000", mnc: '000',
osVersion: browser[2], osVersion: browser[2],
manufacturer: "", manufacturer: '',
device: browser[1], device: browser[1],
osBuildNumber: "0.1", osBuildNumber: '0.1',
localeLanguageIso6391: 'en', localeLanguageIso6391: 'en',
localeCountryIso31661Alpha2: 'en', localeCountryIso31661Alpha2: 'en',
}) })
@@ -43,7 +43,7 @@ export const generateRegistrationNode = (
{ registrationId, signedPreKey, signedIdentityKey }: SignalCreds, { registrationId, signedPreKey, signedIdentityKey }: SignalCreds,
config: Pick<SocketConfig, 'version' | 'browser'> config: Pick<SocketConfig, 'version' | 'browser'>
) => { ) => {
const appVersionBuf = new Uint8Array(Buffer.from(ENCODED_VERSION, "base64")); const appVersionBuf = new Uint8Array(Buffer.from(ENCODED_VERSION, 'base64'))
const companion = { const companion = {
os: config.browser[0], os: config.browser[0],
@@ -54,7 +54,7 @@ export const generateRegistrationNode = (
}, },
platformType: 1, platformType: 1,
requireFullSync: false, requireFullSync: false,
}; }
const companionProto = proto.CompanionProps.encode(companion).finish() const companionProto = proto.CompanionProps.encode(companion).finish()

View File

@@ -20,15 +20,18 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
throw new Error('end of stream') throw new Error('end of stream')
} }
} }
const next = () => { const next = () => {
const value = buffer[indexRef.index] const value = buffer[indexRef.index]
indexRef.index += 1 indexRef.index += 1
return value return value
} }
const readByte = () => { const readByte = () => {
checkEOS(1) checkEOS(1)
return next() return next()
} }
const readStringFromChars = (length: number) => { const readStringFromChars = (length: number) => {
checkEOS(length) checkEOS(length)
const value = buffer.slice(indexRef.index, indexRef.index + length) const value = buffer.slice(indexRef.index, indexRef.index + length)
@@ -36,12 +39,14 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
indexRef.index += length indexRef.index += length
return value.toString('utf-8') return value.toString('utf-8')
} }
const readBytes = (n: number) => { const readBytes = (n: number) => {
checkEOS(n) checkEOS(n)
const value = buffer.slice(indexRef.index, indexRef.index + n) const value = buffer.slice(indexRef.index, indexRef.index + n)
indexRef.index += n indexRef.index += n
return value return value
} }
const readInt = (n: number, littleEndian = false) => { const readInt = (n: number, littleEndian = false) => {
checkEOS(n) checkEOS(n)
let val = 0 let val = 0
@@ -49,22 +54,28 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
const shift = littleEndian ? i : n - 1 - i const shift = littleEndian ? i : n - 1 - i
val |= next() << (shift * 8) val |= next() << (shift * 8)
} }
return val return val
} }
const readInt20 = () => { const readInt20 = () => {
checkEOS(3) checkEOS(3)
return ((next() & 15) << 16) + (next() << 8) + next() return ((next() & 15) << 16) + (next() << 8) + next()
} }
const unpackHex = (value: number) => { const unpackHex = (value: number) => {
if(value >= 0 && value < 16) { if(value >= 0 && value < 16) {
return value < 10 ? '0'.charCodeAt(0) + value : 'A'.charCodeAt(0) + value - 10 return value < 10 ? '0'.charCodeAt(0) + value : 'A'.charCodeAt(0) + value - 10
} }
throw new Error('invalid hex: ' + value) throw new Error('invalid hex: ' + value)
} }
const unpackNibble = (value: number) => { const unpackNibble = (value: number) => {
if(value >= 0 && value <= 9) { if(value >= 0 && value <= 9) {
return '0'.charCodeAt(0) + value return '0'.charCodeAt(0) + value
} }
switch (value) { switch (value) {
case 10: case 10:
return '-'.charCodeAt(0) return '-'.charCodeAt(0)
@@ -76,6 +87,7 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
throw new Error('invalid nibble: ' + value) throw new Error('invalid nibble: ' + value)
} }
} }
const unpackByte = (tag: number, value: number) => { const unpackByte = (tag: number, value: number) => {
if(tag === Tags.NIBBLE_8) { if(tag === Tags.NIBBLE_8) {
return unpackNibble(value) return unpackNibble(value)
@@ -85,6 +97,7 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
throw new Error('unknown tag: ' + tag) throw new Error('unknown tag: ' + tag)
} }
} }
const readPacked8 = (tag: number) => { const readPacked8 = (tag: number) => {
const startByte = readByte() const startByte = readByte()
let value = '' let value = ''
@@ -94,14 +107,18 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
value += String.fromCharCode(unpackByte(tag, (curByte & 0xf0) >> 4)) value += String.fromCharCode(unpackByte(tag, (curByte & 0xf0) >> 4))
value += String.fromCharCode(unpackByte(tag, curByte & 0x0f)) value += String.fromCharCode(unpackByte(tag, curByte & 0x0f))
} }
if(startByte >> 7 !== 0) { if(startByte >> 7 !== 0) {
value = value.slice(0, -1) value = value.slice(0, -1)
} }
return value return value
} }
const isListTag = (tag: number) => { const isListTag = (tag: number) => {
return tag === Tags.LIST_EMPTY || tag === Tags.LIST_8 || tag === Tags.LIST_16 return tag === Tags.LIST_EMPTY || tag === Tags.LIST_8 || tag === Tags.LIST_16
} }
const readListSize = (tag: number) => { const readListSize = (tag: number) => {
switch (tag) { switch (tag) {
case Tags.LIST_EMPTY: case Tags.LIST_EMPTY:
@@ -114,12 +131,15 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
throw new Error('invalid tag for list size: ' + tag) throw new Error('invalid tag for list size: ' + tag)
} }
} }
const getToken = (index: number) => { const getToken = (index: number) => {
if(index < 3 || index >= SingleByteTokens.length) { if(index < 3 || index >= SingleByteTokens.length) {
throw new Error('invalid token index: ' + index) throw new Error('invalid token index: ' + index)
} }
return SingleByteTokens[index] return SingleByteTokens[index]
} }
const readString = (tag: number) => { const readString = (tag: number) => {
if(tag >= 3 && tag <= 235) { if(tag >= 3 && tag <= 235) {
const token = getToken(tag) const token = getToken(tag)
@@ -146,6 +166,7 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
if(typeof i === 'string' && j) { if(typeof i === 'string' && j) {
return i + '@' + j return i + '@' + j
} }
throw new Error('invalid jid pair: ' + i + ', ' + j) throw new Error('invalid jid pair: ' + i + ', ' + j)
case Tags.HEX_8: case Tags.HEX_8:
case Tags.NIBBLE_8: case Tags.NIBBLE_8:
@@ -154,6 +175,7 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
throw new Error('invalid string with tag: ' + tag) throw new Error('invalid string with tag: ' + tag)
} }
} }
const readList = (tag: number) => ( const readList = (tag: number) => (
[...new Array(readListSize(tag))].map(() => decode(buffer, indexRef)) [...new Array(readListSize(tag))].map(() => decode(buffer, indexRef))
) )
@@ -162,6 +184,7 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
if(n < 0 || n > DoubleByteTokens.length) { if(n < 0 || n > DoubleByteTokens.length) {
throw new Error('invalid double token index: ' + n) throw new Error('invalid double token index: ' + n)
} }
return DoubleByteTokens[n] return DoubleByteTokens[n]
} }
@@ -170,6 +193,7 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
if(descrTag === Tags.STREAM_END) { if(descrTag === Tags.STREAM_END) {
throw new Error('unexpected stream end') throw new Error('unexpected stream end')
} }
const header = readString(descrTag) const header = readString(descrTag)
const attrs: BinaryNode['attrs'] = { } const attrs: BinaryNode['attrs'] = { }
let data: BinaryNode['content'] let data: BinaryNode['content']
@@ -206,6 +230,7 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
decoded = readString(tag) decoded = readString(tag)
break break
} }
data = decoded data = decoded
} }
} }
@@ -227,6 +252,7 @@ const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => {
buffer.push((value >> (curShift * 8)) & 0xff) buffer.push((value >> (curShift * 8)) & 0xff)
} }
} }
const pushBytes = (bytes: Uint8Array | Buffer | number[]) => ( const pushBytes = (bytes: Uint8Array | Buffer | number[]) => (
bytes.forEach (b => buffer.push(b)) bytes.forEach (b => buffer.push(b))
) )
@@ -234,7 +260,9 @@ const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => {
pushBytes([(value >> 16) & 0x0f, (value >> 8) & 0xff, value & 0xff]) pushBytes([(value >> 16) & 0x0f, (value >> 8) & 0xff, value & 0xff])
) )
const writeByteLength = (length: number) => { const writeByteLength = (length: number) => {
if (length >= 4294967296) throw new Error('string too large to encode: ' + length) if(length >= 4294967296) {
throw new Error('string too large to encode: ' + length)
}
if(length >= 1 << 20) { if(length >= 1 << 20) {
pushByte(Tags.BINARY_32) pushByte(Tags.BINARY_32)
@@ -247,11 +275,13 @@ const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => {
pushByte(length) pushByte(length)
} }
} }
const writeStringRaw = (str: string) => { const writeStringRaw = (str: string) => {
const bytes = Buffer.from (str, 'utf-8') const bytes = Buffer.from (str, 'utf-8')
writeByteLength(bytes.length) writeByteLength(bytes.length)
pushBytes(bytes) pushBytes(bytes)
} }
const writeToken = (token: number) => { const writeToken = (token: number) => {
if(token < 245) { if(token < 245) {
pushByte(token) pushByte(token)
@@ -259,8 +289,11 @@ const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => {
throw new Error('invalid token') throw new Error('invalid token')
} }
} }
const writeString = (token: string, i?: boolean) => { const writeString = (token: string, i?: boolean) => {
if (token === 'c.us') token = 's.whatsapp.net' if(token === 'c.us') {
token = 's.whatsapp.net'
}
const tokenIndex = SingleByteTokens.indexOf(token) const tokenIndex = SingleByteTokens.indexOf(token)
if(!i && token === 's.whatsapp.net') { if(!i && token === 's.whatsapp.net') {
@@ -274,6 +307,7 @@ const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => {
if(dictionaryIndex < 0 || dictionaryIndex > 3) { if(dictionaryIndex < 0 || dictionaryIndex > 3) {
throw new Error('double byte dict token out of range: ' + token + ', ' + tokenIndex) throw new Error('double byte dict token out of range: ' + token + ', ' + tokenIndex)
} }
writeToken(Tags.DICTIONARY_0 + dictionaryIndex) writeToken(Tags.DICTIONARY_0 + dictionaryIndex)
writeToken(overflow % 256) writeToken(overflow % 256)
} }
@@ -286,11 +320,13 @@ const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => {
} }
} }
} }
const writeJid = (left: string, right: string) => { const writeJid = (left: string, right: string) => {
pushByte(Tags.JID_PAIR) pushByte(Tags.JID_PAIR)
left && left.length > 0 ? writeString(left) : writeToken(Tags.LIST_EMPTY) left && left.length > 0 ? writeString(left) : writeToken(Tags.LIST_EMPTY)
writeString(right) writeString(right)
} }
const writeListStart = (listSize: number) => { const writeListStart = (listSize: number) => {
if(listSize === 0) { if(listSize === 0) {
pushByte(Tags.LIST_EMPTY) pushByte(Tags.LIST_EMPTY)
@@ -300,6 +336,7 @@ const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => {
pushBytes([Tags.LIST_16, listSize]) pushBytes([Tags.LIST_16, listSize])
} }
} }
const validAttributes = Object.keys(attrs).filter(k => ( const validAttributes = Object.keys(attrs).filter(k => (
typeof attrs[k] !== 'undefined' && attrs[k] !== null typeof attrs[k] !== 'undefined' && attrs[k] !== null
)) ))
@@ -322,7 +359,9 @@ const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => {
} else if(Array.isArray(content)) { } else if(Array.isArray(content)) {
writeListStart(content.length) writeListStart(content.length)
for(const item of content) { for(const item of content) {
if(item) encode(item, buffer) if(item) {
encode(item, buffer)
}
} }
} else if(typeof content === 'undefined' || content === null) { } else if(typeof content === 'undefined' || content === null) {

View File

@@ -0,0 +1,81 @@
import { Boom } from '@hapi/boom'
import { proto } from '../../WAProto'
import { BinaryNode } from './types'
// some extra useful utilities
export const getBinaryNodeChildren = ({ content }: BinaryNode, childTag: string) => {
if(Array.isArray(content)) {
return content.filter(item => item.tag === childTag)
}
return []
}
export const getAllBinaryNodeChildren = ({ content }: BinaryNode) => {
if(Array.isArray(content)) {
return content
}
return []
}
export const getBinaryNodeChild = ({ content }: BinaryNode, childTag: string) => {
if(Array.isArray(content)) {
return content.find(item => item.tag === childTag)
}
}
export const getBinaryNodeChildBuffer = (node: BinaryNode, childTag: string) => {
const child = getBinaryNodeChild(node, childTag)?.content
if(Buffer.isBuffer(child) || child instanceof Uint8Array) {
return child
}
}
export const getBinaryNodeChildUInt = (node: BinaryNode, childTag: string, length: number) => {
const buff = getBinaryNodeChildBuffer(node, childTag)
if(buff) {
return bufferToUInt(buff, length)
}
}
export const assertNodeErrorFree = (node: BinaryNode) => {
const errNode = getBinaryNodeChild(node, 'error')
if(errNode) {
throw new Boom(errNode.attrs.text || 'Unknown error', { data: +errNode.attrs.code })
}
}
export const reduceBinaryNodeToDictionary = (node: BinaryNode, tag: string) => {
const nodes = getBinaryNodeChildren(node, tag)
const dict = nodes.reduce(
(dict, { attrs }) => {
dict[attrs.name || attrs.config_code] = attrs.value || attrs.config_value
return dict
}, { } as { [_: string]: string }
)
return dict
}
export const getBinaryNodeMessages = ({ content }: BinaryNode) => {
const msgs: proto.WebMessageInfo[] = []
if(Array.isArray(content)) {
for(const item of content) {
if(item.tag === 'message') {
msgs.push(proto.WebMessageInfo.decode(item.content as Buffer))
}
}
}
return msgs
}
function bufferToUInt(e: Uint8Array | Buffer, t: number) {
let a = 0
for(let i = 0; i < t; i++) {
a = 256 * a + e[i]
}
return a
}

View File

@@ -319,6 +319,7 @@ export const getBinaryNodeMessages = ({ content }: BinaryNode) => {
return msgs return msgs
} }
export * from './generic-utils'
export * from './jid-utils' export * from './jid-utils'
export { Binary } from '../../WABinary/Binary' export { Binary } from '../../WABinary/Binary'
export * from './types' export * from './types'

View File

@@ -1,7 +1,7 @@
export const S_WHATSAPP_NET = '@s.whatsapp.net' export const S_WHATSAPP_NET = '@s.whatsapp.net'
export const OFFICIAL_BIZ_JID = '16505361212@c.us' export const OFFICIAL_BIZ_JID = '16505361212@c.us'
export const SERVER_JID = 'server@c.us' export const SERVER_JID = 'server@c.us'
export const PSA_WID = '0@c.us'; export const PSA_WID = '0@c.us'
export const STORIES_JID = 'status@broadcast' export const STORIES_JID = 'status@broadcast'
export type JidServer = 'c.us' | 'g.us' | 'broadcast' | 's.whatsapp.net' | 'call' export type JidServer = 'c.us' | 'g.us' | 'broadcast' | 's.whatsapp.net' | 'call'
@@ -16,10 +16,11 @@ export const jidEncode = (user: string | number | null, server: JidServer, devic
} }
export const jidDecode = (jid: string) => { export const jidDecode = (jid: string) => {
let sepIdx = typeof jid === 'string' ? jid.indexOf('@') : -1 const sepIdx = typeof jid === 'string' ? jid.indexOf('@') : -1
if(sepIdx < 0) { if(sepIdx < 0) {
return undefined return undefined
} }
const server = jid.slice(sepIdx+1) const server = jid.slice(sepIdx+1)
const userCombined = jid.slice(0, sepIdx) const userCombined = jid.slice(0, sepIdx)
@@ -33,6 +34,7 @@ export const jidDecode = (jid: string) => {
device: device ? +device : undefined device: device ? +device : undefined
} }
} }
/** is the jid a user */ /** is the jid a user */
export const areJidsSameUser = (jid1: string, jid2: string) => ( export const areJidsSameUser = (jid1: string, jid2: string) => (
jidDecode(jid1)?.user === jidDecode(jid2)?.user jidDecode(jid1)?.user === jidDecode(jid2)?.user

View File

@@ -1,5 +1,5 @@
import makeWASocket from './Socket'
import makeWALegacySocket from './LegacySocket' import makeWALegacySocket from './LegacySocket'
import makeWASocket from './Socket'
export * from '../WAProto' export * from '../WAProto'
export * from './Utils' export * from './Utils'

1026
yarn.lock

File diff suppressed because it is too large Load Diff