mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
chore: add linting
This commit is contained in:
6
.eslintignore
Normal file
6
.eslintignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Ignore artifacts:
|
||||||
|
lib
|
||||||
|
coverage
|
||||||
|
*.lock
|
||||||
|
.eslintrc.json
|
||||||
|
src/WABinary/index.ts
|
||||||
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "@adiwajshing"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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>) => (
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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'))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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] })
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
0
src/Store/make-ordered-dictionary.ts
Normal file
0
src/Store/make-ordered-dictionary.ts
Normal 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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Contact } from "./Contact"
|
import { Contact } from './Contact'
|
||||||
|
|
||||||
export type WAConnectionState = 'open' | 'connecting' | 'close'
|
export type WAConnectionState = 'open' | 'connecting' | 'close'
|
||||||
|
|
||||||
|
|||||||
@@ -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> & {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|
||||||
|
|||||||
81
src/WABinary/generic-utils.ts
Normal file
81
src/WABinary/generic-utils.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user