From 8f11f0be76d17937c28be236bd2e66e43789b0f3 Mon Sep 17 00:00:00 2001 From: Adhiraj Singh Date: Wed, 19 Jan 2022 15:54:02 +0530 Subject: [PATCH] chore: add linting --- .eslintignore | 6 + .eslintrc.json | 3 + Example/example.ts | 6 +- package.json | 7 +- src/Defaults/index.ts | 46 +- src/LegacySocket/auth.ts | 149 ++-- src/LegacySocket/chats.ts | 162 ++-- src/LegacySocket/groups.ts | 75 +- src/LegacySocket/index.ts | 2 +- src/LegacySocket/messages.ts | 304 ++++--- src/LegacySocket/socket.ts | 579 ++++++------ src/Socket/chats.ts | 1216 +++++++++++++------------- src/Socket/groups.ts | 50 +- src/Socket/index.ts | 2 +- src/Socket/messages-recv.ts | 881 ++++++++++--------- src/Socket/messages-send.ts | 780 +++++++++-------- src/Socket/socket.ts | 918 +++++++++---------- src/Store/make-ordered-dictionary.ts | 0 src/Tests/test.media-download.ts | 10 +- src/Types/Auth.ts | 4 +- src/Types/Chat.ts | 2 +- src/Types/Events.ts | 8 +- src/Types/GroupMetadata.ts | 2 +- src/Types/Legacy.ts | 6 +- src/Types/Message.ts | 11 +- src/Types/Socket.ts | 8 +- src/Types/State.ts | 2 +- src/Types/index.ts | 3 +- src/Utils/auth-utils.ts | 30 +- src/Utils/chat-utils.ts | 838 +++++++++--------- src/Utils/crypto.ts | 85 +- src/Utils/decode-wa-message.ts | 200 +++-- src/Utils/generics.ts | 322 +++---- src/Utils/history.ts | 81 +- src/Utils/legacy-msgs.ts | 251 +++--- src/Utils/lt-hash.ts | 30 +- src/Utils/make-mutex.ts | 17 +- src/Utils/messages-media.ts | 814 +++++++++-------- src/Utils/messages.ts | 143 +-- src/Utils/noise-handler.ts | 44 +- src/Utils/signal.ts | 46 +- src/Utils/validate-connection.ts | 232 ++--- src/WABinary/Legacy/index.ts | 587 +++++++------ src/WABinary/generic-utils.ts | 81 ++ src/WABinary/index.ts | 1 + src/WABinary/jid-utils.ts | 40 +- src/WABinary/types.ts | 2 +- src/index.ts | 2 +- yarn.lock | 1026 +++++++++++++++++++++- 49 files changed, 5800 insertions(+), 4314 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 src/Store/make-ordered-dictionary.ts create mode 100644 src/WABinary/generic-utils.ts diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..1d9dd7b --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +# Ignore artifacts: +lib +coverage +*.lock +.eslintrc.json +src/WABinary/index.ts \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..7d37e83 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "@adiwajshing" +} \ No newline at end of file diff --git a/Example/example.ts b/Example/example.ts index cb63358..7dcc2be 100644 --- a/Example/example.ts +++ b/Example/example.ts @@ -31,6 +31,10 @@ const startSock = () => { 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 => { 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('presence.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) => { const { connection, lastDisconnect } = update diff --git a/package.json b/package.json index aba51f0..50d112b 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,15 @@ "scripts": { "test": "jest", "prepare": "tsc", - "lint": "eslint '*/*.ts' --quiet --fix", "build:all": "tsc && typedoc", "build:docs": "typedoc", "build:tsc": "tsc", "example": "node --inspect -r ts-node/register Example/example.ts", "example:legacy": "node --inspect -r ts-node/register Example/example-legacy.ts", "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", "license": "MIT", @@ -56,12 +57,14 @@ "WABinary/*.js" ], "devDependencies": { + "@adiwajshing/eslint-config": "git+https://github.com/adiwajshing/eslint-config", "@types/got": "^9.6.11", "@types/jest": "^26.0.24", "@types/node": "^14.6.2", "@types/pino": "^6.3.2", "@types/sharp": "^0.29.4", "@types/ws": "^8.0.0", + "eslint": "^7.0.0", "jest": "^27.0.6", "jimp": "^0.16.1", "qrcode-terminal": "^0.12.0", diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index db81210..1d247f9 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -1,6 +1,6 @@ -import P from "pino" -import type { MediaType, SocketConfig, LegacySocketConfig, CommonSocketConfig } from "../Types" -import { Browsers } from "../Utils" +import P from 'pino' +import type { CommonSocketConfig, LegacySocketConfig, MediaType, SocketConfig } from '../Types' +import { Browsers } from '../Utils' export const UNAUTHORIZED_CODES = [401, 403, 419] @@ -18,40 +18,40 @@ export const NOISE_WA_HEADER = new Uint8Array([87, 65, 5, 2]) // last is "DICT_V export const URL_REGEX = /[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)?/gi const BASE_CONNECTION_CONFIG: CommonSocketConfig = { - version: [2, 2147, 16], + version: [2, 2147, 16], browser: Browsers.baileys('Chrome'), - waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat', - connectTimeoutMs: 20_000, - keepAliveIntervalMs: 25_000, - logger: P().child({ class: 'baileys' }), + waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat', + connectTimeoutMs: 20_000, + keepAliveIntervalMs: 25_000, + logger: P().child({ class: 'baileys' }), printQRInTerminal: false, - emitOwnEvents: true, - defaultQueryTimeoutMs: 60_000, - customUploadHosts: [], + emitOwnEvents: true, + defaultQueryTimeoutMs: 60_000, + customUploadHosts: [], } export const DEFAULT_CONNECTION_CONFIG: SocketConfig = { ...BASE_CONNECTION_CONFIG, - waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat', - getMessage: async() => undefined + waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat', + getMessage: async() => undefined } export const DEFAULT_LEGACY_CONNECTION_CONFIG: LegacySocketConfig = { ...BASE_CONNECTION_CONFIG, - waWebSocketUrl: 'wss://web.whatsapp.com/ws', - phoneResponseTimeMs: 20_000, - expectResponseTimeout: 60_000, + waWebSocketUrl: 'wss://web.whatsapp.com/ws', + phoneResponseTimeMs: 20_000, + expectResponseTimeout: 60_000, } export const MEDIA_PATH_MAP: { [T in MediaType]: string } = { - image: '/mms/image', - video: '/mms/video', - document: '/mms/document', - audio: '/mms/audio', - sticker: '/mms/image', - history: '', - 'md-app-state': '' + image: '/mms/image', + video: '/mms/video', + document: '/mms/document', + audio: '/mms/audio', + sticker: '/mms/image', + history: '', + 'md-app-state': '' } export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[] diff --git a/src/LegacySocket/auth.ts b/src/LegacySocket/auth.ts index 68b2516..6d551cd 100644 --- a/src/LegacySocket/auth.ts +++ b/src/LegacySocket/auth.ts @@ -1,8 +1,8 @@ import { Boom } from '@hapi/boom' -import EventEmitter from "events" -import { LegacyBaileysEventEmitter, LegacySocketConfig, CurveKeyPair, WAInitResponse, ConnectionState, DisconnectReason } from "../Types" -import { newLegacyAuthCreds, bindWaitForConnectionUpdate, computeChallengeResponse, validateNewConnection, Curve, printQRIfNecessaryListener } from "../Utils" -import { makeSocket } from "./socket" +import EventEmitter from 'events' +import { ConnectionState, CurveKeyPair, DisconnectReason, LegacyBaileysEventEmitter, LegacySocketConfig, WAInitResponse } from '../Types' +import { bindWaitForConnectionUpdate, computeChallengeResponse, Curve, newLegacyAuthCreds, printQRIfNecessaryListener, validateNewConnection } from '../Utils' +import { makeSocket } from './socket' const makeAuthSocket = (config: LegacySocketConfig) => { const { @@ -60,14 +60,15 @@ const makeAuthSocket = (config: LegacySocketConfig) => { * If connected, invalidates the credentials with the server */ const logout = async() => { - if(state.connection === 'open') { - await socket.sendNode({ + if(state.connection === 'open') { + await socket.sendNode({ json: ['admin', 'Conn', 'disconnect'], tag: 'goodbye' }) - } + } + // will call state update to close connection - socket?.end( + socket?.end( new Boom('Logged Out', { statusCode: DisconnectReason.loggedOut }) ) } @@ -86,13 +87,15 @@ const makeAuthSocket = (config: LegacySocketConfig) => { const qr = [ref, publicKey, authInfo.clientID].join(',') updateState({ qr }) - initTimeout = setTimeout(async () => { - if(state.connection !== 'connecting') return + initTimeout = setTimeout(async() => { + if(state.connection !== 'connecting') { + return + } logger.debug('regenerating QR') try { // request new QR - const {ref: newRef, ttl: newTTL} = await socket.query({ + const { ref: newRef, ttl: newTTL } = await socket.query({ json: ['admin', 'Conn', 'reref'], expect200: true, longTag: true, @@ -100,94 +103,104 @@ const makeAuthSocket = (config: LegacySocketConfig) => { }) ttl = newTTL ref = newRef - } catch (error) { - logger.error({ error }, `error in QR gen`) - if (error.output?.statusCode === 429) { // too many QR requests + } catch(error) { + logger.error({ error }, 'error in QR gen') + if(error.output?.statusCode === 429) { // too many QR requests socket.end(error) return } } + qrGens += 1 qrLoop(ttl) }, ttl || 20_000) // default is 20s, on the off-chance ttl is not present } + qrLoop(ttl) } + const onOpen = async() => { const canDoLogin = canLogin() - const initQuery = (async () => { - const {ref, ttl} = await socket.query({ - json: ['admin', 'init', version, browser, authInfo.clientID, true], - expect200: true, - longTag: true, - requiresPhoneConnection: false - }) as WAInitResponse + const initQuery = (async() => { + const { ref, ttl } = await socket.query({ + json: ['admin', 'init', version, browser, authInfo.clientID, true], + expect200: true, + longTag: true, + requiresPhoneConnection: false + }) as WAInitResponse - if (!canDoLogin) { - generateKeysForAuth(ref, ttl) - } - })(); - let loginTag: string - if(canDoLogin) { + if(!canDoLogin) { + generateKeysForAuth(ref, ttl) + } + })() + let loginTag: string + if(canDoLogin) { updateEncKeys() - // if we have the info to restore a closed session - const json = [ + // if we have the info to restore a closed session + const json = [ 'admin', - 'login', - authInfo.clientToken, - authInfo.serverToken, - authInfo.clientID, + 'login', + authInfo.clientToken, + authInfo.serverToken, + authInfo.clientID, 'takeover' - ] - loginTag = socket.generateMessageTag(true) - // send login every 10s - const sendLoginReq = () => { - if(state.connection === 'open') { - logger.warn('Received login timeout req when state=open, ignoring...') - return - } - logger.info('sending login request') - socket.sendNode({ + ] + loginTag = socket.generateMessageTag(true) + // send login every 10s + const sendLoginReq = () => { + if(state.connection === 'open') { + logger.warn('Received login timeout req when state=open, ignoring...') + return + } + + logger.info('sending login request') + socket.sendNode({ json, tag: loginTag }) - initTimeout = setTimeout(sendLoginReq, 10_000) - } - sendLoginReq() - } - await initQuery + initTimeout = setTimeout(sendLoginReq, 10_000) + } - // wait for response with tag "s1" - let response = await Promise.race( - [ + sendLoginReq() + } + + await initQuery + + // wait for response with tag "s1" + let response = await Promise.race( + [ socket.waitForMessage('s1', false, undefined).promise, ...(loginTag ? [socket.waitForMessage(loginTag, false, connectTimeoutMs).promise] : []) ] - ) - initTimeout && clearTimeout(initTimeout) - initTimeout = undefined + ) + initTimeout && clearTimeout(initTimeout) + initTimeout = undefined - if(response.status && response.status !== 200) { - throw new Boom(`Unexpected error in login`, { data: response, statusCode: response.status }) - } - // if its a challenge request (we get it when logging in) - if(response[1]?.challenge) { + if(response.status && response.status !== 200) { + throw new Boom('Unexpected error in login', { data: response, statusCode: response.status }) + } + + // if its a challenge request (we get it when logging in) + if(response[1]?.challenge) { const json = computeChallengeResponse(response[1].challenge, authInfo) logger.info('resolving login challenge') await socket.query({ json, expect200: true, timeoutMs: connectTimeoutMs }) response = await socket.waitForMessage('s2', true).promise - } + } + if(!response || !response[1]) { throw new Boom('Received unexpected login response', { data: response }) } + if(response[1].type === 'upgrade_md_prod') { throw new Boom('Require multi-device edition', { statusCode: DisconnectReason.multideviceMismatch }) } - // validate the new connection - const {user, auth} = validateNewConnection(response[1], authInfo, curveKeys)// validate the connection - const isNewLogin = user.id !== state.legacy!.user?.id + + // validate the new connection + const { user, auth } = validateNewConnection(response[1], authInfo, curveKeys)// validate the connection + const isNewLogin = user.id !== state.legacy!.user?.id Object.assign(authInfo, auth) updateEncKeys() @@ -206,6 +219,7 @@ const makeAuthSocket = (config: LegacySocketConfig) => { qr: undefined }) } + ws.once('open', async() => { try { await onOpen() @@ -219,10 +233,10 @@ const makeAuthSocket = (config: LegacySocketConfig) => { } process.nextTick(() => { - ev.emit('connection.update', { + ev.emit('connection.update', { ...state }) - }) + }) return { ...socket, @@ -231,8 +245,9 @@ const makeAuthSocket = (config: LegacySocketConfig) => { ev, canLogin, logout, - /** Waits for the connection to WA to reach a state */ - waitForConnectionUpdate: bindWaitForConnectionUpdate(ev) + /** Waits for the connection to WA to reach a state */ + waitForConnectionUpdate: bindWaitForConnectionUpdate(ev) } } + export default makeAuthSocket \ No newline at end of file diff --git a/src/LegacySocket/chats.ts b/src/LegacySocket/chats.ts index bc0b88f..5e69cf3 100644 --- a/src/LegacySocket/chats.ts +++ b/src/LegacySocket/chats.ts @@ -1,7 +1,7 @@ -import { BinaryNode, jidNormalizedUser } from "../WABinary"; -import { Chat, Contact, WAPresence, PresenceData, LegacySocketConfig, WAFlag, WAMetric, WABusinessProfile, ChatModification, WAMessageKey, WAMessageUpdate, BaileysEventMap } from "../Types"; -import { debouncedTimeout, unixTimestampSeconds } from "../Utils/generics"; -import makeAuthSocket from "./auth"; +import { BaileysEventMap, Chat, ChatModification, Contact, LegacySocketConfig, PresenceData, WABusinessProfile, WAFlag, WAMessageKey, WAMessageUpdate, WAMetric, WAPresence } from '../Types' +import { debouncedTimeout, unixTimestampSeconds } from '../Utils/generics' +import { BinaryNode, jidNormalizedUser } from '../WABinary' +import makeAuthSocket from './auth' const makeChatsSocket = (config: LegacySocketConfig) => { const { logger } = config @@ -22,7 +22,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => { sendNode({ json: { tag: 'query', - attrs: {type: 'chat', epoch: epoch.toString()} + attrs: { type: 'chat', epoch: epoch.toString() } }, binaryTag: [ WAMetric.queryChat, WAFlag.ignore ] }) @@ -43,62 +43,64 @@ const makeChatsSocket = (config: LegacySocketConfig) => { const updateType = attributes.type const jid = jidNormalizedUser(attributes?.jid) - switch(updateType) { - case 'delete': - ev.emit('chats.delete', [jid]) - break - case 'clear': - if(node.content) { - const ids = (node.content as BinaryNode[]).map( - ({ attrs }) => attrs.index - ) - ev.emit('messages.delete', { keys: ids.map(id => ({ id, remoteJid: jid })) }) - } else { - ev.emit('messages.delete', { jid, all: true }) - } - break - case 'archive': - ev.emit('chats.update', [ { id: jid, archive: true } ]) - break - case 'unarchive': - ev.emit('chats.update', [ { id: jid, archive: false } ]) - break - case 'pin': - ev.emit('chats.update', [ { id: jid, pin: attributes.pin ? +attributes.pin : null } ]) - break - case 'star': - case 'unstar': - const starred = updateType === 'star' - const updates: WAMessageUpdate[] = (node.content as BinaryNode[]).map( - ({ attrs }) => ({ - key: { - remoteJid: jid, - id: attrs.index, - fromMe: attrs.owner === 'true' - }, - update: { starred } - }) + switch (updateType) { + case 'delete': + ev.emit('chats.delete', [jid]) + break + case 'clear': + if(node.content) { + const ids = (node.content as BinaryNode[]).map( + ({ attrs }) => attrs.index ) - ev.emit('messages.update', updates) - break - case 'mute': - if(attributes.mute === '0') { - ev.emit('chats.update', [{ id: jid, mute: null }]) - } else { - ev.emit('chats.update', [{ id: jid, mute: +attributes.mute }]) - } - break - default: - logger.warn({ node }, `received unrecognized chat update`) - break + ev.emit('messages.delete', { keys: ids.map(id => ({ id, remoteJid: jid })) }) + } else { + ev.emit('messages.delete', { jid, all: true }) + } + + break + case 'archive': + ev.emit('chats.update', [ { id: jid, archive: true } ]) + break + case 'unarchive': + ev.emit('chats.update', [ { id: jid, archive: false } ]) + break + case 'pin': + ev.emit('chats.update', [ { id: jid, pin: attributes.pin ? +attributes.pin : null } ]) + break + case 'star': + case 'unstar': + const starred = updateType === 'star' + const updates: WAMessageUpdate[] = (node.content as BinaryNode[]).map( + ({ attrs }) => ({ + key: { + remoteJid: jid, + id: attrs.index, + fromMe: attrs.owner === 'true' + }, + update: { starred } + }) + ) + ev.emit('messages.update', updates) + break + case 'mute': + if(attributes.mute === '0') { + ev.emit('chats.update', [{ id: jid, mute: null }]) + } else { + ev.emit('chats.update', [{ id: jid, mute: +attributes.mute }]) + } + + break + default: + logger.warn({ node }, 'received unrecognized chat update') + break } } const applyingPresenceUpdate = (update: BinaryNode['attrs']): BaileysEventMap['presence.update'] => { const id = jidNormalizedUser(update.id) - const participant = jidNormalizedUser(update.participant || update.id) + const participant = jidNormalizedUser(update.participant || update.id) - const presence: PresenceData = { + const presence: PresenceData = { lastSeen: update.t ? +update.t : undefined, lastKnownPresence: update.type as WAPresence } @@ -126,27 +128,30 @@ const makeChatsSocket = (config: LegacySocketConfig) => { } ev.on('connection.update', async({ connection }) => { - if(connection !== 'open') return + if(connection !== 'open') { + return + } + try { await Promise.all([ sendNode({ - json: { tag: 'query', attrs: {type: 'contacts', epoch: '1'} }, + json: { tag: 'query', attrs: { type: 'contacts', epoch: '1' } }, binaryTag: [ WAMetric.queryContact, WAFlag.ignore ] }), sendNode({ - json: { tag: 'query', attrs: {type: 'status', epoch: '1'} }, + json: { tag: 'query', attrs: { type: 'status', epoch: '1' } }, binaryTag: [ WAMetric.queryStatus, WAFlag.ignore ] }), sendNode({ - json: { tag: 'query', attrs: {type: 'quick_reply', epoch: '1'} }, + json: { tag: 'query', attrs: { type: 'quick_reply', epoch: '1' } }, binaryTag: [ WAMetric.queryQuickReply, WAFlag.ignore ] }), sendNode({ - json: { tag: 'query', attrs: {type: 'label', epoch: '1'} }, + json: { tag: 'query', attrs: { type: 'label', epoch: '1' } }, binaryTag: [ WAMetric.queryLabel, WAFlag.ignore ] }), sendNode({ - json: { tag: 'query', attrs: {type: 'emoji', epoch: '1'} }, + json: { tag: 'query', attrs: { type: 'emoji', epoch: '1' } }, binaryTag: [ WAMetric.queryEmoji, WAFlag.ignore ] }), sendNode({ @@ -154,7 +159,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => { tag: 'action', attrs: { type: 'set', epoch: '1' }, content: [ - { tag: 'presence', attrs: {type: 'available'} } + { tag: 'presence', attrs: { type: 'available' } } ] }, binaryTag: [ WAMetric.presence, WAFlag.available ] @@ -167,7 +172,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => { logger.error(`error in sending init queries: ${error}`) } }) - socketEvents.on('CB:response,type:chat', async ({ content: data }: BinaryNode) => { + socketEvents.on('CB:response,type:chat', async({ content: data }: BinaryNode) => { chatsDebounceTimeout.cancel() if(Array.isArray(data)) { const contacts: Contact[] = [] @@ -176,6 +181,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => { if(attrs.name) { contacts.push({ id, name: attrs.name }) } + return { id: jidNormalizedUser(attrs.jid), conversationTimestamp: attrs.t ? +attrs.t : undefined, @@ -196,7 +202,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => { } }) // got all contacts from phone - socketEvents.on('CB:response,type:contacts', async ({ content: data }: BinaryNode) => { + socketEvents.on('CB:response,type:contacts', async({ content: data }: BinaryNode) => { if(Array.isArray(data)) { const contacts = data.map(({ attrs }): Contact => { return { @@ -225,15 +231,18 @@ const makeChatsSocket = (config: LegacySocketConfig) => { } }) // read updates - socketEvents.on ('CB:action,,read', async ({ content }: BinaryNode) => { + socketEvents.on ('CB:action,,read', async({ content }: BinaryNode) => { if(Array.isArray(content)) { const { attrs } = content[0] const update: Partial = { id: jidNormalizedUser(attrs.jid) } - if(attrs.type === 'false') update.unreadCount = -1 - else update.unreadCount = 0 + if(attrs.type === 'false') { + update.unreadCount = -1 + } else { + update.unreadCount = 0 + } ev.emit('chats.update', [update]) } @@ -295,7 +304,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => { * @param jid the ID of the person/group you are modifiying */ chatModify: async(modification: ChatModification, jid: string, chatInfo: Pick, timestampNow?: number) => { - let chatAttrs: BinaryNode['attrs'] = { jid: jid } + const chatAttrs: BinaryNode['attrs'] = { jid: jid } let data: BinaryNode[] | undefined = undefined timestampNow = timestampNow || unixTimestampSeconds() @@ -356,6 +365,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => { // apply it and emit events executeChatModification(node) } + return response }, /** @@ -381,7 +391,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => { * @param jid the ID of the person/group who you are updating * @param type your presence */ - sendPresenceUpdate: ( type: WAPresence, jid: string | undefined) => ( + sendPresenceUpdate: (type: WAPresence, jid: string | undefined) => ( sendNode({ binaryTag: [WAMetric.presence, WAFlag[type]], // weird stuff WA does json: { @@ -400,7 +410,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => { * Request updates on the presence of a user * this returns nothing, you'll receive updates in chats.update event * */ - presenceSubscribe: async (jid: string) => ( + presenceSubscribe: async(jid: string) => ( sendNode({ json: ['action', 'presence', 'subscribe', jid] }) ), /** Query the status of the person (see groupMetadata() for groups) */ @@ -423,11 +433,12 @@ const makeChatsSocket = (config: LegacySocketConfig) => { }, /** Updates business profile. */ updateBusinessProfile: async(profile: WABusinessProfile) => { - if (profile.business_hours?.config) { + if(profile.business_hours?.config) { profile.business_hours.business_config = 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 }) }, updateProfileName: async(name: string) => { @@ -447,6 +458,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => { } }) ev.emit('contacts.update', [{ id: user.id, name }]) } + return response }, /** @@ -454,7 +466,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => { * @param jid * @param img */ - async updateProfilePicture (jid: string, img: Buffer) { + async updateProfilePicture(jid: string, img: Buffer) { jid = jidNormalizedUser (jid) const data = { img: Buffer.from([]), preview: Buffer.from([]) } //await generateProfilePicture(img) TODO const tag = this.generateMessageTag () @@ -480,6 +492,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => { } }) } + ev.emit('contacts.update', [ { id: jid, imgUrl: eurl } ]) } }, @@ -513,8 +526,8 @@ const makeChatsSocket = (config: LegacySocketConfig) => { }] } = await query({ json: [ - "query", "businessProfile", - [ { "wid": jid.replace('@s.whatsapp.net', '@c.us') } ], + 'query', 'businessProfile', + [ { 'wid': jid.replace('@s.whatsapp.net', '@c.us') } ], 84 ], expect200: true, @@ -528,4 +541,5 @@ const makeChatsSocket = (config: LegacySocketConfig) => { } } } + export default makeChatsSocket \ No newline at end of file diff --git a/src/LegacySocket/groups.ts b/src/LegacySocket/groups.ts index ed4d975..038f21b 100644 --- a/src/LegacySocket/groups.ts +++ b/src/LegacySocket/groups.ts @@ -1,7 +1,7 @@ -import { BinaryNode, jidNormalizedUser } from "../WABinary"; -import { LegacySocketConfig, GroupModificationResponse, ParticipantAction, GroupMetadata, WAFlag, WAMetric, WAGroupCreateResponse, GroupParticipant } from "../Types"; -import { generateMessageID, unixTimestampSeconds } from "../Utils/generics"; -import makeMessagesSocket from "./messages"; +import { GroupMetadata, GroupModificationResponse, GroupParticipant, LegacySocketConfig, ParticipantAction, WAFlag, WAGroupCreateResponse, WAMetric } from '../Types' +import { generateMessageID, unixTimestampSeconds } from '../Utils/generics' +import { BinaryNode, jidNormalizedUser } from '../WABinary' +import makeMessagesSocket from './messages' const makeGroupsSocket = (config: LegacySocketConfig) => { const { logger } = config @@ -17,9 +17,9 @@ const makeGroupsSocket = (config: LegacySocketConfig) => { } = sock /** Generic function for group queries */ - const groupQuery = async(type: string, jid?: string, subject?: string, participants?: string[], additionalNodes?: BinaryNode[]) => { - const tag = generateMessageTag() - const result = await setQuery ([ + const groupQuery = async(type: string, jid?: string, subject?: string, participants?: string[], additionalNodes?: BinaryNode[]) => { + const tag = generateMessageTag() + const result = await setQuery ([ { tag: 'group', attrs: { @@ -36,12 +36,12 @@ const makeGroupsSocket = (config: LegacySocketConfig) => { additionalNodes } ], [WAMetric.group, 136], tag) - return result - } + return result + } - /** Get the metadata of the group from WA */ - const groupMetadataFull = async (jid: string) => { - const metadata = await query({ + /** Get the metadata of the group from WA */ + const groupMetadataFull = async(jid: string) => { + const metadata = await query({ json: ['query', 'GroupMetadata', jid], expect200: true }) @@ -62,13 +62,14 @@ const makeGroupsSocket = (config: LegacySocketConfig) => { } return meta - } - /** Get the metadata (works after you've left the group also) */ - const groupMetadataMinimal = async (jid: string) => { - const { attrs, content }:BinaryNode = await query({ + } + + /** Get the metadata (works after you've left the group also) */ + const groupMetadataMinimal = async(jid: string) => { + const { attrs, content }:BinaryNode = await query({ json: { tag: 'query', - attrs: {type: 'group', jid: jid, epoch: currentEpoch().toString()} + attrs: { type: 'group', jid: jid, epoch: currentEpoch().toString() } }, binaryTag: [WAMetric.group, WAFlag.ignore], expect200: true @@ -89,16 +90,17 @@ const makeGroupsSocket = (config: LegacySocketConfig) => { } } } - const meta: GroupMetadata = { - id: jid, - owner: attrs?.creator, - creation: +attrs?.create, - subject: null, - desc, - participants - } + + const meta: GroupMetadata = { + id: jid, + owner: attrs?.creator, + creation: +attrs?.create, + subject: null, + desc, + participants + } return meta - } + } socketEvents.on('CB:Chat,cmd:action', (json: BinaryNode) => { /*const data = json[1].data @@ -129,8 +131,11 @@ const makeGroupsSocket = (config: LegacySocketConfig) => { groupMetadata: async(jid: string, minimal: boolean) => { let result: GroupMetadata - if(minimal) result = await groupMetadataMinimal(jid) - else result = await groupMetadataFull(jid) + if(minimal) { + result = await groupMetadataMinimal(jid) + } else { + result = await groupMetadataFull(jid) + } return result }, @@ -139,13 +144,13 @@ const makeGroupsSocket = (config: LegacySocketConfig) => { * @param title like, the title of the group * @param participants people to include in the group */ - groupCreate: async (title: string, participants: string[]) => { + groupCreate: async(title: string, participants: string[]) => { const response = await groupQuery('create', null, title, participants) as WAGroupCreateResponse const gid = response.gid let metadata: GroupMetadata try { metadata = await groupMetadataFull(gid) - } catch (error) { + } catch(error) { logger.warn (`error in group creation: ${error}, switching gid & checking`) // if metadata is not available const comps = gid.replace ('@g.us', '').split ('-') @@ -154,6 +159,7 @@ const makeGroupsSocket = (config: LegacySocketConfig) => { metadata = await groupMetadataFull(gid) logger.warn (`group ID switched from ${gid} to ${response.gid}`) } + ev.emit('chats.upsert', [ { id: response.gid!, @@ -168,7 +174,7 @@ const makeGroupsSocket = (config: LegacySocketConfig) => { * Leave a group * @param jid the ID of the group */ - groupLeave: async (id: string) => { + groupLeave: async(id: string) => { await groupQuery('leave', id) ev.emit('chats.update', [ { id, readOnly: true } ]) }, @@ -177,7 +183,7 @@ const makeGroupsSocket = (config: LegacySocketConfig) => { * @param {string} jid the ID of the group * @param {string} title the new title of the group */ - groupUpdateSubject: async (id: string, title: string) => { + groupUpdateSubject: async(id: string, title: string) => { await groupQuery('subject', id, title) ev.emit('chats.update', [ { id, name: title } ]) ev.emit('contacts.update', [ { id, name: title } ]) @@ -188,11 +194,11 @@ const makeGroupsSocket = (config: LegacySocketConfig) => { * @param {string} jid the ID of the group * @param {string} title the new title of the group */ - groupUpdateDescription: async (jid: string, description: string) => { + groupUpdateDescription: async(jid: string, description: string) => { const metadata = await groupMetadataFull(jid) const node: BinaryNode = { tag: 'description', - attrs: {id: generateMessageID(), prev: metadata?.descId}, + attrs: { id: generateMessageID(), prev: metadata?.descId }, content: Buffer.from(description, 'utf-8') } @@ -247,4 +253,5 @@ const makeGroupsSocket = (config: LegacySocketConfig) => { } } + export default makeGroupsSocket \ No newline at end of file diff --git a/src/LegacySocket/index.ts b/src/LegacySocket/index.ts index 3f1f7bf..78caed4 100644 --- a/src/LegacySocket/index.ts +++ b/src/LegacySocket/index.ts @@ -1,5 +1,5 @@ -import { LegacySocketConfig } from '../Types' import { DEFAULT_LEGACY_CONNECTION_CONFIG } from '../Defaults' +import { LegacySocketConfig } from '../Types' import _makeLegacySocket from './groups' // export the last socket layer const makeLegacySocket = (config: Partial) => ( diff --git a/src/LegacySocket/messages.ts b/src/LegacySocket/messages.ts index d1f11a8..f9b8a8d 100644 --- a/src/LegacySocket/messages.ts +++ b/src/LegacySocket/messages.ts @@ -1,15 +1,15 @@ -import { BinaryNode, getBinaryNodeMessages, isJidGroup, jidNormalizedUser, areJidsSameUser } from "../WABinary"; 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 { toNumber, generateWAMessage, decryptMediaMessageBuffer, extractMessageContent, getWAUploadToServer } from "../Utils"; -import makeChatsSocket from "./chats"; -import { WA_DEFAULT_EPHEMERAL } from "../Defaults"; -import { proto } from "../../WAProto"; +import { proto } from '../../WAProto' +import { WA_DEFAULT_EPHEMERAL } from '../Defaults' +import { AnyMessageContent, Chat, GroupMetadata, LegacySocketConfig, MediaConnInfo, MessageInfo, MessageInfoUpdate, MessageUpdateType, MiscMessageGenerationOptions, ParticipantAction, WAFlag, WAMessage, WAMessageCursor, WAMessageKey, WAMessageStatus, WAMessageStubType, WAMessageUpdate, WAMetric, WAUrlInfo } from '../Types' +import { decryptMediaMessageBuffer, extractMessageContent, generateWAMessage, getWAUploadToServer, toNumber } from '../Utils' +import { areJidsSameUser, BinaryNode, getBinaryNodeMessages, isJidGroup, jidNormalizedUser } from '../WABinary' +import makeChatsSocket from './chats' const STATUS_MAP = { read: WAMessageStatus.READ, message: WAMessageStatus.DELIVERY_ACK, - error: WAMessageStatus.ERROR + error: WAMessageStatus.ERROR } as { [_: string]: WAMessageStatus } const makeMessagesSocket = (config: LegacySocketConfig) => { @@ -27,10 +27,10 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { let mediaConn: Promise const refreshMediaConn = async(forceGet = false) => { - let media = await mediaConn - if (!media || forceGet || (new Date().getTime()-media.fetchDate.getTime()) > media.ttl*1000) { + const media = await mediaConn + if(!media || forceGet || (new Date().getTime()-media.fetchDate.getTime()) > media.ttl*1000) { mediaConn = (async() => { - const {media_conn} = await query({ + const { media_conn } = await query({ json: ['query', 'mediaConn'], requiresPhoneConnection: false, expect200: true @@ -38,9 +38,10 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { media_conn.fetchDate = new Date() return media_conn as MediaConnInfo })() - } - return mediaConn - } + } + + return mediaConn + } const fetchMessagesFromWA = async( jid: string, @@ -51,7 +52,8 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { if(cursor) { key = 'before' in cursor ? cursor.before : cursor.after } - const { content }:BinaryNode = await query({ + + const { content }:BinaryNode = await query({ json: { tag: 'query', attrs: { @@ -71,15 +73,18 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { if(Array.isArray(content)) { return content.map(data => proto.WebMessageInfo.decode(data.content as Buffer)) } + return [] - } + } const updateMediaMessage = async(message: WAMessage) => { const content = message.message?.audioMessage || message.message?.videoMessage || message.message?.imageMessage || message.message?.stickerMessage || message.message?.documentMessage - if (!content) throw new Boom( - `given message ${message.key.id} is not a media message`, - { statusCode: 400, data: message } - ) + if(!content) { + throw new Boom( + `given message ${message.key.id} is not a media message`, + { statusCode: 400, data: message } + ) + } const response: BinaryNode = await query ({ json: { @@ -133,35 +138,36 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { } const protocolMessage = message.message?.protocolMessage || message.message?.ephemeralMessage?.message?.protocolMessage - // if it's a message to delete another message - if (protocolMessage) { - switch (protocolMessage.type) { - case proto.ProtocolMessage.ProtocolMessageType.REVOKE: - const key = protocolMessage.key - const messageStubType = WAMessageStubType.REVOKE - ev.emit('messages.update', [ - { - // the key of the deleted message is updated - update: { message: null, key: message.key, messageStubType }, - key - } - ]) - return - case proto.ProtocolMessage.ProtocolMessageType.EPHEMERAL_SETTING: - chatUpdate.ephemeralSettingTimestamp = message.messageTimestamp + // if it's a message to delete another message + if(protocolMessage) { + switch (protocolMessage.type) { + case proto.ProtocolMessage.ProtocolMessageType.REVOKE: + const key = protocolMessage.key + const messageStubType = WAMessageStubType.REVOKE + ev.emit('messages.update', [ + { + // the key of the deleted message is updated + update: { message: null, key: message.key, messageStubType }, + key + } + ]) + return + case proto.ProtocolMessage.ProtocolMessageType.EPHEMERAL_SETTING: + chatUpdate.ephemeralSettingTimestamp = message.messageTimestamp chatUpdate.ephemeralExpiration = protocolMessage.ephemeralExpiration - if(isJidGroup(jid)) { - emitGroupUpdate({ ephemeralDuration: protocolMessage.ephemeralExpiration || null }) - } - break - default: - break - } - } + if(isJidGroup(jid)) { + emitGroupUpdate({ ephemeralDuration: protocolMessage.ephemeralExpiration || null }) + } + + break + default: + break + } + } // check if the message is an action - if (message.messageStubType) { + if(message.messageStubType) { const { user } = state.legacy! //let actor = jidNormalizedUser (message.participant) let participants: string[] @@ -170,44 +176,47 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { ) switch (message.messageStubType) { - case WAMessageStubType.CHANGE_EPHEMERAL_SETTING: - chatUpdate.ephemeralSettingTimestamp = message.messageTimestamp - chatUpdate.ephemeralExpiration = +message.messageStubParameters[0] - if(isJidGroup(jid)) { - emitGroupUpdate({ ephemeralDuration: +message.messageStubParameters[0] || null }) - } - break - case WAMessageStubType.GROUP_PARTICIPANT_LEAVE: - case WAMessageStubType.GROUP_PARTICIPANT_REMOVE: - participants = message.messageStubParameters.map (jidNormalizedUser) - emitParticipantsUpdate('remove') - // mark the chat read only if you left the group - if (participants.includes(user.id)) { - chatUpdate.readOnly = true - } - break - case WAMessageStubType.GROUP_PARTICIPANT_ADD: - case WAMessageStubType.GROUP_PARTICIPANT_INVITE: - case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN: - participants = message.messageStubParameters.map (jidNormalizedUser) - if (participants.includes(user.id)) { - chatUpdate.readOnly = null - } - emitParticipantsUpdate('add') - break - case WAMessageStubType.GROUP_CHANGE_ANNOUNCE: - const announce = message.messageStubParameters[0] === 'on' - emitGroupUpdate({ announce }) - break - case WAMessageStubType.GROUP_CHANGE_RESTRICT: - const restrict = message.messageStubParameters[0] === 'on' - emitGroupUpdate({ restrict }) - break - case WAMessageStubType.GROUP_CHANGE_SUBJECT: - case WAMessageStubType.GROUP_CREATE: - chatUpdate.name = message.messageStubParameters[0] - emitGroupUpdate({ subject: chatUpdate.name }) - break + case WAMessageStubType.CHANGE_EPHEMERAL_SETTING: + chatUpdate.ephemeralSettingTimestamp = message.messageTimestamp + chatUpdate.ephemeralExpiration = +message.messageStubParameters[0] + if(isJidGroup(jid)) { + emitGroupUpdate({ ephemeralDuration: +message.messageStubParameters[0] || null }) + } + + break + case WAMessageStubType.GROUP_PARTICIPANT_LEAVE: + case WAMessageStubType.GROUP_PARTICIPANT_REMOVE: + participants = message.messageStubParameters.map (jidNormalizedUser) + emitParticipantsUpdate('remove') + // mark the chat read only if you left the group + if(participants.includes(user.id)) { + chatUpdate.readOnly = true + } + + break + case WAMessageStubType.GROUP_PARTICIPANT_ADD: + case WAMessageStubType.GROUP_PARTICIPANT_INVITE: + case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN: + participants = message.messageStubParameters.map (jidNormalizedUser) + if(participants.includes(user.id)) { + chatUpdate.readOnly = null + } + + emitParticipantsUpdate('add') + break + case WAMessageStubType.GROUP_CHANGE_ANNOUNCE: + const announce = message.messageStubParameters[0] === 'on' + emitGroupUpdate({ announce }) + break + case WAMessageStubType.GROUP_CHANGE_RESTRICT: + const restrict = message.messageStubParameters[0] === 'on' + emitGroupUpdate({ restrict }) + break + case WAMessageStubType.GROUP_CHANGE_SUBJECT: + case WAMessageStubType.GROUP_CREATE: + chatUpdate.name = message.messageStubParameters[0] + emitGroupUpdate({ subject: chatUpdate.name }) + break } } @@ -221,8 +230,8 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { const waUploadToServer = getWAUploadToServer(config, refreshMediaConn) /** Query a string to check if it has a url, if it does, return WAUrlInfo */ - const generateUrlInfo = async(text: string) => { - const response: BinaryNode = await query({ + const generateUrlInfo = async(text: string) => { + const response: BinaryNode = await query({ json: { tag: 'query', attrs: { @@ -236,14 +245,15 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { requiresPhoneConnection: false }) const urlInfo = { ...response.attrs } as any as WAUrlInfo - if(response && response.content) { - urlInfo.jpegThumbnail = response.content as Buffer - } - return urlInfo - } + if(response && response.content) { + urlInfo.jpegThumbnail = response.content as Buffer + } + + return urlInfo + } /** Relay (send) a WAMessage; more advanced functionality to send a built WA Message, you may want to stick with sendMessage() */ - const relayMessage = async(message: WAMessage, { waitForAck } = { waitForAck: true }) => { + const relayMessage = async(message: WAMessage, { waitForAck } = { waitForAck: true }) => { const json: BinaryNode = { tag: 'action', attrs: { epoch: currentEpoch().toString(), type: 'relay' }, @@ -256,35 +266,37 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { ] } const isMsgToMe = areJidsSameUser(message.key.remoteJid, state.legacy.user?.id || '') - const flag = isMsgToMe ? WAFlag.acknowledge : WAFlag.ignore // acknowledge when sending message to oneself - const mID = message.key.id + const flag = isMsgToMe ? WAFlag.acknowledge : WAFlag.ignore // acknowledge when sending message to oneself + const mID = message.key.id const finalState = isMsgToMe ? WAMessageStatus.READ : WAMessageStatus.SERVER_ACK - message.status = WAMessageStatus.PENDING - const promise = query({ - json, - binaryTag: [WAMetric.message, flag], - tag: mID, - expect200: true, - requiresPhoneConnection: true - }) + message.status = WAMessageStatus.PENDING + const promise = query({ + json, + binaryTag: [WAMetric.message, flag], + tag: mID, + expect200: true, + requiresPhoneConnection: true + }) - if(waitForAck) { - await promise + if(waitForAck) { + await promise message.status = finalState - } else { - const emitUpdate = (status: WAMessageStatus) => { - message.status = status - ev.emit('messages.update', [ { key: message.key, update: { status } } ]) - } - promise + } else { + const emitUpdate = (status: WAMessageStatus) => { + message.status = status + ev.emit('messages.update', [ { key: message.key, update: { status } } ]) + } + + promise .then(() => emitUpdate(finalState)) .catch(() => emitUpdate(WAMessageStatus.ERROR)) - } + } + if(config.emitOwnEvents) { onMessage(message, 'append') } - } + } // messages received const messagesUpdate = (node: BinaryNode, isLatest: boolean) => { @@ -330,26 +342,30 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { logger.warn({ content, key }, 'got unknown status update for message') } } + ev.emit('messages.update', updates) } } - const onMessageInfoUpdate = ([,attributes]: [string,{[_: string]: any}]) => { + + const onMessageInfoUpdate = ([, attributes]: [string, {[_: string]: any}]) => { let ids = attributes.id as string[] | string if(typeof ids === 'string') { ids = [ids] } + let updateKey: keyof MessageInfoUpdate['update'] - switch(attributes.ack.toString()) { - case '2': - updateKey = 'deliveries' - break - case '3': - updateKey = 'reads' - break - default: - logger.warn({ attributes }, `received unknown message info update`) - return + switch (attributes.ack.toString()) { + case '2': + updateKey = 'deliveries' + break + case '3': + updateKey = 'reads' + break + default: + logger.warn({ attributes }, 'received unknown message info update') + return } + const keyPartial = { remoteJid: jidNormalizedUser(attributes.to), fromMe: areJidsSameUser(attributes.from, state.legacy?.user?.id || ''), @@ -406,38 +422,43 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { const [{ attrs }] = (innerData as BinaryNode[]) const jid = jidNormalizedUser(attrs.jid) const date = new Date(+attrs.t * 1000) - switch(tag) { - case 'read': - info.reads[jid] = date - break - case 'delivery': - info.deliveries[jid] = date - break + switch (tag) { + case 'read': + info.reads[jid] = date + break + case 'delivery': + info.deliveries[jid] = date + break } } } + return info }, downloadMediaMessage: async(message: WAMessage, type: 'buffer' | 'stream' = 'buffer') => { - const downloadMediaMessage = async () => { - let mContent = extractMessageContent(message.message) - if (!mContent) throw new Boom('No message present', { statusCode: 400, data: message }) + const downloadMediaMessage = async() => { + const mContent = extractMessageContent(message.message) + if(!mContent) { + throw new Boom('No message present', { statusCode: 400, data: message }) + } const stream = await decryptMediaMessageBuffer(mContent) if(type === 'buffer') { let buffer = Buffer.from([]) - for await(const chunk of stream) { + for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]) } + return buffer } + return stream } try { const result = await downloadMediaMessage() return result - } catch (error) { + } catch(error) { if(error.message.includes('404')) { // media needs to be updated logger.info (`updating media of message: ${message.key.id}`) @@ -446,6 +467,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { const result = await downloadMediaMessage() return result } + throw error } }, @@ -453,15 +475,15 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { fetchMessagesFromWA, /** Load a single message specified by the ID */ loadMessageFromWA: async(jid: string, id: string) => { - let message: WAMessage - // load the message before the given message - let messages = (await fetchMessagesFromWA(jid, 1, { before: {id, fromMe: true} })) - if(!messages[0]) messages = (await fetchMessagesFromWA(jid, 1, { before: {id, fromMe: false} })) + let messages = (await fetchMessagesFromWA(jid, 1, { before: { id, fromMe: true } })) + if(!messages[0]) { + messages = (await fetchMessagesFromWA(jid, 1, { before: { id, fromMe: false } })) + } + // the message after the loaded message is the message required const [actual] = await fetchMessagesFromWA(jid, 1, { after: messages[0] && messages[0].key }) - message = actual - return message + return actual }, searchMessages: async(txt: string, inJid: string | null, count: number, page: number) => { const node: BinaryNode = await query({ @@ -499,8 +521,8 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { ) { const { disappearingMessagesInChat } = content const value = typeof disappearingMessagesInChat === 'boolean' ? - (disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : - disappearingMessagesInChat + (disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : + disappearingMessagesInChat const tag = generateMessageTag(true) await setQuery([ { diff --git a/src/LegacySocket/socket.ts b/src/LegacySocket/socket.ts index edce5e7..333ca99 100644 --- a/src/LegacySocket/socket.ts +++ b/src/LegacySocket/socket.ts @@ -1,11 +1,11 @@ import { Boom } from '@hapi/boom' -import { STATUS_CODES } from "http" -import { promisify } from "util" -import WebSocket from "ws" -import { BinaryNode, encodeBinaryNodeLegacy } from "../WABinary" -import { DisconnectReason, LegacySocketConfig, SocketQueryOptions, SocketSendMessageOptions, WAFlag, WAMetric, WATag } from "../Types" -import { aesEncrypt, hmacSign, promiseTimeout, unixTimestampSeconds, decodeWAMessage } from "../Utils" -import { DEFAULT_ORIGIN, DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, PHONE_CONNECTION_CB } from "../Defaults" +import { STATUS_CODES } from 'http' +import { promisify } from 'util' +import WebSocket from 'ws' +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 { aesEncrypt, decodeWAMessage, hmacSign, promiseTimeout, unixTimestampSeconds } from '../Utils' +import { BinaryNode, encodeBinaryNodeLegacy } from '../WABinary' /** * Connects to WA servers and performs: @@ -14,13 +14,13 @@ import { DEFAULT_ORIGIN, DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, PHONE_CONNECTION_C * - query phone connection */ export const makeSocket = ({ - waWebSocketUrl, - connectTimeoutMs, - phoneResponseTimeMs, - logger, - agent, - keepAliveIntervalMs, - expectResponseTimeout, + waWebSocketUrl, + connectTimeoutMs, + phoneResponseTimeMs, + logger, + agent, + keepAliveIntervalMs, + expectResponseTimeout, }: LegacySocketConfig) => { // for generating tags const referenceDateSeconds = unixTimestampSeconds(new Date()) @@ -37,33 +37,35 @@ export const makeSocket = ({ 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits', } }) - ws.setMaxListeners(0) + ws.setMaxListeners(0) let lastDateRecv: Date let epoch = 0 let authInfo: { encKey: Buffer, macKey: Buffer } let keepAliveReq: NodeJS.Timeout - let phoneCheckInterval: NodeJS.Timeout - let phoneCheckListeners = 0 + let phoneCheckInterval: NodeJS.Timeout + let phoneCheckListeners = 0 - const phoneConnectionChanged = (value: boolean) => { - ws.emit('phone-connection', { value }) - } + const phoneConnectionChanged = (value: boolean) => { + ws.emit('phone-connection', { value }) + } const sendPromise = promisify(ws.send) /** generate message tag and increment epoch */ const generateMessageTag = (longTag: boolean = false) => { - const tag = `${longTag ? referenceDateSeconds : (referenceDateSeconds%1000)}.--${epoch}` - epoch += 1 // increment message count, it makes the 'epoch' field when sending binary messages - return tag - } - const sendRawMessage = (data: Buffer | string) => { - if(ws.readyState !== ws.OPEN) { - throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) - } + const tag = `${longTag ? referenceDateSeconds : (referenceDateSeconds%1000)}.--${epoch}` + epoch += 1 // increment message count, it makes the 'epoch' field when sending binary messages + return tag + } + + const sendRawMessage = (data: Buffer | string) => { + if(ws.readyState !== ws.OPEN) { + throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) + } + + return sendPromise.call(ws, data) as Promise + } - return sendPromise.call(ws, data) as Promise - } /** * Send a message to the WA servers * @returns the tag attached in the message @@ -73,17 +75,19 @@ export const makeSocket = ({ ) => { tag = tag || generateMessageTag(longTag) let data: Buffer | string - if(logger.level === 'trace') { - logger.trace({ tag, fromMe: true, json, binaryTag }, 'communication') - } + if(logger.level === 'trace') { + logger.trace({ tag, fromMe: true, json, binaryTag }, 'communication') + } if(binaryTag) { - if(Array.isArray(json)) { - throw new Boom('Expected BinaryNode with binary code', { statusCode: 400 }) - } + if(Array.isArray(json)) { + throw new Boom('Expected BinaryNode with binary code', { statusCode: 400 }) + } + if(!authInfo) { 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 buff = aesEncrypt(binary, authInfo.encKey) // encrypt it using AES and our encKey @@ -98,242 +102,268 @@ export const makeSocket = ({ } else { data = `${tag},${JSON.stringify(json)}` } + await sendRawMessage(data) return tag } + const end = (error: Error | undefined) => { - logger.info({ error }, 'connection closed') + logger.info({ error }, 'connection closed') - ws.removeAllListeners('close') - ws.removeAllListeners('error') - ws.removeAllListeners('open') - ws.removeAllListeners('message') + ws.removeAllListeners('close') + ws.removeAllListeners('error') + ws.removeAllListeners('open') + ws.removeAllListeners('message') - phoneCheckListeners = 0 - clearInterval(keepAliveReq) - clearPhoneCheckInterval() + phoneCheckListeners = 0 + clearInterval(keepAliveReq) + clearPhoneCheckInterval() - if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) { - try { ws.close() } catch { } - } + if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) { + try { + ws.close() + } catch{ } + } - ws.emit('ws-close', error) - ws.removeAllListeners('ws-close') + ws.emit('ws-close', error) + ws.removeAllListeners('ws-close') } + const onMessageRecieved = (message: string | Buffer) => { - if(message[0] === '!' || message[0] === '!'.charCodeAt(0)) { - // when the first character in the message is an '!', the server is sending a pong frame - const timestamp = message.slice(1, message.length).toString() - lastDateRecv = new Date(parseInt(timestamp)) - ws.emit('received-pong') - } else { - let messageTag: string - let json: any - try { - const dec = decodeWAMessage(message, authInfo) - messageTag = dec[0] - json = dec[1] - if (!json) return - } catch (error) { - end(error) + if(message[0] === '!' || message[0] === '!'.charCodeAt(0)) { + // when the first character in the message is an '!', the server is sending a pong frame + const timestamp = message.slice(1, message.length).toString() + lastDateRecv = new Date(parseInt(timestamp)) + ws.emit('received-pong') + } else { + let messageTag: string + let json: any + try { + const dec = decodeWAMessage(message, authInfo) + messageTag = dec[0] + json = dec[1] + if(!json) { + return + } + } catch(error) { + end(error) return - } - //if (this.shouldLogMessages) this.messageLog.push ({ tag: messageTag, json: JSON.stringify(json), fromMe: false }) + } + //if (this.shouldLogMessages) this.messageLog.push ({ tag: messageTag, json: JSON.stringify(json), fromMe: false }) - if (logger.level === 'trace') { - logger.trace({ tag: messageTag, fromMe: false, json }, 'communication') - } + if(logger.level === 'trace') { + logger.trace({ tag: messageTag, fromMe: false, json }, 'communication') + } - let anyTriggered = false - /* Check if this is a response to a message we sent */ - anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${messageTag}`, json) - /* Check if this is a response to a message we are expecting */ - const l0 = json.tag || json[0] || '' - const l1 = json?.attrs || json?.[1] || { } - const l2 = json?.content?.[0]?.tag || json[2]?.[0] || '' + let anyTriggered = false + /* Check if this is a response to a message we sent */ + anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${messageTag}`, json) + /* Check if this is a response to a message we are expecting */ + const l0 = json.tag || json[0] || '' + const l1 = json?.attrs || json?.[1] || { } + const l2 = json?.content?.[0]?.tag || json[2]?.[0] || '' - Object.keys(l1).forEach(key => { - anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, json) || anyTriggered - anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, json) || anyTriggered - anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, json) || anyTriggered - }) - anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, json) || anyTriggered - anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, json) || anyTriggered + Object.keys(l1).forEach(key => { + anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, json) || anyTriggered + anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, json) || anyTriggered + anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, json) || anyTriggered + }) + anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, json) || anyTriggered + anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, json) || anyTriggered - if (!anyTriggered && logger.level === 'debug') { - logger.debug({ unhandled: true, tag: messageTag, fromMe: false, json }, 'communication recv') - } - } - } + if(!anyTriggered && logger.level === 'debug') { + logger.debug({ unhandled: true, tag: messageTag, fromMe: false, json }, 'communication recv') + } + } + } /** Exits a query if the phone connection is active and no response is still found */ - const exitQueryIfResponseNotExpected = (tag: string, cancel: (error: Boom) => void) => { - let timeout: NodeJS.Timeout - const listener = ([, connected]) => { - if(connected) { - timeout = setTimeout(() => { - logger.info({ tag }, `cancelling wait for message as a response is no longer expected from the phone`) + const exitQueryIfResponseNotExpected = (tag: string, cancel: (error: Boom) => void) => { + let timeout: NodeJS.Timeout + const listener = ([, connected]) => { + if(connected) { + timeout = setTimeout(() => { + 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 })) - }, expectResponseTimeout) - ws.off(PHONE_CONNECTION_CB, listener) - } - } - ws.on(PHONE_CONNECTION_CB, listener) - return () => { - ws.off(PHONE_CONNECTION_CB, listener) - timeout && clearTimeout(timeout) - } - } - /** interval is started when a query takes too long to respond */ - const startPhoneCheckInterval = () => { - phoneCheckListeners += 1 - if (!phoneCheckInterval) { - // if its been a long time and we haven't heard back from WA, send a ping - phoneCheckInterval = setInterval(() => { - if(phoneCheckListeners <= 0) { - logger.warn('phone check called without listeners') - return - } - logger.info('checking phone connection...') - sendAdminTest() + }, expectResponseTimeout) + ws.off(PHONE_CONNECTION_CB, listener) + } + } + + ws.on(PHONE_CONNECTION_CB, listener) + return () => { + ws.off(PHONE_CONNECTION_CB, listener) + timeout && clearTimeout(timeout) + } + } + + /** interval is started when a query takes too long to respond */ + const startPhoneCheckInterval = () => { + phoneCheckListeners += 1 + if(!phoneCheckInterval) { + // if its been a long time and we haven't heard back from WA, send a ping + phoneCheckInterval = setInterval(() => { + if(phoneCheckListeners <= 0) { + logger.warn('phone check called without listeners') + return + } + + logger.info('checking phone connection...') + sendAdminTest() + + phoneConnectionChanged(false) + }, phoneResponseTimeMs) + } + } + + const clearPhoneCheckInterval = () => { + phoneCheckListeners -= 1 + if(phoneCheckListeners <= 0) { + clearInterval(phoneCheckInterval) + phoneCheckInterval = undefined + phoneCheckListeners = 0 + } + } - phoneConnectionChanged(false) - }, phoneResponseTimeMs) - } - } - const clearPhoneCheckInterval = () => { - phoneCheckListeners -= 1 - if (phoneCheckListeners <= 0) { - clearInterval(phoneCheckInterval) - phoneCheckInterval = undefined - phoneCheckListeners = 0 - } - } /** checks for phone connection */ - const sendAdminTest = () => sendNode({ json: ['admin', 'test'] }) - /** + const sendAdminTest = () => sendNode({ json: ['admin', 'test'] }) + /** * Wait for a message with a certain tag to be received * @param tag the message tag to await * @param json query that was sent * @param timeoutMs timeout after which the promise will reject */ const waitForMessage = (tag: string, requiresPhoneConnection: boolean, timeoutMs?: number) => { - if(ws.readyState !== ws.OPEN) { - throw new Boom('Connection not open', { statusCode: DisconnectReason.connectionClosed }) - } + if(ws.readyState !== ws.OPEN) { + throw new Boom('Connection not open', { statusCode: DisconnectReason.connectionClosed }) + } - let cancelToken = () => { } + let cancelToken = () => { } - return { - promise: (async() => { - let onRecv: (json) => void - let onErr: (err) => void - let cancelPhoneChecker: () => void - try { - const result = await promiseTimeout(timeoutMs, - (resolve, reject) => { - onRecv = resolve - onErr = err => { - reject(err || new Boom('Intentional Close', { statusCode: DisconnectReason.connectionClosed })) - } - cancelToken = () => onErr(new Boom('Cancelled', { statusCode: 500 })) + return { + promise: (async() => { + let onRecv: (json) => void + let onErr: (err) => void + let cancelPhoneChecker: () => void + try { + const result = await promiseTimeout(timeoutMs, + (resolve, reject) => { + onRecv = resolve + onErr = err => { + reject(err || new Boom('Intentional Close', { statusCode: DisconnectReason.connectionClosed })) + } + + cancelToken = () => onErr(new Boom('Cancelled', { statusCode: 500 })) - if(requiresPhoneConnection) { - startPhoneCheckInterval() - cancelPhoneChecker = exitQueryIfResponseNotExpected(tag, onErr) - } + if(requiresPhoneConnection) { + startPhoneCheckInterval() + cancelPhoneChecker = exitQueryIfResponseNotExpected(tag, onErr) + } - ws.on(`TAG:${tag}`, onRecv) - ws.on('ws-close', onErr) // if the socket closes, you'll never receive the message - }, - ) - return result as any - } finally { - requiresPhoneConnection && clearPhoneCheckInterval() - cancelPhoneChecker && cancelPhoneChecker() + ws.on(`TAG:${tag}`, onRecv) + ws.on('ws-close', onErr) // if the socket closes, you'll never receive the message + }, + ) + return result as any + } finally { + requiresPhoneConnection && clearPhoneCheckInterval() + cancelPhoneChecker && cancelPhoneChecker() - ws.off(`TAG:${tag}`, onRecv) - ws.off('ws-close', onErr) // if the socket closes, you'll never receive the message - } - })(), - cancelToken: () => { cancelToken() } - } - } - /** + ws.off(`TAG:${tag}`, onRecv) + ws.off('ws-close', onErr) // if the socket closes, you'll never receive the message + } + })(), + cancelToken: () => { + cancelToken() + } + } + } + + /** * Query something from the WhatsApp servers * @param json the query itself * @param binaryTags the tags to attach if the query is supposed to be sent encoded in binary * @param timeoutMs timeout after which the query will be failed (set to null to disable a timeout) * @param tag the tag to attach to the message */ - const query = async( - { json, timeoutMs, expect200, tag, longTag, binaryTag, requiresPhoneConnection }: SocketQueryOptions - ) => { + const query = async( + { json, timeoutMs, expect200, tag, longTag, binaryTag, requiresPhoneConnection }: SocketQueryOptions + ) => { tag = tag || generateMessageTag(longTag) - const { promise, cancelToken } = waitForMessage(tag, requiresPhoneConnection, timeoutMs) - try { - await sendNode({ json, tag, binaryTag }) - } catch(error) { - cancelToken() - // swallow error - await promise.catch(() => { }) - // throw back the error - throw error - } + const { promise, cancelToken } = waitForMessage(tag, requiresPhoneConnection, timeoutMs) + try { + await sendNode({ json, tag, binaryTag }) + } catch(error) { + cancelToken() + // swallow error + await promise.catch(() => { }) + // throw back the error + throw error + } - const response = await promise - const responseStatusCode = +(response.status ? response.status : 200) // default status - // read here: http://getstatuscode.com/599 - if(responseStatusCode === 599) { // the connection has gone bad - end(new Boom('WA server overloaded', { statusCode: 599, data: { query: json, response } })) - } - if(expect200 && Math.floor(responseStatusCode/100) !== 2) { - const message = STATUS_CODES[responseStatusCode] || 'unknown' - throw new Boom( - `Unexpected status in '${Array.isArray(json) ? json[0] : (json?.tag || 'query')}': ${message}(${responseStatusCode})`, - { data: { query: json, response }, statusCode: response.status } - ) - } - return response - } + const response = await promise + const responseStatusCode = +(response.status ? response.status : 200) // default status + // read here: http://getstatuscode.com/599 + if(responseStatusCode === 599) { // the connection has gone bad + end(new Boom('WA server overloaded', { statusCode: 599, data: { query: json, response } })) + } + + if(expect200 && Math.floor(responseStatusCode/100) !== 2) { + const message = STATUS_CODES[responseStatusCode] || 'unknown' + throw new Boom( + `Unexpected status in '${Array.isArray(json) ? json[0] : (json?.tag || 'query')}': ${message}(${responseStatusCode})`, + { data: { query: json, response }, statusCode: response.status } + ) + } + + return response + } + const startKeepAliveRequest = () => ( - keepAliveReq = setInterval(() => { - if (!lastDateRecv) lastDateRecv = new Date() - const diff = Date.now() - lastDateRecv.getTime() - /* + keepAliveReq = setInterval(() => { + if(!lastDateRecv) { + lastDateRecv = new Date() + } + + const diff = Date.now() - lastDateRecv.getTime() + /* check if it's been a suspicious amount of time since the server responded with our last seen it could be that the network is down */ - if (diff > keepAliveIntervalMs+5000) { - end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost })) - } else if(ws.readyState === ws.OPEN) { - sendRawMessage('?,,') // if its all good, send a keep alive request - } else { - logger.warn('keep alive called when WS not open') - } - }, keepAliveIntervalMs) - ) + if(diff > keepAliveIntervalMs+5000) { + end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost })) + } else if(ws.readyState === ws.OPEN) { + sendRawMessage('?,,') // if its all good, send a keep alive request + } else { + logger.warn('keep alive called when WS not open') + } + }, keepAliveIntervalMs) + ) - const waitForSocketOpen = async() => { - if(ws.readyState === ws.OPEN) return - if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) { - throw new Boom('Connection Already Closed', { statusCode: DisconnectReason.connectionClosed }) - } - let onOpen: () => void - let onClose: (err: Error) => void - await new Promise((resolve, reject) => { - onOpen = () => resolve(undefined) - onClose = reject - ws.on('open', onOpen) - ws.on('close', onClose) - ws.on('error', onClose) - }) - .finally(() => { - ws.off('open', onOpen) - ws.off('close', onClose) - ws.off('error', onClose) - }) - } + const waitForSocketOpen = async() => { + if(ws.readyState === ws.OPEN) { + return + } + + if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) { + throw new Boom('Connection Already Closed', { statusCode: DisconnectReason.connectionClosed }) + } + + let onOpen: () => void + let onClose: (err: Error) => void + await new Promise((resolve, reject) => { + onOpen = () => resolve(undefined) + onClose = reject + ws.on('open', onOpen) + ws.on('close', onClose) + ws.on('error', onClose) + }) + .finally(() => { + ws.off('open', onOpen) + ws.off('close', onClose) + ws.off('error', onClose) + }) + } ws.on('message', onMessageRecieved) ws.on('open', () => { @@ -343,57 +373,58 @@ export const makeSocket = ({ ws.on('error', end) ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionLost }))) - ws.on(PHONE_CONNECTION_CB, json => { - if (!json[1]) { - end(new Boom('Connection terminated by phone', { statusCode: DisconnectReason.connectionLost })) - logger.info('Connection terminated by phone, closing...') - } else { - phoneConnectionChanged(true) - } - }) - ws.on('CB:Cmd,type:disconnect', json => { - const {kind} = json[1] - let reason: DisconnectReason - switch(kind) { - case 'replaced': - reason = DisconnectReason.connectionReplaced - break - default: - reason = DisconnectReason.connectionLost - break - } - end(new Boom( - `Connection terminated by server: "${kind || 'unknown'}"`, - { statusCode: reason } - )) - }) + ws.on(PHONE_CONNECTION_CB, json => { + if(!json[1]) { + end(new Boom('Connection terminated by phone', { statusCode: DisconnectReason.connectionLost })) + logger.info('Connection terminated by phone, closing...') + } else { + phoneConnectionChanged(true) + } + }) + ws.on('CB:Cmd,type:disconnect', json => { + const { kind } = json[1] + let reason: DisconnectReason + switch (kind) { + case 'replaced': + reason = DisconnectReason.connectionReplaced + break + default: + reason = DisconnectReason.connectionLost + break + } + + end(new Boom( + `Connection terminated by server: "${kind || 'unknown'}"`, + { statusCode: reason } + )) + }) return { - type: 'legacy' as 'legacy', - ws, - sendAdminTest, - updateKeys: (info: { encKey: Buffer, macKey: Buffer }) => authInfo = info, - waitForSocketOpen, + type: 'legacy' as 'legacy', + ws, + sendAdminTest, + updateKeys: (info: { encKey: Buffer, macKey: Buffer }) => authInfo = info, + waitForSocketOpen, sendNode, generateMessageTag, - waitForMessage, - query, - /** Generic function for action, set queries */ - setQuery: async(nodes: BinaryNode[], binaryTag: WATag = [WAMetric.group, WAFlag.ignore], tag?: string) => { - const json: BinaryNode = { - tag: 'action', - attrs: { epoch: epoch.toString(), type: 'set' }, - content: nodes - } + waitForMessage, + query, + /** Generic function for action, set queries */ + setQuery: async(nodes: BinaryNode[], binaryTag: WATag = [WAMetric.group, WAFlag.ignore], tag?: string) => { + const json: BinaryNode = { + tag: 'action', + attrs: { epoch: epoch.toString(), type: 'set' }, + content: nodes + } - return query({ - json, - binaryTag, - tag, - expect200: true, - requiresPhoneConnection: true - }) as Promise<{ status: number }> - }, + return query({ + json, + binaryTag, + tag, + expect200: true, + requiresPhoneConnection: true + }) as Promise<{ status: number }> + }, currentEpoch: () => epoch, end } diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index a96994a..385dd18 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -1,10 +1,10 @@ -import { SocketConfig, WAPresence, PresenceData, Chat, WAPatchCreate, WAMediaUpload, ChatMutation, WAPatchName, AppStateChunk, LTHashState, ChatModification, Contact, WABusinessProfile, WABusinessHoursConfig } from "../Types"; -import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidNormalizedUser, S_WHATSAPP_NET, reduceBinaryNodeToDictionary } from "../WABinary"; +import { Boom } from '@hapi/boom' import { proto } from '../../WAProto' -import { generateProfilePicture, toNumber, encodeSyncdPatch, decodePatches, extractSyncdPatches, chatModificationToAppPatch, decodeSyncdSnapshot, newLTHashState } from "../Utils"; -import { makeMessagesSocket } from "./messages-send"; -import makeMutex from "../Utils/make-mutex"; -import { Boom } from "@hapi/boom"; +import { AppStateChunk, Chat, ChatModification, ChatMutation, Contact, LTHashState, PresenceData, SocketConfig, WABusinessHoursConfig, WABusinessProfile, WAMediaUpload, WAPatchCreate, WAPatchName, WAPresence } from '../Types' +import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, newLTHashState, toNumber } from '../Utils' +import makeMutex from '../Utils/make-mutex' +import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary' +import { makeMessagesSocket } from './messages-send' const MAX_SYNC_ATTEMPTS = 5 @@ -15,671 +15,687 @@ export const makeChatsSocket = (config: SocketConfig) => { ev, ws, authState, - generateMessageTag, + generateMessageTag, sendNode, - query, - fetchPrivacySettings, + query, + fetchPrivacySettings, } = sock - const mutationMutex = makeMutex() + const mutationMutex = makeMutex() - const getAppStateSyncKey = async(keyId: string) => { - const { [keyId]: key } = await authState.keys.get('app-state-sync-key', [keyId]) - return key - } + const getAppStateSyncKey = async(keyId: string) => { + const { [keyId]: key } = await authState.keys.get('app-state-sync-key', [keyId]) + return key + } - const interactiveQuery = async(userNodes: BinaryNode[], queryNode: BinaryNode) => { - const result = await query({ - tag: 'iq', - attrs: { - to: S_WHATSAPP_NET, - type: 'get', - xmlns: 'usync', - }, - content: [ - { - tag: 'usync', - attrs: { - sid: generateMessageTag(), - mode: 'query', - last: 'true', - index: '0', - context: 'interactive', - }, - content: [ - { - tag: 'query', - attrs: { }, - content: [ queryNode ] - }, - { - tag: 'list', - attrs: { }, - content: userNodes - } - ] - } - ], - }) + const interactiveQuery = async(userNodes: BinaryNode[], queryNode: BinaryNode) => { + const result = await query({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + type: 'get', + xmlns: 'usync', + }, + content: [ + { + tag: 'usync', + attrs: { + sid: generateMessageTag(), + mode: 'query', + last: 'true', + index: '0', + context: 'interactive', + }, + content: [ + { + tag: 'query', + attrs: { }, + content: [ queryNode ] + }, + { + tag: 'list', + attrs: { }, + content: userNodes + } + ] + } + ], + }) - const usyncNode = getBinaryNodeChild(result, 'usync') - const listNode = getBinaryNodeChild(usyncNode, 'list') - const users = getBinaryNodeChildren(listNode, 'user') + const usyncNode = getBinaryNodeChild(result, 'usync') + const listNode = getBinaryNodeChild(usyncNode, 'list') + const users = getBinaryNodeChildren(listNode, 'user') - return users - } + return users + } - const onWhatsApp = async(...jids: string[]) => { - const results = await interactiveQuery( - [ - { - tag: 'user', - attrs: { }, - content: jids.map( - jid => ({ - tag: 'contact', - attrs: { }, - content: `+${jid}` - }) - ) - } - ], - { tag: 'contact', attrs: { } } - ) + const onWhatsApp = async(...jids: string[]) => { + const results = await interactiveQuery( + [ + { + tag: 'user', + attrs: { }, + content: jids.map( + jid => ({ + tag: 'contact', + attrs: { }, + content: `+${jid}` + }) + ) + } + ], + { tag: 'contact', attrs: { } } + ) - return results.map(user => { - const contact = getBinaryNodeChild(user, 'contact') - return { exists: contact.attrs.type === 'in', jid: user.attrs.jid } - }).filter(item => item.exists) - } + return results.map(user => { + const contact = getBinaryNodeChild(user, 'contact') + return { exists: contact.attrs.type === 'in', jid: user.attrs.jid } + }).filter(item => item.exists) + } - const fetchStatus = async(jid: string) => { - const [result] = await interactiveQuery( - [{ tag: 'user', attrs: { jid } }], - { tag: 'status', attrs: { } } - ) - if(result) { - const status = getBinaryNodeChild(result, 'status') - return { - status: status.content!.toString(), - setAt: new Date(+status.attrs.t * 1000) - } - } - } + const fetchStatus = async(jid: string) => { + const [result] = await interactiveQuery( + [{ tag: 'user', attrs: { jid } }], + { tag: 'status', attrs: { } } + ) + if(result) { + const status = getBinaryNodeChild(result, 'status') + return { + status: status.content!.toString(), + setAt: new Date(+status.attrs.t * 1000) + } + } + } - const updateProfilePicture = async(jid: string, content: WAMediaUpload) => { - const { img } = await generateProfilePicture(content) - await query({ - tag: 'iq', - attrs: { - to: jidNormalizedUser(jid), - type: 'set', - xmlns: 'w:profile:picture' - }, - content: [ - { - tag: 'picture', - attrs: { type: 'image' }, - content: img - } - ] - }) - } + const updateProfilePicture = async(jid: string, content: WAMediaUpload) => { + const { img } = await generateProfilePicture(content) + await query({ + tag: 'iq', + attrs: { + to: jidNormalizedUser(jid), + type: 'set', + xmlns: 'w:profile:picture' + }, + content: [ + { + tag: 'picture', + attrs: { type: 'image' }, + content: img + } + ] + }) + } - const fetchBlocklist = async() => { - const result = await query({ - tag: 'iq', - attrs: { - xmlns: 'blocklist', - to: S_WHATSAPP_NET, - type: 'get' - } - }) - const child = result.content?.[0] as BinaryNode - return (child.content as BinaryNode[])?.map(i => i.attrs.jid) - } + const fetchBlocklist = async() => { + const result = await query({ + tag: 'iq', + attrs: { + xmlns: 'blocklist', + to: S_WHATSAPP_NET, + type: 'get' + } + }) + const child = result.content?.[0] as BinaryNode + return (child.content as BinaryNode[])?.map(i => i.attrs.jid) + } - const updateBlockStatus = async(jid: string, action: 'block' | 'unblock') => { - await query({ - tag: 'iq', - attrs: { - xmlns: 'blocklist', - to: S_WHATSAPP_NET, - type: 'set' - }, - content: [ - { - tag: 'item', - attrs: { - action, - jid - } - } - ] - }) - } + const updateBlockStatus = async(jid: string, action: 'block' | 'unblock') => { + await query({ + tag: 'iq', + attrs: { + xmlns: 'blocklist', + to: S_WHATSAPP_NET, + type: 'set' + }, + content: [ + { + tag: 'item', + attrs: { + action, + jid + } + } + ] + }) + } - const getBusinessProfile = async (jid: string): Promise => { - const results = await query({ - tag: 'iq', - attrs: { - to: 's.whatsapp.net', - xmlns: 'w:biz', - type: 'get' - }, - content: [{ - tag: 'business_profile', - attrs: { v: '244' }, - content: [{ - tag: 'profile', - attrs: { jid } - }] - }] - }) - const profiles = getBinaryNodeChild(getBinaryNodeChild(results, 'business_profile'), 'profile') - if (!profiles) { - // if not bussines - if (logger.level == 'trace') logger.trace({ jid }, 'Not bussines') - return - } - const address = getBinaryNodeChild(profiles, 'address') - const description = getBinaryNodeChild(profiles, 'description') - const website = getBinaryNodeChild(profiles, 'website') - const email = getBinaryNodeChild(profiles, 'email') - const category = getBinaryNodeChild(getBinaryNodeChild(profiles, 'categories'), 'category') - const business_hours = getBinaryNodeChild(profiles, 'business_hours') - const business_hours_config = business_hours && getBinaryNodeChildren(business_hours, 'business_hours_config') - return { - wid: profiles.attrs?.jid, - address: address?.content.toString(), - description: description?.content.toString(), - website: [website?.content.toString()], - email: email?.content.toString(), - category: category?.content.toString(), - business_hours: { - timezone: business_hours?.attrs?.timezone, - business_config: business_hours_config?.map(({ attrs }) => attrs as unknown as WABusinessHoursConfig) - } - } as unknown as WABusinessProfile - } + const getBusinessProfile = async(jid: string): Promise => { + const results = await query({ + tag: 'iq', + attrs: { + to: 's.whatsapp.net', + xmlns: 'w:biz', + type: 'get' + }, + content: [{ + tag: 'business_profile', + attrs: { v: '244' }, + content: [{ + tag: 'profile', + attrs: { jid } + }] + }] + }) + const profiles = getBinaryNodeChild(getBinaryNodeChild(results, 'business_profile'), 'profile') + if(!profiles) { + // if not bussines + if(logger.level === 'trace') { + logger.trace({ jid }, 'Not bussines') + } - const updateAccountSyncTimestamp = async(fromTimestamp: number | string) => { - logger.info({ fromTimestamp }, 'requesting account sync') - await sendNode({ - tag: 'iq', - attrs: { - to: S_WHATSAPP_NET, - type: 'set', - xmlns: 'urn:xmpp:whatsapp:dirty', - id: generateMessageTag(), - }, - content: [ - { - tag: 'clean', - attrs: { - type: 'account_sync', - timestamp: fromTimestamp.toString(), - } - } - ] - }) - } + return + } - const resyncAppState = async(collections: WAPatchName[]) => { - const appStateChunk: AppStateChunk = {totalMutations: [], collectionsToHandle: []} - // we use this to determine which events to fire - // otherwise when we resync from scratch -- all notifications will fire - const initialVersionMap: { [T in WAPatchName]?: number } = { } + const address = getBinaryNodeChild(profiles, 'address') + const description = getBinaryNodeChild(profiles, 'description') + const website = getBinaryNodeChild(profiles, 'website') + const email = getBinaryNodeChild(profiles, 'email') + const category = getBinaryNodeChild(getBinaryNodeChild(profiles, 'categories'), 'category') + const business_hours = getBinaryNodeChild(profiles, 'business_hours') + const business_hours_config = business_hours && getBinaryNodeChildren(business_hours, 'business_hours_config') + return { + wid: profiles.attrs?.jid, + address: address?.content.toString(), + description: description?.content.toString(), + website: [website?.content.toString()], + email: email?.content.toString(), + category: category?.content.toString(), + business_hours: { + timezone: business_hours?.attrs?.timezone, + business_config: business_hours_config?.map(({ attrs }) => attrs as unknown as WABusinessHoursConfig) + } + } as unknown as WABusinessProfile + } - await authState.keys.transaction( - async() => { - const collectionsToHandle = new Set(collections) - // in case something goes wrong -- ensure we don't enter a loop that cannot be exited from - const attemptsMap = { } as { [T in WAPatchName]: number | undefined } - // keep executing till all collections are done - // sometimes a single patch request will not return all the patches (God knows why) - // so we fetch till they're all done (this is determined by the "has_more_patches" flag) - while(collectionsToHandle.size) { - const states = { } as { [T in WAPatchName]: LTHashState } - const nodes: BinaryNode[] = [] + const updateAccountSyncTimestamp = async(fromTimestamp: number | string) => { + logger.info({ fromTimestamp }, 'requesting account sync') + await sendNode({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + type: 'set', + xmlns: 'urn:xmpp:whatsapp:dirty', + id: generateMessageTag(), + }, + content: [ + { + tag: 'clean', + attrs: { + type: 'account_sync', + timestamp: fromTimestamp.toString(), + } + } + ] + }) + } - for(const name of collectionsToHandle) { - const result = await authState.keys.get('app-state-sync-version', [name]) - let state = result[name] + const resyncAppState = async(collections: WAPatchName[]) => { + const appStateChunk: AppStateChunk = { totalMutations: [], collectionsToHandle: [] } + // we use this to determine which events to fire + // otherwise when we resync from scratch -- all notifications will fire + const initialVersionMap: { [T in WAPatchName]?: number } = { } - if(state) { - if(typeof initialVersionMap[name] === 'undefined') { - initialVersionMap[name] = state.version - } - } else { - state = newLTHashState() - } + await authState.keys.transaction( + async() => { + const collectionsToHandle = new Set(collections) + // in case something goes wrong -- ensure we don't enter a loop that cannot be exited from + const attemptsMap = { } as { [T in WAPatchName]: number | undefined } + // keep executing till all collections are done + // sometimes a single patch request will not return all the patches (God knows why) + // so we fetch till they're all done (this is determined by the "has_more_patches" flag) + while(collectionsToHandle.size) { + const states = { } as { [T in WAPatchName]: LTHashState } + const nodes: BinaryNode[] = [] - states[name] = state + for(const name of collectionsToHandle) { + const result = await authState.keys.get('app-state-sync-version', [name]) + let state = result[name] - logger.info(`resyncing ${name} from v${state.version}`) + if(state) { + if(typeof initialVersionMap[name] === 'undefined') { + initialVersionMap[name] = state.version + } + } else { + state = newLTHashState() + } - nodes.push({ - tag: 'collection', - attrs: { - name, - version: state.version.toString(), - // return snapshot if being synced from scratch - return_snapshot: (!state.version).toString() - } - }) - } + states[name] = state - const result = await query({ - tag: 'iq', - attrs: { - to: S_WHATSAPP_NET, - xmlns: 'w:sync:app:state', - type: 'set' - }, - content: [ - { - tag: 'sync', - attrs: { }, - content: nodes - } - ] - }) + logger.info(`resyncing ${name} from v${state.version}`) + + nodes.push({ + tag: 'collection', + attrs: { + name, + version: state.version.toString(), + // return snapshot if being synced from scratch + return_snapshot: (!state.version).toString() + } + }) + } + + const result = await query({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + xmlns: 'w:sync:app:state', + type: 'set' + }, + content: [ + { + tag: 'sync', + attrs: { }, + content: nodes + } + ] + }) - const decoded = await extractSyncdPatches(result) // extract from binary node - for(const key in decoded) { - const name = key as WAPatchName - const { patches, hasMorePatches, snapshot } = decoded[name] - try { - if(snapshot) { - const { state: newState, mutations } = await decodeSyncdSnapshot(name, snapshot, getAppStateSyncKey, initialVersionMap[name]) - states[name] = newState + const decoded = await extractSyncdPatches(result) // extract from binary node + for(const key in decoded) { + const name = key as WAPatchName + const { patches, hasMorePatches, snapshot } = decoded[name] + try { + if(snapshot) { + const { state: newState, mutations } = await decodeSyncdSnapshot(name, snapshot, getAppStateSyncKey, initialVersionMap[name]) + states[name] = newState - logger.info(`restored state of ${name} from snapshot to v${newState.version} with ${mutations.length} mutations`) + logger.info(`restored state of ${name} from snapshot to v${newState.version} with ${mutations.length} mutations`) - await authState.keys.set({ 'app-state-sync-version': { [name]: newState } }) + await authState.keys.set({ 'app-state-sync-version': { [name]: newState } }) - appStateChunk.totalMutations.push(...mutations) - } - // only process if there are syncd patches - if(patches.length) { - const { newMutations, state: newState } = await decodePatches(name, patches, states[name], getAppStateSyncKey, initialVersionMap[name]) + appStateChunk.totalMutations.push(...mutations) + } + + // only process if there are syncd patches + if(patches.length) { + const { newMutations, state: newState } = await decodePatches(name, patches, states[name], getAppStateSyncKey, initialVersionMap[name]) - await authState.keys.set({ 'app-state-sync-version': { [name]: newState } }) + await authState.keys.set({ 'app-state-sync-version': { [name]: newState } }) - logger.info(`synced ${name} to v${newState.version}`) - if(newMutations.length) { - logger.trace({ newMutations, name }, 'recv new mutations') - } + logger.info(`synced ${name} to v${newState.version}`) + if(newMutations.length) { + logger.trace({ newMutations, name }, 'recv new mutations') + } - appStateChunk.totalMutations.push(...newMutations) - } - if(hasMorePatches) { - logger.info(`${name} has more patches...`) - } else { // collection is done with sync - collectionsToHandle.delete(name) - } - } catch(error) { - 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 } }) - // increment number of retries - attemptsMap[name] = (attemptsMap[name] || 0) + 1 - // if retry attempts overshoot - // or key not found - if(attemptsMap[name] >= MAX_SYNC_ATTEMPTS || error.output?.statusCode === 404) { - // stop retrying - collectionsToHandle.delete(name) - } - } - } - } - } - ) + appStateChunk.totalMutations.push(...newMutations) + } - processSyncActions(appStateChunk.totalMutations) + if(hasMorePatches) { + logger.info(`${name} has more patches...`) + } else { // collection is done with sync + collectionsToHandle.delete(name) + } + } catch(error) { + 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 } }) + // increment number of retries + attemptsMap[name] = (attemptsMap[name] || 0) + 1 + // if retry attempts overshoot + // or key not found + if(attemptsMap[name] >= MAX_SYNC_ATTEMPTS || error.output?.statusCode === 404) { + // stop retrying + collectionsToHandle.delete(name) + } + } + } + } + } + ) - return appStateChunk - } + processSyncActions(appStateChunk.totalMutations) - /** + return appStateChunk + } + + /** * fetch the profile picture of a user/group * type = "preview" for a low res picture * type = "image for the high res picture" */ - const profilePictureUrl = async(jid: string, type: 'preview' | 'image' = 'preview', timeoutMs?: number) => { - jid = jidNormalizedUser(jid) - const result = await query({ - tag: 'iq', - attrs: { - to: jid, - type: 'get', - xmlns: 'w:profile:picture' - }, - content: [ - { tag: 'picture', attrs: { type, query: 'url' } } - ] - }, timeoutMs) - const child = getBinaryNodeChild(result, 'picture') - return child?.attrs?.url - } + const profilePictureUrl = async(jid: string, type: 'preview' | 'image' = 'preview', timeoutMs?: number) => { + jid = jidNormalizedUser(jid) + const result = await query({ + tag: 'iq', + attrs: { + to: jid, + type: 'get', + xmlns: 'w:profile:picture' + }, + content: [ + { tag: 'picture', attrs: { type, query: 'url' } } + ] + }, timeoutMs) + const child = getBinaryNodeChild(result, 'picture') + return child?.attrs?.url + } - const sendPresenceUpdate = async(type: WAPresence, toJid?: string) => { - const me = authState.creds.me! - if(type === 'available' || type === 'unavailable') { - await sendNode({ - tag: 'presence', - attrs: { - name: me!.name, - type - } - }) - } else { - await sendNode({ - tag: 'chatstate', - attrs: { - from: me!.id!, - to: toJid, - }, - content: [ - { tag: type, attrs: { } } - ] - }) - } - } + const sendPresenceUpdate = async(type: WAPresence, toJid?: string) => { + const me = authState.creds.me! + if(type === 'available' || type === 'unavailable') { + await sendNode({ + tag: 'presence', + attrs: { + name: me!.name, + type + } + }) + } else { + await sendNode({ + tag: 'chatstate', + attrs: { + from: me!.id!, + to: toJid, + }, + content: [ + { tag: type, attrs: { } } + ] + }) + } + } - const presenceSubscribe = (toJid: string) => ( - sendNode({ - tag: 'presence', - attrs: { - to: toJid, - id: generateMessageTag(), - type: 'subscribe' - } - }) - ) + const presenceSubscribe = (toJid: string) => ( + sendNode({ + tag: 'presence', + attrs: { + to: toJid, + id: generateMessageTag(), + type: 'subscribe' + } + }) + ) - const handlePresenceUpdate = ({ tag, attrs, content }: BinaryNode) => { - let presence: PresenceData - const jid = attrs.from - const participant = attrs.participant || attrs.from - if(tag === 'presence') { - presence = { - lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available', - lastSeen: attrs.t ? +attrs.t : undefined - } - } else if(Array.isArray(content)) { - const [firstChild] = content - let type = firstChild.tag as WAPresence - if(type === 'paused') { - type = 'available' - } - presence = { lastKnownPresence: type } - } else { - logger.error({ tag, attrs, content }, 'recv invalid presence node') - } - if(presence) { - ev.emit('presence.update', { id: jid, presences: { [participant]: presence } }) - } - } + const handlePresenceUpdate = ({ tag, attrs, content }: BinaryNode) => { + let presence: PresenceData + const jid = attrs.from + const participant = attrs.participant || attrs.from + if(tag === 'presence') { + presence = { + lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available', + lastSeen: attrs.t ? +attrs.t : undefined + } + } else if(Array.isArray(content)) { + const [firstChild] = content + let type = firstChild.tag as WAPresence + if(type === 'paused') { + type = 'available' + } - const resyncMainAppState = async() => { + presence = { lastKnownPresence: type } + } else { + logger.error({ tag, attrs, content }, 'recv invalid presence node') + } + + if(presence) { + ev.emit('presence.update', { id: jid, presences: { [participant]: presence } }) + } + } + + const resyncMainAppState = async() => { - logger.debug('resyncing main app state') + logger.debug('resyncing main app state') - await ( - mutationMutex.mutex( - () => resyncAppState([ - 'critical_block', - 'critical_unblock_low', - 'regular_high', - 'regular_low', - 'regular' - ]) - ) - .catch(err => ( - logger.warn({ trace: err.stack }, 'failed to sync app state') - )) - ) - } + await ( + mutationMutex.mutex( + () => resyncAppState([ + 'critical_block', + 'critical_unblock_low', + 'regular_high', + 'regular_low', + 'regular' + ]) + ) + .catch(err => ( + logger.warn({ trace: err.stack }, 'failed to sync app state') + )) + ) + } - const processSyncActions = (actions: ChatMutation[]) => { - const updates: { [jid: string]: Partial } = {} - const contactUpdates: { [jid: string]: Contact } = {} - const msgDeletes: proto.IMessageKey[] = [] + const processSyncActions = (actions: ChatMutation[]) => { + const updates: { [jid: string]: Partial } = {} + const contactUpdates: { [jid: string]: Contact } = {} + const msgDeletes: proto.IMessageKey[] = [] - for(const { syncAction: { value: action }, index: [_, id, msgId, fromMe] } of actions) { - const update: Partial = { id } - if(action?.muteAction) { - update.mute = action.muteAction?.muted ? - toNumber(action.muteAction!.muteEndTimestamp!) : - undefined - } else if(action?.archiveChatAction) { - update.archive = !!action.archiveChatAction?.archived - } else if(action?.markChatAsReadAction) { - update.unreadCount = !!action.markChatAsReadAction?.read ? 0 : -1 - } else if(action?.clearChatAction) { - msgDeletes.push({ - remoteJid: id, - id: msgId, - fromMe: fromMe === '1' - }) - } else if(action?.contactAction) { - contactUpdates[id] = { - ...(contactUpdates[id] || {}), - id, - name: action.contactAction!.fullName - } - } else if(action?.pushNameSetting) { - const me = { - ...authState.creds.me!, - name: action?.pushNameSetting?.name! - } - ev.emit('creds.update', { me }) - } else if(action?.pinAction) { - update.pin = action.pinAction?.pinned ? toNumber(action.timestamp) : undefined - } else { - logger.warn({ action, id }, 'unprocessable update') - } + for(const { syncAction: { value: action }, index: [_, id, msgId, fromMe] } of actions) { + const update: Partial = { id } + if(action?.muteAction) { + update.mute = action.muteAction?.muted ? + toNumber(action.muteAction!.muteEndTimestamp!) : + undefined + } else if(action?.archiveChatAction) { + update.archive = !!action.archiveChatAction?.archived + } else if(action?.markChatAsReadAction) { + update.unreadCount = !!action.markChatAsReadAction?.read ? 0 : -1 + } else if(action?.clearChatAction) { + msgDeletes.push({ + remoteJid: id, + id: msgId, + fromMe: fromMe === '1' + }) + } else if(action?.contactAction) { + contactUpdates[id] = { + ...(contactUpdates[id] || {}), + id, + name: action.contactAction!.fullName + } + } else if(action?.pushNameSetting) { + const me = { + ...authState.creds.me!, + name: action?.pushNameSetting?.name! + } + ev.emit('creds.update', { me }) + } else if(action?.pinAction) { + update.pin = action.pinAction?.pinned ? toNumber(action.timestamp) : undefined + } else { + logger.warn({ action, id }, 'unprocessable update') + } - if(Object.keys(update).length > 1) { - updates[update.id] = { - ...(updates[update.id] || {}), - ...update - } - } - } + if(Object.keys(update).length > 1) { + updates[update.id] = { + ...(updates[update.id] || {}), + ...update + } + } + } - if(Object.values(updates).length) { - ev.emit('chats.update', Object.values(updates)) - } - if(Object.values(contactUpdates).length) { - ev.emit('contacts.upsert', Object.values(contactUpdates)) - } - if(msgDeletes.length) { - ev.emit('messages.delete', { keys: msgDeletes }) - } - } + if(Object.values(updates).length) { + ev.emit('chats.update', Object.values(updates)) + } - const appPatch = async(patchCreate: WAPatchCreate) => { - const name = patchCreate.type - const myAppStateKeyId = authState.creds.myAppStateKeyId - if(!myAppStateKeyId) { - throw new Boom(`App state key not present!`, { statusCode: 400 }) - } + if(Object.values(contactUpdates).length) { + ev.emit('contacts.upsert', Object.values(contactUpdates)) + } - await mutationMutex.mutex( - async() => { - logger.debug({ patch: patchCreate }, 'applying app patch') + if(msgDeletes.length) { + ev.emit('messages.delete', { keys: msgDeletes }) + } + } - await resyncAppState([name]) - const { [name]: initial } = await authState.keys.get('app-state-sync-version', [name]) - const { patch, state } = await encodeSyncdPatch( - patchCreate, - myAppStateKeyId, - initial, - getAppStateSyncKey, - ) + const appPatch = async(patchCreate: WAPatchCreate) => { + const name = patchCreate.type + const myAppStateKeyId = authState.creds.myAppStateKeyId + if(!myAppStateKeyId) { + throw new Boom('App state key not present!', { statusCode: 400 }) + } - const node: BinaryNode = { - tag: 'iq', - attrs: { - to: S_WHATSAPP_NET, - type: 'set', - xmlns: 'w:sync:app:state' - }, - content: [ - { - tag: 'sync', - attrs: { }, - content: [ - { - tag: 'collection', - attrs: { - name, - version: (state.version-1).toString(), - return_snapshot: 'false' - }, - content: [ - { - tag: 'patch', - attrs: { }, - content: proto.SyncdPatch.encode(patch).finish() - } - ] - } - ] - } - ] - } - await query(node) + await mutationMutex.mutex( + async() => { + logger.debug({ patch: patchCreate }, 'applying app patch') + + await resyncAppState([name]) + const { [name]: initial } = await authState.keys.get('app-state-sync-version', [name]) + const { patch, state } = await encodeSyncdPatch( + patchCreate, + myAppStateKeyId, + initial, + getAppStateSyncKey, + ) + + const node: BinaryNode = { + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + type: 'set', + xmlns: 'w:sync:app:state' + }, + content: [ + { + tag: 'sync', + attrs: { }, + content: [ + { + tag: 'collection', + attrs: { + name, + version: (state.version-1).toString(), + return_snapshot: 'false' + }, + content: [ + { + tag: 'patch', + attrs: { }, + content: proto.SyncdPatch.encode(patch).finish() + } + ] + } + ] + } + ] + } + await query(node) - await authState.keys.set({ 'app-state-sync-version': { [name]: state } }) + await authState.keys.set({ 'app-state-sync-version': { [name]: state } }) - if(config.emitOwnEvents) { - const result = await decodePatches(name, [{ ...patch, version: { version: state.version }, }], initial, getAppStateSyncKey) - processSyncActions(result.newMutations) - } - } - ) - } - /** sending abt props may fix QR scan fail if server expects */ - const fetchAbt = async() => { - const abtNode = await query({ - tag: 'iq', - attrs: { - to: S_WHATSAPP_NET, - xmlns: 'abt', - type: 'get', - id: generateMessageTag(), - }, - content: [ - { tag: 'props', attrs: { protocol: '1' } } - ] - }) + if(config.emitOwnEvents) { + const result = await decodePatches(name, [{ ...patch, version: { version: state.version }, }], initial, getAppStateSyncKey) + processSyncActions(result.newMutations) + } + } + ) + } - const propsNode = getBinaryNodeChild(abtNode, 'props') + /** sending abt props may fix QR scan fail if server expects */ + const fetchAbt = async() => { + const abtNode = await query({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + xmlns: 'abt', + type: 'get', + id: generateMessageTag(), + }, + content: [ + { tag: 'props', attrs: { protocol: '1' } } + ] + }) + + const propsNode = getBinaryNodeChild(abtNode, 'props') - let props: { [_: string]: string } = { } - if(propsNode) { - props = reduceBinaryNodeToDictionary(propsNode, 'prop') - } - logger.debug('fetched abt') + let props: { [_: string]: string } = { } + if(propsNode) { + props = reduceBinaryNodeToDictionary(propsNode, 'prop') + } - return props - } - /** sending non-abt props may fix QR scan fail if server expects */ - const fetchProps = async() => { - const resultNode = await query({ - tag: 'iq', - attrs: { - to: S_WHATSAPP_NET, - xmlns: 'w', - type: 'get', - id: generateMessageTag(), - }, - content: [ - { tag: 'props', attrs: { } } - ] - }) + logger.debug('fetched abt') - const propsNode = getBinaryNodeChild(resultNode, 'props') + return props + } + + /** sending non-abt props may fix QR scan fail if server expects */ + const fetchProps = async() => { + const resultNode = await query({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + xmlns: 'w', + type: 'get', + id: generateMessageTag(), + }, + content: [ + { tag: 'props', attrs: { } } + ] + }) + + const propsNode = getBinaryNodeChild(resultNode, 'props') - let props: { [_: string]: string } = { } - if(propsNode) { - props = reduceBinaryNodeToDictionary(propsNode, 'prop') - } - logger.debug('fetched props') + let props: { [_: string]: string } = { } + if(propsNode) { + props = reduceBinaryNodeToDictionary(propsNode, 'prop') + } - return props - } - /** + logger.debug('fetched props') + + return props + } + + /** * modify a chat -- mark unread, read etc. * lastMessages must be sorted in reverse chronologically * requires the last messages till the last message received; required for archive & unread */ - const chatModify = (mod: ChatModification, jid: string) => { - const patch = chatModificationToAppPatch(mod, jid) - return appPatch(patch) - } + const chatModify = (mod: ChatModification, jid: string) => { + const patch = chatModificationToAppPatch(mod, jid) + return appPatch(patch) + } - ws.on('CB:presence', handlePresenceUpdate) - ws.on('CB:chatstate', handlePresenceUpdate) + ws.on('CB:presence', handlePresenceUpdate) + ws.on('CB:chatstate', handlePresenceUpdate) - ws.on('CB:ib,,dirty', async(node: BinaryNode) => { - const { attrs } = getBinaryNodeChild(node, 'dirty') - const type = attrs.type - switch(type) { - case 'account_sync': - let { lastAccountSyncTimestamp } = authState.creds - if(lastAccountSyncTimestamp) { - await updateAccountSyncTimestamp(lastAccountSyncTimestamp) - } - lastAccountSyncTimestamp = +attrs.timestamp - ev.emit('creds.update', { lastAccountSyncTimestamp }) - break - default: - logger.info({ node }, `received unknown sync`) - break - } - }) + ws.on('CB:ib,,dirty', async(node: BinaryNode) => { + const { attrs } = getBinaryNodeChild(node, 'dirty') + const type = attrs.type + switch (type) { + case 'account_sync': + let { lastAccountSyncTimestamp } = authState.creds + if(lastAccountSyncTimestamp) { + await updateAccountSyncTimestamp(lastAccountSyncTimestamp) + } - ws.on('CB:notification,type:server_sync', (node: BinaryNode) => { - const update = getBinaryNodeChild(node, 'collection') - if(update) { - const name = update.attrs.name as WAPatchName - mutationMutex.mutex( - async() => { - await resyncAppState([name]) - .catch(err => logger.error({ trace: err.stack, node }, `failed to sync state`)) - } - ) - } - }) + lastAccountSyncTimestamp = +attrs.timestamp + ev.emit('creds.update', { lastAccountSyncTimestamp }) + break + default: + logger.info({ node }, 'received unknown sync') + break + } + }) - ev.on('connection.update', ({ connection }) => { - if(connection === 'open') { - sendPresenceUpdate('available') - fetchBlocklist() - fetchPrivacySettings() - fetchAbt() - fetchProps() - } - }) + ws.on('CB:notification,type:server_sync', (node: BinaryNode) => { + const update = getBinaryNodeChild(node, 'collection') + if(update) { + const name = update.attrs.name as WAPatchName + mutationMutex.mutex( + async() => { + await resyncAppState([name]) + .catch(err => logger.error({ trace: err.stack, node }, 'failed to sync state')) + } + ) + } + }) + + ev.on('connection.update', ({ connection }) => { + if(connection === 'open') { + sendPresenceUpdate('available') + fetchBlocklist() + fetchPrivacySettings() + fetchAbt() + fetchProps() + } + }) return { ...sock, - appPatch, + appPatch, sendPresenceUpdate, presenceSubscribe, - profilePictureUrl, - onWhatsApp, - fetchBlocklist, - fetchStatus, - updateProfilePicture, - updateBlockStatus, - getBusinessProfile, - resyncAppState, - chatModify, - resyncMainAppState, + profilePictureUrl, + onWhatsApp, + fetchBlocklist, + fetchStatus, + updateProfilePicture, + updateBlockStatus, + getBusinessProfile, + resyncAppState, + chatModify, + resyncMainAppState, } } \ No newline at end of file diff --git a/src/Socket/groups.ts b/src/Socket/groups.ts index 4c8e635..b6e8097 100644 --- a/src/Socket/groups.ts +++ b/src/Socket/groups.ts @@ -1,7 +1,7 @@ -import { generateMessageID } from "../Utils"; -import { SocketConfig, GroupMetadata, ParticipantAction } from "../Types"; -import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidEncode, jidNormalizedUser } from "../WABinary"; -import { makeSocket } from "./socket"; +import { GroupMetadata, ParticipantAction, SocketConfig } from '../Types' +import { generateMessageID } from '../Utils' +import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidEncode, jidNormalizedUser } from '../WABinary' +import { makeSocket } from './socket' export const makeGroupsSocket = (config: SocketConfig) => { const sock = makeSocket(config) @@ -9,24 +9,24 @@ export const makeGroupsSocket = (config: SocketConfig) => { const groupQuery = async(jid: string, type: 'get' | 'set', content: BinaryNode[]) => ( query({ - tag: 'iq', - attrs: { - type, - xmlns: 'w:g2', - to: jid, - }, - content - }) + tag: 'iq', + attrs: { + type, + xmlns: 'w:g2', + to: jid, + }, + content + }) ) - const groupMetadata = async(jid: string) => { - const result = await groupQuery( + const groupMetadata = async(jid: string) => { + const result = await groupQuery( jid, 'get', [ { tag: 'query', attrs: { request: 'interactive' } } ] ) - return extractGroupMetadata(result) - } + return extractGroupMetadata(result) + } return { ...sock, @@ -101,8 +101,8 @@ export const makeGroupsSocket = (config: SocketConfig) => { return participantsAffected.map(p => p.attrs.jid) }, groupUpdateDescription: async(jid: string, description?: string) => { - const metadata = await groupMetadata(jid); - const prev = metadata.descId ?? null; + const metadata = await groupMetadata(jid) + const prev = metadata.descId ?? null await groupQuery( jid, @@ -111,10 +111,10 @@ export const makeGroupsSocket = (config: SocketConfig) => { { tag: 'description', attrs: { - ...( description ? { id: generateMessageID() } : { delete: 'true' } ), + ...(description ? { id: generateMessageID() } : { delete: 'true' }), ...(prev ? { prev } : {}) }, - content: description ? [{tag: 'body', attrs: {}, content: Buffer.from(description, 'utf-8')}] : null + content: description ? [{ tag: 'body', attrs: {}, content: Buffer.from(description, 'utf-8') }] : null } ] ) @@ -124,12 +124,12 @@ export const makeGroupsSocket = (config: SocketConfig) => { const inviteNode = getBinaryNodeChild(result, 'invite') return inviteNode.attrs.code }, - groupRevokeInvite: async (jid: string) => { + groupRevokeInvite: async(jid: string) => { const result = await groupQuery(jid, 'set', [{ tag: 'invite', attrs: {} }]) const inviteNode = getBinaryNodeChild(result, 'invite') return inviteNode.attrs.code }, - groupAcceptInvite: async (code: string) => { + groupAcceptInvite: async(code: string) => { const results = await groupQuery('@g.us', 'set', [{ tag: 'invite', attrs: { code } }]) const result = getBinaryNodeChild(results, 'group') return result.attrs.jid @@ -148,8 +148,8 @@ export const makeGroupsSocket = (config: SocketConfig) => { tag: 'iq', attrs: { to: '@g.us', - xmlns: 'w:g2', - type: 'get', + xmlns: 'w:g2', + type: 'get', }, content: [ { @@ -175,6 +175,7 @@ export const makeGroupsSocket = (config: SocketConfig) => { data[meta.id] = meta } } + return data } } @@ -190,6 +191,7 @@ export const extractGroupMetadata = (result: BinaryNode) => { desc = getBinaryNodeChild(descChild, 'body')?.content as string descId = descChild.attrs.id } + const groupId = group.attrs.id.includes('@') ? group.attrs.id : jidEncode(group.attrs.id, 'g.us') const eph = getBinaryNodeChild(group, 'ephemeral')?.attrs.expiration const metadata: GroupMetadata = { diff --git a/src/Socket/index.ts b/src/Socket/index.ts index e2f7f1e..b61c19e 100644 --- a/src/Socket/index.ts +++ b/src/Socket/index.ts @@ -1,5 +1,5 @@ -import { SocketConfig } from '../Types' import { DEFAULT_CONNECTION_CONFIG } from '../Defaults' +import { SocketConfig } from '../Types' import { makeMessagesRecvSocket as _makeSocket } from './messages-recv' // export the last socket layer diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index dc3119f..7146431 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -1,24 +1,25 @@ -import { SocketConfig, WAMessageStubType, ParticipantAction, Chat, GroupMetadata } from "../Types" -import { decodeMessageStanza, encodeBigEndian, toNumber, downloadAndProcessHistorySyncNotification, generateSignalPubKey, xmppPreKey, xmppSignedPreKey } from "../Utils" -import { BinaryNode, jidDecode, jidEncode, areJidsSameUser, getBinaryNodeChildren, jidNormalizedUser, getAllBinaryNodeChildren, BinaryNodeAttributes, isJidGroup } from '../WABinary' -import { proto } from "../../WAProto" -import { KEY_BUNDLE_TYPE } from "../Defaults" -import { makeChatsSocket } from "./chats" -import { extractGroupMetadata } from "./groups" +import { proto } from '../../WAProto' +import { KEY_BUNDLE_TYPE } from '../Defaults' +import { Chat, GroupMetadata, ParticipantAction, SocketConfig, WAMessageStubType } from '../Types' +import { decodeMessageStanza, downloadAndProcessHistorySyncNotification, encodeBigEndian, generateSignalPubKey, toNumber, xmppPreKey, xmppSignedPreKey } from '../Utils' +import { areJidsSameUser, BinaryNode, BinaryNodeAttributes, getAllBinaryNodeChildren, getBinaryNodeChildren, isJidGroup, jidDecode, jidEncode, jidNormalizedUser } from '../WABinary' +import { makeChatsSocket } from './chats' +import { extractGroupMetadata } from './groups' const STATUS_MAP: { [_: string]: proto.WebMessageInfo.WebMessageInfoStatus } = { - 'played': proto.WebMessageInfo.WebMessageInfoStatus.PLAYED, - 'read': proto.WebMessageInfo.WebMessageInfoStatus.READ, - 'read-self': proto.WebMessageInfo.WebMessageInfoStatus.READ + 'played': proto.WebMessageInfo.WebMessageInfoStatus.PLAYED, + 'read': proto.WebMessageInfo.WebMessageInfoStatus.READ, + 'read-self': proto.WebMessageInfo.WebMessageInfoStatus.READ } const getStatusFromReceiptType = (type: string | undefined) => { - const status = STATUS_MAP[type] - if(typeof type === 'undefined') { - return proto.WebMessageInfo.WebMessageInfoStatus.DELIVERY_ACK - } - return status + const status = STATUS_MAP[type] + if(typeof type === 'undefined') { + return proto.WebMessageInfo.WebMessageInfoStatus.DELIVERY_ACK + } + + return status } export const makeMessagesRecvSocket = (config: SocketConfig) => { @@ -26,477 +27,501 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { const sock = makeChatsSocket(config) const { ev, - authState, + authState, ws, - assertSessions, - assertingPreKeys, + assertSessions, + assertingPreKeys, sendNode, - relayMessage, - sendReceipt, - resyncMainAppState, + relayMessage, + sendReceipt, + resyncMainAppState, } = sock - const msgRetryMap = config.msgRetryCounterMap || { } + const msgRetryMap = config.msgRetryCounterMap || { } - const historyCache = new Set() + const historyCache = new Set() - const sendMessageAck = async({ tag, attrs }: BinaryNode, extraAttrs: BinaryNodeAttributes) => { - const stanza: BinaryNode = { - tag: 'ack', - attrs: { - id: attrs.id, - to: attrs.from, - ...extraAttrs, - } - } - if(!!attrs.participant) { - stanza.attrs.participant = attrs.participant - } - logger.debug({ recv: attrs, sent: stanza.attrs }, `sent "${tag}" ack`) - await sendNode(stanza) - } + const sendMessageAck = async({ tag, attrs }: BinaryNode, extraAttrs: BinaryNodeAttributes) => { + const stanza: BinaryNode = { + tag: 'ack', + attrs: { + id: attrs.id, + to: attrs.from, + ...extraAttrs, + } + } + if(!!attrs.participant) { + stanza.attrs.participant = attrs.participant + } - const sendRetryRequest = async(node: BinaryNode) => { - const msgId = node.attrs.id - const retryCount = msgRetryMap[msgId] || 1 - if(retryCount >= 5) { - logger.debug({ retryCount, msgId }, 'reached retry limit, clearing') - delete msgRetryMap[msgId] - return - } - msgRetryMap[msgId] = retryCount+1 + logger.debug({ recv: attrs, sent: stanza.attrs }, `sent "${tag}" ack`) + await sendNode(stanza) + } - const isGroup = !!node.attrs.participant - const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds + const sendRetryRequest = async(node: BinaryNode) => { + const msgId = node.attrs.id + const retryCount = msgRetryMap[msgId] || 1 + if(retryCount >= 5) { + logger.debug({ retryCount, msgId }, 'reached retry limit, clearing') + delete msgRetryMap[msgId] + return + } + + msgRetryMap[msgId] = retryCount+1 + + const isGroup = !!node.attrs.participant + const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds - const deviceIdentity = proto.ADVSignedDeviceIdentity.encode(account).finish() - await assertingPreKeys(1, async preKeys => { - const [keyId] = Object.keys(preKeys) - const key = preKeys[+keyId] + const deviceIdentity = proto.ADVSignedDeviceIdentity.encode(account).finish() + await assertingPreKeys(1, async preKeys => { + const [keyId] = Object.keys(preKeys) + const key = preKeys[+keyId] - const decFrom = node.attrs.from ? jidDecode(node.attrs.from) : undefined - const receipt: BinaryNode = { - tag: 'receipt', - attrs: { - id: msgId, - type: 'retry', - to: isGroup ? node.attrs.from : jidEncode(decFrom!.user, 's.whatsapp.net', decFrom!.device, 0) - }, - content: [ - { - tag: 'retry', - attrs: { - count: retryCount.toString(), - id: node.attrs.id, - t: node.attrs.t, - v: '1' - } - }, - { - tag: 'registration', - attrs: { }, - content: encodeBigEndian(authState.creds.registrationId) - } - ] - } - if(node.attrs.recipient) { - receipt.attrs.recipient = node.attrs.recipient - } - if(node.attrs.participant) { - receipt.attrs.participant = node.attrs.participant - } - if(retryCount > 1) { - const exec = generateSignalPubKey(Buffer.from(KEY_BUNDLE_TYPE)).slice(0, 1); + const decFrom = node.attrs.from ? jidDecode(node.attrs.from) : undefined + const receipt: BinaryNode = { + tag: 'receipt', + attrs: { + id: msgId, + type: 'retry', + to: isGroup ? node.attrs.from : jidEncode(decFrom!.user, 's.whatsapp.net', decFrom!.device, 0) + }, + content: [ + { + tag: 'retry', + attrs: { + count: retryCount.toString(), + id: node.attrs.id, + t: node.attrs.t, + v: '1' + } + }, + { + tag: 'registration', + attrs: { }, + content: encodeBigEndian(authState.creds.registrationId) + } + ] + } + if(node.attrs.recipient) { + receipt.attrs.recipient = node.attrs.recipient + } - (receipt.content! as BinaryNode[]).push({ - tag: 'keys', - attrs: { }, - content: [ - { tag: 'type', attrs: { }, content: exec }, - { tag: 'identity', attrs: { }, content: identityKey.public }, - xmppPreKey(key, +keyId), - xmppSignedPreKey(signedPreKey), - { tag: 'device-identity', attrs: { }, content: deviceIdentity } - ] - }) - } - await sendNode(receipt) + if(node.attrs.participant) { + receipt.attrs.participant = node.attrs.participant + } - logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt') - }) - } + if(retryCount > 1) { + const exec = generateSignalPubKey(Buffer.from(KEY_BUNDLE_TYPE)).slice(0, 1); - const processMessage = async(message: proto.IWebMessageInfo, chatUpdate: Partial) => { - const protocolMsg = message.message?.protocolMessage - if(protocolMsg) { - switch(protocolMsg.type) { - case proto.ProtocolMessage.ProtocolMessageType.HISTORY_SYNC_NOTIFICATION: - const histNotification = protocolMsg!.historySyncNotification + (receipt.content! as BinaryNode[]).push({ + tag: 'keys', + attrs: { }, + content: [ + { tag: 'type', attrs: { }, content: exec }, + { tag: 'identity', attrs: { }, content: identityKey.public }, + xmppPreKey(key, +keyId), + xmppSignedPreKey(signedPreKey), + { tag: 'device-identity', attrs: { }, content: deviceIdentity } + ] + }) + } + + await sendNode(receipt) + + logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt') + }) + } + + const processMessage = async(message: proto.IWebMessageInfo, chatUpdate: Partial) => { + const protocolMsg = message.message?.protocolMessage + if(protocolMsg) { + switch (protocolMsg.type) { + case proto.ProtocolMessage.ProtocolMessageType.HISTORY_SYNC_NOTIFICATION: + const histNotification = protocolMsg!.historySyncNotification - logger.info({ histNotification, id: message.key.id }, 'got history notification') - const { chats, contacts, messages, isLatest } = await downloadAndProcessHistorySyncNotification(histNotification, historyCache) + logger.info({ histNotification, id: message.key.id }, 'got history notification') + const { chats, contacts, messages, isLatest } = await downloadAndProcessHistorySyncNotification(histNotification, historyCache) - const meJid = authState.creds.me!.id - await sendNode({ - tag: 'receipt', - attrs: { - id: message.key.id, - type: 'hist_sync', - to: jidEncode(jidDecode(meJid).user, 'c.us') - } - }) + const meJid = authState.creds.me!.id + await sendNode({ + tag: 'receipt', + attrs: { + id: message.key.id, + type: 'hist_sync', + to: jidEncode(jidDecode(meJid).user, 'c.us') + } + }) - if(chats.length) ev.emit('chats.set', { chats, isLatest }) - if(messages.length) ev.emit('messages.set', { messages, isLatest }) - if(contacts.length) ev.emit('contacts.set', { contacts }) + if(chats.length) { + ev.emit('chats.set', { chats, isLatest }) + } - break - case proto.ProtocolMessage.ProtocolMessageType.APP_STATE_SYNC_KEY_SHARE: - const keys = protocolMsg.appStateSyncKeyShare!.keys - if(keys?.length) { - let newAppStateSyncKeyId = '' - for(const { keyData, keyId } of keys) { - const strKeyId = Buffer.from(keyId.keyId!).toString('base64') + if(messages.length) { + ev.emit('messages.set', { messages, isLatest }) + } + + if(contacts.length) { + ev.emit('contacts.set', { contacts }) + } + + break + case proto.ProtocolMessage.ProtocolMessageType.APP_STATE_SYNC_KEY_SHARE: + const keys = protocolMsg.appStateSyncKeyShare!.keys + if(keys?.length) { + let newAppStateSyncKeyId = '' + for(const { keyData, keyId } of keys) { + const strKeyId = Buffer.from(keyId.keyId!).toString('base64') - logger.info({ strKeyId }, 'injecting new app state sync key') - await authState.keys.set({ 'app-state-sync-key': { [strKeyId]: keyData } }) + logger.info({ strKeyId }, 'injecting new app state sync key') + await authState.keys.set({ 'app-state-sync-key': { [strKeyId]: keyData } }) - newAppStateSyncKeyId = strKeyId - } + newAppStateSyncKeyId = strKeyId + } - ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId }) + ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId }) - resyncMainAppState() - } else [ - logger.info({ protocolMsg }, 'recv app state sync with 0 keys') - ] - break - case proto.ProtocolMessage.ProtocolMessageType.REVOKE: - ev.emit('messages.update', [ - { - key: { - ...message.key, - id: protocolMsg.key!.id - }, - update: { message: null, messageStubType: WAMessageStubType.REVOKE, key: message.key } - } - ]) - break - case proto.ProtocolMessage.ProtocolMessageType.EPHEMERAL_SETTING: - chatUpdate.ephemeralSettingTimestamp = toNumber(message.messageTimestamp) - chatUpdate.ephemeralExpiration = protocolMsg.ephemeralExpiration || null - break - } - } else if(message.messageStubType) { - const meJid = authState.creds.me!.id - const jid = message.key!.remoteJid! - //let actor = whatsappID (message.participant) - let participants: string[] - const emitParticipantsUpdate = (action: ParticipantAction) => ( - ev.emit('group-participants.update', { id: jid, participants, action }) - ) - const emitGroupUpdate = (update: Partial) => { - ev.emit('groups.update', [ { id: jid, ...update } ]) - } + resyncMainAppState() + } else { + [ + logger.info({ protocolMsg }, 'recv app state sync with 0 keys') + ] + } - switch (message.messageStubType) { - case WAMessageStubType.GROUP_PARTICIPANT_LEAVE: - case WAMessageStubType.GROUP_PARTICIPANT_REMOVE: - participants = message.messageStubParameters - emitParticipantsUpdate('remove') - // mark the chat read only if you left the group - if(participants.includes(meJid)) { - chatUpdate.readOnly = true - } - break - case WAMessageStubType.GROUP_PARTICIPANT_ADD: - case WAMessageStubType.GROUP_PARTICIPANT_INVITE: - case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN: - participants = message.messageStubParameters - if (participants.includes(meJid)) { - chatUpdate.readOnly = false - } - emitParticipantsUpdate('add') - break - case WAMessageStubType.GROUP_CHANGE_ANNOUNCE: - const announceValue = message.messageStubParameters[0] - emitGroupUpdate({ announce: announceValue === 'true' || announceValue === 'on' }) - break - case WAMessageStubType.GROUP_CHANGE_RESTRICT: - const restrictValue = message.messageStubParameters[0] - emitGroupUpdate({ restrict: restrictValue === 'true' || restrictValue === 'on' }) - break - case WAMessageStubType.GROUP_CHANGE_SUBJECT: - chatUpdate.name = message.messageStubParameters[0] - emitGroupUpdate({ subject: chatUpdate.name }) - break - } - } - } + break + case proto.ProtocolMessage.ProtocolMessageType.REVOKE: + ev.emit('messages.update', [ + { + key: { + ...message.key, + id: protocolMsg.key!.id + }, + update: { message: null, messageStubType: WAMessageStubType.REVOKE, key: message.key } + } + ]) + break + case proto.ProtocolMessage.ProtocolMessageType.EPHEMERAL_SETTING: + chatUpdate.ephemeralSettingTimestamp = toNumber(message.messageTimestamp) + chatUpdate.ephemeralExpiration = protocolMsg.ephemeralExpiration || null + break + } + } else if(message.messageStubType) { + const meJid = authState.creds.me!.id + const jid = message.key!.remoteJid! + //let actor = whatsappID (message.participant) + let participants: string[] + const emitParticipantsUpdate = (action: ParticipantAction) => ( + ev.emit('group-participants.update', { id: jid, participants, action }) + ) + const emitGroupUpdate = (update: Partial) => { + ev.emit('groups.update', [ { id: jid, ...update } ]) + } - const processNotification = (node: BinaryNode): Partial => { - const result: Partial = { } - const [child] = getAllBinaryNodeChildren(node) + switch (message.messageStubType) { + case WAMessageStubType.GROUP_PARTICIPANT_LEAVE: + case WAMessageStubType.GROUP_PARTICIPANT_REMOVE: + participants = message.messageStubParameters + emitParticipantsUpdate('remove') + // mark the chat read only if you left the group + if(participants.includes(meJid)) { + chatUpdate.readOnly = true + } - if(node.attrs.type === 'w:gp2') { - switch(child?.tag) { - case 'create': - const metadata = extractGroupMetadata(child) + break + case WAMessageStubType.GROUP_PARTICIPANT_ADD: + case WAMessageStubType.GROUP_PARTICIPANT_INVITE: + case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN: + participants = message.messageStubParameters + if(participants.includes(meJid)) { + chatUpdate.readOnly = false + } - result.messageStubType = WAMessageStubType.GROUP_CREATE - result.messageStubParameters = [metadata.subject] - result.key = { participant: metadata.owner } + emitParticipantsUpdate('add') + break + case WAMessageStubType.GROUP_CHANGE_ANNOUNCE: + const announceValue = message.messageStubParameters[0] + emitGroupUpdate({ announce: announceValue === 'true' || announceValue === 'on' }) + break + case WAMessageStubType.GROUP_CHANGE_RESTRICT: + const restrictValue = message.messageStubParameters[0] + emitGroupUpdate({ restrict: restrictValue === 'true' || restrictValue === 'on' }) + break + case WAMessageStubType.GROUP_CHANGE_SUBJECT: + chatUpdate.name = message.messageStubParameters[0] + emitGroupUpdate({ subject: chatUpdate.name }) + break + } + } + } - ev.emit('chats.upsert', [{ - id: metadata.id, - name: metadata.subject, - conversationTimestamp: metadata.creation, - }]) - ev.emit('groups.upsert', [metadata]) - break - case 'ephemeral': - case 'not_ephemeral': - result.message = { - protocolMessage: { - type: proto.ProtocolMessage.ProtocolMessageType.EPHEMERAL_SETTING, - ephemeralExpiration: +(child.attrs.expiration || 0) - } - } - break - case 'promote': - case 'demote': - case 'remove': - case 'add': - case 'leave': - const stubType = `GROUP_PARTICIPANT_${child.tag!.toUpperCase()}` - result.messageStubType = WAMessageStubType[stubType] + const processNotification = (node: BinaryNode): Partial => { + const result: Partial = { } + const [child] = getAllBinaryNodeChildren(node) - const participants = getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid) - if( - participants.length === 1 && + if(node.attrs.type === 'w:gp2') { + switch (child?.tag) { + case 'create': + const metadata = extractGroupMetadata(child) + + result.messageStubType = WAMessageStubType.GROUP_CREATE + result.messageStubParameters = [metadata.subject] + result.key = { participant: metadata.owner } + + ev.emit('chats.upsert', [{ + id: metadata.id, + name: metadata.subject, + conversationTimestamp: metadata.creation, + }]) + ev.emit('groups.upsert', [metadata]) + break + case 'ephemeral': + case 'not_ephemeral': + result.message = { + protocolMessage: { + type: proto.ProtocolMessage.ProtocolMessageType.EPHEMERAL_SETTING, + ephemeralExpiration: +(child.attrs.expiration || 0) + } + } + break + case 'promote': + case 'demote': + case 'remove': + case 'add': + case 'leave': + const stubType = `GROUP_PARTICIPANT_${child.tag!.toUpperCase()}` + result.messageStubType = WAMessageStubType[stubType] + + const participants = getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid) + if( + participants.length === 1 && // if recv. "remove" message and sender removed themselves // mark as left areJidsSameUser(participants[0], node.attrs.participant) && child.tag === 'remove' - ) { - result.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE - } - result.messageStubParameters = participants - break - case 'subject': - result.messageStubType = WAMessageStubType.GROUP_CHANGE_SUBJECT - result.messageStubParameters = [ child.attrs.subject ] - break - case 'announcement': - case 'not_announcement': - result.messageStubType = WAMessageStubType.GROUP_CHANGE_ANNOUNCE - result.messageStubParameters = [ (child.tag === 'announcement') ? 'on' : 'off' ] - break - case 'locked': - case 'unlocked': - result.messageStubType = WAMessageStubType.GROUP_CHANGE_RESTRICT - result.messageStubParameters = [ (child.tag === 'locked') ? 'on' : 'off' ] - break + ) { + result.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE + } + + result.messageStubParameters = participants + break + case 'subject': + result.messageStubType = WAMessageStubType.GROUP_CHANGE_SUBJECT + result.messageStubParameters = [ child.attrs.subject ] + break + case 'announcement': + case 'not_announcement': + result.messageStubType = WAMessageStubType.GROUP_CHANGE_ANNOUNCE + result.messageStubParameters = [ (child.tag === 'announcement') ? 'on' : 'off' ] + break + case 'locked': + case 'unlocked': + result.messageStubType = WAMessageStubType.GROUP_CHANGE_RESTRICT + result.messageStubParameters = [ (child.tag === 'locked') ? 'on' : 'off' ] + break - } - } else { - switch(child.tag) { - case 'devices': - const devices = getBinaryNodeChildren(child, 'device') - if(areJidsSameUser(child.attrs.jid, authState.creds!.me!.id)) { - const deviceJids = devices.map(d => d.attrs.jid) - logger.info({ deviceJids }, 'got my own devices') - } - break - } - } - if(Object.keys(result).length) { - return result - } - } - // recv a message - ws.on('CB:message', async(stanza: BinaryNode) => { - const msg = await decodeMessageStanza(stanza, authState) - // message failed to decrypt - if(msg.messageStubType === proto.WebMessageInfo.WebMessageInfoStubType.CIPHERTEXT) { - logger.error( - { msgId: msg.key.id, params: msg.messageStubParameters }, - 'failure in decrypting message' - ) - await sendRetryRequest(stanza) - } else { - await sendMessageAck(stanza, { class: 'receipt' }) - // no type in the receipt => message delivered - await sendReceipt(msg.key.remoteJid!, msg.key.participant, [msg.key.id!], undefined) - logger.debug({ msg: msg.key }, 'sent delivery receipt') - } + } + } else { + switch (child.tag) { + case 'devices': + const devices = getBinaryNodeChildren(child, 'device') + if(areJidsSameUser(child.attrs.jid, authState.creds!.me!.id)) { + const deviceJids = devices.map(d => d.attrs.jid) + logger.info({ deviceJids }, 'got my own devices') + } + + break + } + } + + if(Object.keys(result).length) { + return result + } + } + + // recv a message + ws.on('CB:message', async(stanza: BinaryNode) => { + const msg = await decodeMessageStanza(stanza, authState) + // message failed to decrypt + if(msg.messageStubType === proto.WebMessageInfo.WebMessageInfoStubType.CIPHERTEXT) { + logger.error( + { msgId: msg.key.id, params: msg.messageStubParameters }, + 'failure in decrypting message' + ) + await sendRetryRequest(stanza) + } else { + await sendMessageAck(stanza, { class: 'receipt' }) + // no type in the receipt => message delivered + await sendReceipt(msg.key.remoteJid!, msg.key.participant, [msg.key.id!], undefined) + logger.debug({ msg: msg.key }, 'sent delivery receipt') + } - msg.key.remoteJid = jidNormalizedUser(msg.key.remoteJid!) - ev.emit('messages.upsert', { messages: [msg], type: stanza.attrs.offline ? 'append' : 'notify' }) - }) + msg.key.remoteJid = jidNormalizedUser(msg.key.remoteJid!) + ev.emit('messages.upsert', { messages: [msg], type: stanza.attrs.offline ? 'append' : 'notify' }) + }) - ws.on('CB:ack,class:message', async(node: BinaryNode) => { - await sendNode({ - tag: 'ack', - attrs: { - class: 'receipt', - id: node.attrs.id, - from: node.attrs.from - } - }) - logger.debug({ attrs: node.attrs }, 'sending receipt for ack') - }) + ws.on('CB:ack,class:message', async(node: BinaryNode) => { + await sendNode({ + tag: 'ack', + attrs: { + class: 'receipt', + id: node.attrs.id, + from: node.attrs.from + } + }) + logger.debug({ attrs: node.attrs }, 'sending receipt for ack') + }) - ws.on('CB:call', async(node: BinaryNode) => { - logger.info({ node }, 'recv call') + ws.on('CB:call', async(node: BinaryNode) => { + logger.info({ node }, 'recv call') - const [child] = getAllBinaryNodeChildren(node) - if(!!child?.tag) { - await sendMessageAck(node, { class: 'call', type: child.tag }) - } - }) + const [child] = getAllBinaryNodeChildren(node) + if(!!child?.tag) { + await sendMessageAck(node, { class: 'call', type: child.tag }) + } + }) - const sendMessagesAgain = async(key: proto.IMessageKey, ids: string[]) => { - const msgs = await Promise.all( - ids.map(id => ( - config.getMessage({ ...key, id }) - )) - ) + const sendMessagesAgain = async(key: proto.IMessageKey, ids: string[]) => { + const msgs = await Promise.all( + ids.map(id => ( + config.getMessage({ ...key, id }) + )) + ) - const participant = key.participant || key.remoteJid - await assertSessions([participant], true) + const participant = key.participant || key.remoteJid + await assertSessions([participant], true) - if(isJidGroup(key.remoteJid)) { - await authState.keys.set({ 'sender-key-memory': { [key.remoteJid]: null } }) - } + if(isJidGroup(key.remoteJid)) { + await authState.keys.set({ 'sender-key-memory': { [key.remoteJid]: null } }) + } - logger.debug({ participant }, 'forced new session for retry recp') + logger.debug({ participant }, 'forced new session for retry recp') - for(let i = 0; i < msgs.length;i++) { - if(msgs[i]) { - await relayMessage(key.remoteJid, msgs[i], { - messageId: ids[i], - participant - }) - } else { - logger.debug({ jid: key.remoteJid, id: ids[i] }, 'recv retry request, but message not available') - } - } - } + for(let i = 0; i < msgs.length;i++) { + if(msgs[i]) { + await relayMessage(key.remoteJid, msgs[i], { + messageId: ids[i], + participant + }) + } else { + logger.debug({ jid: key.remoteJid, id: ids[i] }, 'recv retry request, but message not available') + } + } + } - const handleReceipt = async(node: BinaryNode) => { - let shouldAck = true + const handleReceipt = async(node: BinaryNode) => { + let shouldAck = true - const { attrs, content } = node - const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, authState.creds.me?.id) - const remoteJid = !isNodeFromMe ? attrs.from : attrs.recipient - const fromMe = !attrs.recipient + const { attrs, content } = node + const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, authState.creds.me?.id) + const remoteJid = !isNodeFromMe ? attrs.from : attrs.recipient + const fromMe = !attrs.recipient - const ids = [attrs.id] - if(Array.isArray(content)) { - const items = getBinaryNodeChildren(content[0], 'item') - ids.push(...items.map(i => i.attrs.id)) - } + const ids = [attrs.id] + if(Array.isArray(content)) { + const items = getBinaryNodeChildren(content[0], 'item') + ids.push(...items.map(i => i.attrs.id)) + } - const key: proto.IMessageKey = { - remoteJid, - id: '', - fromMe, - participant: attrs.participant - } + const key: proto.IMessageKey = { + remoteJid, + id: '', + fromMe, + participant: attrs.participant + } - const status = getStatusFromReceiptType(attrs.type) - if( - typeof status !== 'undefined' && + const status = getStatusFromReceiptType(attrs.type) + if( + typeof status !== 'undefined' && ( - // basically, we only want to know when a message from us has been delivered to/read by the other person - // or another device of ours has read some messages - status > proto.WebMessageInfo.WebMessageInfoStatus.DELIVERY_ACK || + // basically, we only want to know when a message from us has been delivered to/read by the other person + // or another device of ours has read some messages + status > proto.WebMessageInfo.WebMessageInfoStatus.DELIVERY_ACK || !isNodeFromMe ) - ) { - ev.emit('messages.update', ids.map(id => ({ - key: { ...key, id }, - update: { status } - }))) - } + ) { + ev.emit('messages.update', ids.map(id => ({ + key: { ...key, id }, + update: { status } + }))) + } - if(attrs.type === 'retry') { - // correctly set who is asking for the retry - key.participant = key.participant || attrs.from - if(key.fromMe) { - try { - logger.debug({ attrs }, 'recv retry request') - await sendMessagesAgain(key, ids) - } catch(error) { - logger.error({ key, ids, trace: error.stack }, 'error in sending message again') - shouldAck = false - } - } else { - logger.info({ attrs, key }, 'recv retry for not fromMe message') - } - } + if(attrs.type === 'retry') { + // correctly set who is asking for the retry + key.participant = key.participant || attrs.from + if(key.fromMe) { + try { + logger.debug({ attrs }, 'recv retry request') + await sendMessagesAgain(key, ids) + } catch(error) { + logger.error({ key, ids, trace: error.stack }, 'error in sending message again') + shouldAck = false + } + } else { + logger.info({ attrs, key }, 'recv retry for not fromMe message') + } + } - if(shouldAck) { - await sendMessageAck(node, { class: 'receipt', type: attrs.type }) - } + if(shouldAck) { + await sendMessageAck(node, { class: 'receipt', type: attrs.type }) + } - } + } - ws.on('CB:receipt', handleReceipt) + ws.on('CB:receipt', handleReceipt) - ws.on('CB:notification', async(node: BinaryNode) => { - await sendMessageAck(node, { class: 'notification', type: node.attrs.type }) + ws.on('CB:notification', async(node: BinaryNode) => { + await sendMessageAck(node, { class: 'notification', type: node.attrs.type }) - const msg = processNotification(node) - if(msg) { - const fromMe = areJidsSameUser(node.attrs.participant || node.attrs.from, authState.creds.me!.id) - msg.key = { - remoteJid: node.attrs.from, - fromMe, - participant: node.attrs.participant, - id: node.attrs.id, - ...(msg.key || {}) - } - msg.messageTimestamp = +node.attrs.t + const msg = processNotification(node) + if(msg) { + const fromMe = areJidsSameUser(node.attrs.participant || node.attrs.from, authState.creds.me!.id) + msg.key = { + remoteJid: node.attrs.from, + fromMe, + participant: node.attrs.participant, + id: node.attrs.id, + ...(msg.key || {}) + } + msg.messageTimestamp = +node.attrs.t - const fullMsg = proto.WebMessageInfo.fromObject(msg) - ev.emit('messages.upsert', { messages: [fullMsg], type: 'append' }) - } - }) + const fullMsg = proto.WebMessageInfo.fromObject(msg) + ev.emit('messages.upsert', { messages: [fullMsg], type: 'append' }) + } + }) - ev.on('messages.upsert', async({ messages, type }) => { - if(type === 'notify' || type === 'append') { - const chat: Partial = { id: messages[0].key.remoteJid } - const contactNameUpdates: { [_: string]: string } = { } - for(const msg of messages) { - if(!!msg.pushName) { - const jid = msg.key.fromMe ? jidNormalizedUser(authState.creds.me!.id) : (msg.key.participant || msg.key.remoteJid) - contactNameUpdates[jid] = msg.pushName - // update our pushname too - if(msg.key.fromMe && authState.creds.me?.name !== msg.pushName) { - ev.emit('creds.update', { me: { ...authState.creds.me!, name: msg.pushName! } }) - } - } + ev.on('messages.upsert', async({ messages, type }) => { + if(type === 'notify' || type === 'append') { + const chat: Partial = { id: messages[0].key.remoteJid } + const contactNameUpdates: { [_: string]: string } = { } + for(const msg of messages) { + if(!!msg.pushName) { + const jid = msg.key.fromMe ? jidNormalizedUser(authState.creds.me!.id) : (msg.key.participant || msg.key.remoteJid) + contactNameUpdates[jid] = msg.pushName + // update our pushname too + if(msg.key.fromMe && authState.creds.me?.name !== msg.pushName) { + ev.emit('creds.update', { me: { ...authState.creds.me!, name: msg.pushName! } }) + } + } - await processMessage(msg, chat) - if(!!msg.message && !msg.message!.protocolMessage) { - chat.conversationTimestamp = toNumber(msg.messageTimestamp) - if(!msg.key.fromMe) { - chat.unreadCount = (chat.unreadCount || 0) + 1 - } - } - } - if(Object.keys(chat).length > 1) { - ev.emit('chats.update', [ chat ]) - } - if(Object.keys(contactNameUpdates).length) { - ev.emit('contacts.update', Object.keys(contactNameUpdates).map( - id => ({ id, notify: contactNameUpdates[id] }) - )) - } - } - }) + await processMessage(msg, chat) + if(!!msg.message && !msg.message!.protocolMessage) { + chat.conversationTimestamp = toNumber(msg.messageTimestamp) + if(!msg.key.fromMe) { + chat.unreadCount = (chat.unreadCount || 0) + 1 + } + } + } + + if(Object.keys(chat).length > 1) { + ev.emit('chats.update', [ chat ]) + } + + if(Object.keys(contactNameUpdates).length) { + ev.emit('contacts.update', Object.keys(contactNameUpdates).map( + id => ({ id, notify: contactNameUpdates[id] }) + )) + } + } + }) return { ...sock, processMessage, sendMessageAck, sendRetryRequest } } diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 22eaf1f..e6b0cfd 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -1,439 +1,452 @@ -import { SocketConfig, MediaConnInfo, AnyMessageContent, MiscMessageGenerationOptions, WAMediaUploadFunction, MessageRelayOptions } from "../Types" -import { encodeWAMessage, generateMessageID, generateWAMessage, encryptSenderKeyMsgSignalProto, encryptSignalProto, extractDeviceJids, jidToSignalProtocolAddress, parseAndInjectE2ESessions, getWAUploadToServer } from "../Utils" -import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET, BinaryNodeAttributes, JidWithDevice, reduceBinaryNodeToDictionary } from '../WABinary' -import { proto } from "../../WAProto" -import { WA_DEFAULT_EPHEMERAL } from "../Defaults" -import { makeGroupsSocket } from "./groups" -import NodeCache from "node-cache" +import NodeCache from 'node-cache' +import { proto } from '../../WAProto' +import { WA_DEFAULT_EPHEMERAL } from '../Defaults' +import { AnyMessageContent, MediaConnInfo, MessageRelayOptions, MiscMessageGenerationOptions, SocketConfig } from '../Types' +import { encodeWAMessage, encryptSenderKeyMsgSignalProto, encryptSignalProto, extractDeviceJids, generateMessageID, generateWAMessage, getWAUploadToServer, jidToSignalProtocolAddress, parseAndInjectE2ESessions } from '../Utils' +import { BinaryNode, BinaryNodeAttributes, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidDecode, jidEncode, jidNormalizedUser, JidWithDevice, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary' +import { makeGroupsSocket } from './groups' export const makeMessagesSocket = (config: SocketConfig) => { const { logger } = config const sock = makeGroupsSocket(config) const { ev, - authState, - query, - generateMessageTag, + authState, + query, + generateMessageTag, sendNode, - groupMetadata, - groupToggleEphemeral + groupMetadata, + groupToggleEphemeral } = sock - const userDevicesCache = config.userDevicesCache || new NodeCache({ - stdTTL: 300, // 5 minutes - useClones: false - }) - let privacySettings: { [_: string]: string } | undefined + const userDevicesCache = config.userDevicesCache || new NodeCache({ + stdTTL: 300, // 5 minutes + useClones: false + }) + let privacySettings: { [_: string]: string } | undefined - const fetchPrivacySettings = async(force: boolean = false) => { - if(!privacySettings || force) { - const { content } = await query({ - tag: 'iq', - attrs: { - xmlns: 'privacy', - to: S_WHATSAPP_NET, - type: 'get' - }, - content: [ - { tag: 'privacy', attrs: { } } - ] - }) - privacySettings = reduceBinaryNodeToDictionary(content[0] as BinaryNode, 'category') - } - return privacySettings - } + const fetchPrivacySettings = async(force: boolean = false) => { + if(!privacySettings || force) { + const { content } = await query({ + tag: 'iq', + attrs: { + xmlns: 'privacy', + to: S_WHATSAPP_NET, + type: 'get' + }, + content: [ + { tag: 'privacy', attrs: { } } + ] + }) + privacySettings = reduceBinaryNodeToDictionary(content[0] as BinaryNode, 'category') + } - let mediaConn: Promise - const refreshMediaConn = async(forceGet = false) => { - let media = await mediaConn - if (!media || forceGet || (new Date().getTime()-media.fetchDate.getTime()) > media.ttl*1000) { + return privacySettings + } + + let mediaConn: Promise + const refreshMediaConn = async(forceGet = false) => { + const media = await mediaConn + if(!media || forceGet || (new Date().getTime()-media.fetchDate.getTime()) > media.ttl*1000) { mediaConn = (async() => { const result = await query({ - tag: 'iq', - attrs: { - type: 'set', - xmlns: 'w:m', - to: S_WHATSAPP_NET, - }, - content: [ { tag: 'media_conn', attrs: { } } ] - }) - const mediaConnNode = getBinaryNodeChild(result, 'media_conn') - const node: MediaConnInfo = { - hosts: getBinaryNodeChildren(mediaConnNode, 'host').map( - item => item.attrs as any - ), - auth: mediaConnNode.attrs.auth, - ttl: +mediaConnNode.attrs.ttl, - fetchDate: new Date() - } - logger.debug('fetched media conn') + tag: 'iq', + attrs: { + type: 'set', + xmlns: 'w:m', + to: S_WHATSAPP_NET, + }, + content: [ { tag: 'media_conn', attrs: { } } ] + }) + const mediaConnNode = getBinaryNodeChild(result, 'media_conn') + const node: MediaConnInfo = { + hosts: getBinaryNodeChildren(mediaConnNode, 'host').map( + item => item.attrs as any + ), + auth: mediaConnNode.attrs.auth, + ttl: +mediaConnNode.attrs.ttl, + fetchDate: new Date() + } + logger.debug('fetched media conn') return node })() - } - return mediaConn - } - /** + } + + return mediaConn + } + + /** * generic send receipt function * used for receipts of phone call, read, delivery etc. * */ - const sendReceipt = async(jid: string, participant: string | undefined, messageIds: string[], type: 'read' | 'read-self' | undefined) => { - const node: BinaryNode = { - tag: 'receipt', - attrs: { - id: messageIds[0], - t: Date.now().toString(), - to: jid, - }, - } - if(type) { - node.attrs.type = type - } - if(participant) { - node.attrs.participant = participant - } - const remainingMessageIds = messageIds.slice(1) - if(remainingMessageIds.length) { - node.content = [ - { - tag: 'list', - attrs: { }, - content: remainingMessageIds.map(id => ({ - tag: 'item', - attrs: { id } - })) - } - ] - } + const sendReceipt = async(jid: string, participant: string | undefined, messageIds: string[], type: 'read' | 'read-self' | undefined) => { + const node: BinaryNode = { + tag: 'receipt', + attrs: { + id: messageIds[0], + t: Date.now().toString(), + to: jid, + }, + } + if(type) { + node.attrs.type = type + } - logger.debug({ jid, messageIds, type }, 'sending receipt for messages') - await sendNode(node) - } + if(participant) { + node.attrs.participant = participant + } - const sendReadReceipt = async(jid: string, participant: string | undefined, messageIds: string[]) => { - const privacySettings = await fetchPrivacySettings() - // based on privacy settings, we have to change the read type - const readType = privacySettings.readreceipts === 'all' ? 'read' : 'read-self' - return sendReceipt(jid, participant, messageIds, readType) - } + const remainingMessageIds = messageIds.slice(1) + if(remainingMessageIds.length) { + node.content = [ + { + tag: 'list', + attrs: { }, + content: remainingMessageIds.map(id => ({ + tag: 'item', + attrs: { id } + })) + } + ] + } - const getUSyncDevices = async(jids: string[], ignoreZeroDevices: boolean) => { - const deviceResults: JidWithDevice[] = [] + logger.debug({ jid, messageIds, type }, 'sending receipt for messages') + await sendNode(node) + } + + const sendReadReceipt = async(jid: string, participant: string | undefined, messageIds: string[]) => { + const privacySettings = await fetchPrivacySettings() + // based on privacy settings, we have to change the read type + const readType = privacySettings.readreceipts === 'all' ? 'read' : 'read-self' + return sendReceipt(jid, participant, messageIds, readType) + } + + const getUSyncDevices = async(jids: string[], ignoreZeroDevices: boolean) => { + const deviceResults: JidWithDevice[] = [] - const users: BinaryNode[] = [] - jids = Array.from(new Set(jids)) - for(let jid of jids) { - const user = jidDecode(jid).user - jid = jidNormalizedUser(jid) - if(userDevicesCache.has(user)) { - const devices: JidWithDevice[] = userDevicesCache.get(user) - deviceResults.push(...devices) + const users: BinaryNode[] = [] + jids = Array.from(new Set(jids)) + for(let jid of jids) { + const user = jidDecode(jid).user + jid = jidNormalizedUser(jid) + if(userDevicesCache.has(user)) { + const devices: JidWithDevice[] = userDevicesCache.get(user) + deviceResults.push(...devices) - logger.trace({ user }, 'using cache for devices') - } else { - users.push({ tag: 'user', attrs: { jid } }) - } - } + logger.trace({ user }, 'using cache for devices') + } else { + users.push({ tag: 'user', attrs: { jid } }) + } + } - const iq: BinaryNode = { - tag: 'iq', - attrs: { - to: S_WHATSAPP_NET, - type: 'get', - xmlns: 'usync', - }, - content: [ - { - tag: 'usync', - attrs: { - sid: generateMessageTag(), - mode: 'query', - last: 'true', - index: '0', - context: 'message', - }, - content: [ - { - tag: 'query', - attrs: { }, - content: [ - { - tag: 'devices', - attrs: { version: '2' } - } - ] - }, - { tag: 'list', attrs: { }, content: users } - ] - }, - ], - } - const result = await query(iq) - const extracted = extractDeviceJids(result, authState.creds.me!.id, ignoreZeroDevices) - const deviceMap: { [_: string]: JidWithDevice[] } = {} + const iq: BinaryNode = { + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + type: 'get', + xmlns: 'usync', + }, + content: [ + { + tag: 'usync', + attrs: { + sid: generateMessageTag(), + mode: 'query', + last: 'true', + index: '0', + context: 'message', + }, + content: [ + { + tag: 'query', + attrs: { }, + content: [ + { + tag: 'devices', + attrs: { version: '2' } + } + ] + }, + { tag: 'list', attrs: { }, content: users } + ] + }, + ], + } + const result = await query(iq) + const extracted = extractDeviceJids(result, authState.creds.me!.id, ignoreZeroDevices) + const deviceMap: { [_: string]: JidWithDevice[] } = {} - for(const item of extracted) { - deviceMap[item.user] = deviceMap[item.user] || [] - deviceMap[item.user].push(item) + for(const item of extracted) { + deviceMap[item.user] = deviceMap[item.user] || [] + deviceMap[item.user].push(item) - deviceResults.push(item) - } + deviceResults.push(item) + } - for(const key in deviceMap) { - userDevicesCache.set(key, deviceMap[key]) - } + for(const key in deviceMap) { + userDevicesCache.set(key, deviceMap[key]) + } - return deviceResults - } + return deviceResults + } - const assertSessions = async(jids: string[], force: boolean) => { - let jidsRequiringFetch: string[] = [] - if(force) { - jidsRequiringFetch = jids - } else { - const addrs = jids.map(jid => jidToSignalProtocolAddress(jid).toString()) - const sessions = await authState.keys.get('session', addrs) - for(const jid of jids) { - const signalId = jidToSignalProtocolAddress(jid).toString() - if(!sessions[signalId]) { - jidsRequiringFetch.push(jid) - } - } - } + const assertSessions = async(jids: string[], force: boolean) => { + let jidsRequiringFetch: string[] = [] + if(force) { + jidsRequiringFetch = jids + } else { + const addrs = jids.map(jid => jidToSignalProtocolAddress(jid).toString()) + const sessions = await authState.keys.get('session', addrs) + for(const jid of jids) { + const signalId = jidToSignalProtocolAddress(jid).toString() + if(!sessions[signalId]) { + jidsRequiringFetch.push(jid) + } + } + } - if(jidsRequiringFetch.length) { - logger.debug({ jidsRequiringFetch }, `fetching sessions`) - const result = await query({ - tag: 'iq', - attrs: { - xmlns: 'encrypt', - type: 'get', - to: S_WHATSAPP_NET, - }, - content: [ - { - tag: 'key', - attrs: { }, - content: jidsRequiringFetch.map( - jid => ({ - tag: 'user', - attrs: { jid, reason: 'identity' }, - }) - ) - } - ] - }) - await parseAndInjectE2ESessions(result, authState) - return true - } - return false - } + if(jidsRequiringFetch.length) { + logger.debug({ jidsRequiringFetch }, 'fetching sessions') + const result = await query({ + tag: 'iq', + attrs: { + xmlns: 'encrypt', + type: 'get', + to: S_WHATSAPP_NET, + }, + content: [ + { + tag: 'key', + attrs: { }, + content: jidsRequiringFetch.map( + jid => ({ + tag: 'user', + attrs: { jid, reason: 'identity' }, + }) + ) + } + ] + }) + await parseAndInjectE2ESessions(result, authState) + return true + } - const createParticipantNodes = async(jids: string[], bytes: Buffer) => { - await assertSessions(jids, false) + return false + } - if(authState.keys.isInTransaction()) { - await authState.keys.prefetch( - 'session', - jids.map(jid => jidToSignalProtocolAddress(jid).toString()) - ) - } + const createParticipantNodes = async(jids: string[], bytes: Buffer) => { + await assertSessions(jids, false) + + if(authState.keys.isInTransaction()) { + await authState.keys.prefetch( + 'session', + jids.map(jid => jidToSignalProtocolAddress(jid).toString()) + ) + } - const nodes = await Promise.all( - jids.map( - async jid => { - const { type, ciphertext } = await encryptSignalProto(jid, bytes, authState) - const node: BinaryNode = { - tag: 'to', - attrs: { jid }, - content: [{ - tag: 'enc', - attrs: { v: '2', type }, - content: ciphertext - }] - } - return node - } - ) - ) - return nodes - } + const nodes = await Promise.all( + jids.map( + async jid => { + const { type, ciphertext } = await encryptSignalProto(jid, bytes, authState) + const node: BinaryNode = { + tag: 'to', + attrs: { jid }, + content: [{ + tag: 'enc', + attrs: { v: '2', type }, + content: ciphertext + }] + } + return node + } + ) + ) + return nodes + } - const relayMessage = async( - jid: string, - message: proto.IMessage, - { messageId: msgId, participant, additionalAttributes, cachedGroupMetadata }: MessageRelayOptions - ) => { - const meId = authState.creds.me!.id + const relayMessage = async( + jid: string, + message: proto.IMessage, + { messageId: msgId, participant, additionalAttributes, cachedGroupMetadata }: MessageRelayOptions + ) => { + const meId = authState.creds.me!.id - const { user, server } = jidDecode(jid) - const isGroup = server === 'g.us' - msgId = msgId || generateMessageID() + const { user, server } = jidDecode(jid) + const isGroup = server === 'g.us' + msgId = msgId || generateMessageID() - const encodedMsg = encodeWAMessage(message) - const participants: BinaryNode[] = [] + const encodedMsg = encodeWAMessage(message) + const participants: BinaryNode[] = [] - const destinationJid = jidEncode(user, isGroup ? 'g.us' : 's.whatsapp.net') + const destinationJid = jidEncode(user, isGroup ? 'g.us' : 's.whatsapp.net') - const binaryNodeContent: BinaryNode[] = [] + const binaryNodeContent: BinaryNode[] = [] - const devices: JidWithDevice[] = [] - if(participant) { - const { user, device } = jidDecode(participant) - devices.push({ user, device }) - } + const devices: JidWithDevice[] = [] + if(participant) { + const { user, device } = jidDecode(participant) + devices.push({ user, device }) + } - await authState.keys.transaction( - async() => { - if(isGroup) { - const { ciphertext, senderKeyDistributionMessageKey } = await encryptSenderKeyMsgSignalProto(destinationJid, encodedMsg, meId, authState) + await authState.keys.transaction( + async() => { + if(isGroup) { + const { ciphertext, senderKeyDistributionMessageKey } = await encryptSenderKeyMsgSignalProto(destinationJid, encodedMsg, meId, authState) - const [groupData, senderKeyMap] = await Promise.all([ - (async() => { - let groupData = cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined - if(!groupData) groupData = await groupMetadata(jid) - return groupData - })(), - (async() => { - const result = await authState.keys.get('sender-key-memory', [jid]) - return result[jid] || { } - })() - ]) + const [groupData, senderKeyMap] = await Promise.all([ + (async() => { + let groupData = cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined + if(!groupData) { + groupData = await groupMetadata(jid) + } + + return groupData + })(), + (async() => { + const result = await authState.keys.get('sender-key-memory', [jid]) + return result[jid] || { } + })() + ]) - if(!participant) { - const participantsList = groupData.participants.map(p => p.id) - const additionalDevices = await getUSyncDevices(participantsList, false) - devices.push(...additionalDevices) - } + if(!participant) { + const participantsList = groupData.participants.map(p => p.id) + const additionalDevices = await getUSyncDevices(participantsList, false) + devices.push(...additionalDevices) + } - const senderKeyJids: string[] = [] - // ensure a connection is established with every device - for(const {user, device} of devices) { - const jid = jidEncode(user, 's.whatsapp.net', device) - if(!senderKeyMap[jid]) { - senderKeyJids.push(jid) - // store that this person has had the sender keys sent to them - senderKeyMap[jid] = true - } - } - // if there are some participants with whom the session has not been established - // if there are, we re-send the senderkey - if(senderKeyJids.length) { - logger.debug({ senderKeyJids }, 'sending new sender key') + const senderKeyJids: string[] = [] + // ensure a connection is established with every device + for(const { user, device } of devices) { + const jid = jidEncode(user, 's.whatsapp.net', device) + if(!senderKeyMap[jid]) { + senderKeyJids.push(jid) + // store that this person has had the sender keys sent to them + senderKeyMap[jid] = true + } + } + + // if there are some participants with whom the session has not been established + // if there are, we re-send the senderkey + if(senderKeyJids.length) { + logger.debug({ senderKeyJids }, 'sending new sender key') - const encSenderKeyMsg = encodeWAMessage({ - senderKeyDistributionMessage: { - axolotlSenderKeyDistributionMessage: senderKeyDistributionMessageKey, - groupId: destinationJid - } - }) + const encSenderKeyMsg = encodeWAMessage({ + senderKeyDistributionMessage: { + axolotlSenderKeyDistributionMessage: senderKeyDistributionMessageKey, + groupId: destinationJid + } + }) - participants.push( - ...(await createParticipantNodes(senderKeyJids, encSenderKeyMsg)) - ) - } + participants.push( + ...(await createParticipantNodes(senderKeyJids, encSenderKeyMsg)) + ) + } - binaryNodeContent.push({ - tag: 'enc', - attrs: { v: '2', type: 'skmsg' }, - content: ciphertext - }) + binaryNodeContent.push({ + tag: 'enc', + attrs: { v: '2', type: 'skmsg' }, + content: ciphertext + }) - await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } }) - } else { - const { user: meUser } = jidDecode(meId) + await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } }) + } else { + const { user: meUser } = jidDecode(meId) - const encodedMeMsg = encodeWAMessage({ - deviceSentMessage: { - destinationJid, - message - } - }) + const encodedMeMsg = encodeWAMessage({ + deviceSentMessage: { + destinationJid, + message + } + }) - if(!participant) { - devices.push({ user }) - devices.push({ user: meUser }) + if(!participant) { + devices.push({ user }) + devices.push({ user: meUser }) - const additionalDevices = await getUSyncDevices([ meId, jid ], true) - devices.push(...additionalDevices) - } + const additionalDevices = await getUSyncDevices([ meId, jid ], true) + devices.push(...additionalDevices) + } - const meJids: string[] = [] - const otherJids: string[] = [] - for(const { user, device } of devices) { - const jid = jidEncode(user, 's.whatsapp.net', device) - const isMe = user === meUser - if(isMe) meJids.push(jid) - else otherJids.push(jid) - } + const meJids: string[] = [] + const otherJids: string[] = [] + for(const { user, device } of devices) { + const jid = jidEncode(user, 's.whatsapp.net', device) + const isMe = user === meUser + if(isMe) { + meJids.push(jid) + } else { + otherJids.push(jid) + } + } - const [meNodes, otherNodes] = await Promise.all([ - createParticipantNodes(meJids, encodedMeMsg), - createParticipantNodes(otherJids, encodedMsg) - ]) - participants.push(...meNodes) - participants.push(...otherNodes) - } + const [meNodes, otherNodes] = await Promise.all([ + createParticipantNodes(meJids, encodedMeMsg), + createParticipantNodes(otherJids, encodedMsg) + ]) + participants.push(...meNodes) + participants.push(...otherNodes) + } - if(participants.length) { - binaryNodeContent.push({ - tag: 'participants', - attrs: { }, - content: participants - }) - } + if(participants.length) { + binaryNodeContent.push({ + tag: 'participants', + attrs: { }, + content: participants + }) + } - const stanza: BinaryNode = { - tag: 'message', - attrs: { - id: msgId, - type: 'text', - to: destinationJid, - ...(additionalAttributes || {}) - }, - content: binaryNodeContent - } + const stanza: BinaryNode = { + tag: 'message', + attrs: { + id: msgId, + type: 'text', + to: destinationJid, + ...(additionalAttributes || {}) + }, + content: binaryNodeContent + } - const shouldHaveIdentity = !!participants.find( - participant => (participant.content! as BinaryNode[]).find(n => n.attrs.type === 'pkmsg') - ) + const shouldHaveIdentity = !!participants.find( + participant => (participant.content! as BinaryNode[]).find(n => n.attrs.type === 'pkmsg') + ) - if(shouldHaveIdentity) { - (stanza.content as BinaryNode[]).push({ - tag: 'device-identity', - attrs: { }, - content: proto.ADVSignedDeviceIdentity.encode(authState.creds.account).finish() - }) + if(shouldHaveIdentity) { + (stanza.content as BinaryNode[]).push({ + tag: 'device-identity', + attrs: { }, + content: proto.ADVSignedDeviceIdentity.encode(authState.creds.account).finish() + }) - logger.debug({ jid }, 'adding device identity') - } + logger.debug({ jid }, 'adding device identity') + } - logger.debug({ msgId }, `sending message to ${participants.length} devices`) + logger.debug({ msgId }, `sending message to ${participants.length} devices`) - await sendNode(stanza) - } - ) + await sendNode(stanza) + } + ) - return msgId - } + return msgId + } - const waUploadToServer = getWAUploadToServer(config, refreshMediaConn) + const waUploadToServer = getWAUploadToServer(config, refreshMediaConn) return { ...sock, - assertSessions, - relayMessage, - sendReceipt, - sendReadReceipt, - refreshMediaConn, + assertSessions, + relayMessage, + sendReceipt, + sendReadReceipt, + refreshMediaConn, waUploadToServer, - fetchPrivacySettings, - sendMessage: async( + fetchPrivacySettings, + sendMessage: async( jid: string, content: AnyMessageContent, options: MiscMessageGenerationOptions = { } ) => { - const userJid = authState.creds.me!.id + const userJid = authState.creds.me!.id if( typeof content === 'object' && 'disappearingMessagesInChat' in content && @@ -442,9 +455,9 @@ export const makeMessagesSocket = (config: SocketConfig) => { ) { const { disappearingMessagesInChat } = content const value = typeof disappearingMessagesInChat === 'boolean' ? - (disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : - disappearingMessagesInChat - await groupToggleEphemeral(jid, value) + (disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : + disappearingMessagesInChat + await groupToggleEphemeral(jid, value) } else { const fullMsg = await generateWAMessage( jid, @@ -452,28 +465,29 @@ export const makeMessagesSocket = (config: SocketConfig) => { { logger, userJid, - // multi-device does not have this yet + // multi-device does not have this yet //getUrlInfo: generateUrlInfo, upload: waUploadToServer, - mediaCache: config.mediaCache, + mediaCache: config.mediaCache, ...options, } ) - const isDeleteMsg = 'delete' in content && !!content.delete - const additionalAttributes: BinaryNodeAttributes = { } - // required for delete - if(isDeleteMsg) { - additionalAttributes.edit = '7' - } + const isDeleteMsg = 'delete' in content && !!content.delete + const additionalAttributes: BinaryNodeAttributes = { } + // required for delete + if(isDeleteMsg) { + additionalAttributes.edit = '7' + } await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id!, additionalAttributes }) - if(config.emitOwnEvents) { - process.nextTick(() => { - ev.emit('messages.upsert', { messages: [fullMsg], type: 'append' }) - }) - } + if(config.emitOwnEvents) { + process.nextTick(() => { + ev.emit('messages.upsert', { messages: [fullMsg], type: 'append' }) + }) + } + return fullMsg } - } + } } } diff --git a/src/Socket/socket.ts b/src/Socket/socket.ts index 14a6e55..d31df02 100644 --- a/src/Socket/socket.ts +++ b/src/Socket/socket.ts @@ -1,13 +1,13 @@ import { Boom } from '@hapi/boom' -import EventEmitter from 'events' -import { promisify } from "util" -import WebSocket from "ws" import { randomBytes } from 'crypto' +import EventEmitter from 'events' +import { promisify } from 'util' +import WebSocket from 'ws' import { proto } from '../../WAProto' -import { DisconnectReason, SocketConfig, BaileysEventEmitter, AuthenticationCreds } from "../Types" -import { Curve, generateRegistrationNode, configureSuccessfulPairing, generateLoginNode, encodeBigEndian, promiseTimeout, generateOrGetPreKeys, xmppSignedPreKey, xmppPreKey, getPreKeys, makeNoiseHandler, useSingleFileAuthState, addTransactionCapability, bindWaitForConnectionUpdate, printQRIfNecessaryListener } from "../Utils" -import { DEFAULT_ORIGIN, DEF_TAG_PREFIX, DEF_CALLBACK_PREFIX, KEY_BUNDLE_TYPE } from "../Defaults" -import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, S_WHATSAPP_NET, getBinaryNodeChild } from '../WABinary' +import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, DEFAULT_ORIGIN, KEY_BUNDLE_TYPE } from '../Defaults' +import { AuthenticationCreds, BaileysEventEmitter, DisconnectReason, SocketConfig } from '../Types' +import { addTransactionCapability, bindWaitForConnectionUpdate, configureSuccessfulPairing, Curve, encodeBigEndian, generateLoginNode, generateOrGetPreKeys, generateRegistrationNode, getPreKeys, makeNoiseHandler, printQRIfNecessaryListener, promiseTimeout, useSingleFileAuthState, xmppPreKey, xmppSignedPreKey } from '../Utils' +import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, getBinaryNodeChild, S_WHATSAPP_NET } from '../WABinary' /** * Connects to WA servers and performs: @@ -16,16 +16,16 @@ import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, S_WHATSAPP_NET, getB * - query phone connection */ export const makeSocket = ({ - waWebSocketUrl, - connectTimeoutMs, - logger, - agent, - keepAliveIntervalMs, - version, - browser, - auth: initialAuthState, - printQRInTerminal, - defaultQueryTimeoutMs + waWebSocketUrl, + connectTimeoutMs, + logger, + agent, + keepAliveIntervalMs, + version, + browser, + auth: initialAuthState, + printQRInTerminal, + defaultQueryTimeoutMs }: SocketConfig) => { const ws = new WebSocket(waWebSocketUrl, undefined, { origin: DEFAULT_ORIGIN, @@ -40,500 +40,524 @@ export const makeSocket = ({ 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits' } }) - ws.setMaxListeners(0) - const ev = new EventEmitter() as BaileysEventEmitter - /** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */ - const ephemeralKeyPair = Curve.generateKeyPair() - /** WA noise protocol wrapper */ - const noise = makeNoiseHandler(ephemeralKeyPair) - let authState = initialAuthState - if(!authState) { - authState = useSingleFileAuthState('./auth-info-multi.json').state + ws.setMaxListeners(0) + const ev = new EventEmitter() as BaileysEventEmitter + /** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */ + const ephemeralKeyPair = Curve.generateKeyPair() + /** WA noise protocol wrapper */ + const noise = makeNoiseHandler(ephemeralKeyPair) + let authState = initialAuthState + if(!authState) { + authState = useSingleFileAuthState('./auth-info-multi.json').state - logger.warn(` + logger.warn(` Baileys just created a single file state for your credentials. This will not be supported soon. Please pass the credentials in the config itself `) - } - const { creds } = authState + } + + const { creds } = authState - let lastDateRecv: Date + let lastDateRecv: Date let epoch = 0 let keepAliveReq: NodeJS.Timeout - let qrTimer: NodeJS.Timeout + let qrTimer: NodeJS.Timeout - const uqTagId = `${randomBytes(1).toString('hex')[0]}.${randomBytes(1).toString('hex')[0]}-` - const generateMessageTag = () => `${uqTagId}${epoch++}` + const uqTagId = `${randomBytes(1).toString('hex')[0]}.${randomBytes(1).toString('hex')[0]}-` + const generateMessageTag = () => `${uqTagId}${epoch++}` const sendPromise = promisify(ws.send) /** send a raw buffer */ const sendRawMessage = async(data: Buffer | Uint8Array) => { - if(ws.readyState !== ws.OPEN) { - throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) - } - const bytes = noise.encodeFrame(data) + if(ws.readyState !== ws.OPEN) { + throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) + } + + const bytes = noise.encodeFrame(data) await sendPromise.call(ws, bytes) as Promise - } - /** send a binary node */ - const sendNode = (node: BinaryNode) => { - let buff = encodeBinaryNode(node) - return sendRawMessage(buff) - } - /** await the next incoming message */ - const awaitNextMessage = async(sendMsg?: Uint8Array) => { - if(ws.readyState !== ws.OPEN) { - throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) - } - let onOpen: (data: any) => void - let onClose: (err: Error) => void + } - const result = new Promise((resolve, reject) => { - onOpen = (data: any) => resolve(data) - onClose = reject - ws.on('frame', onOpen) - ws.on('close', onClose) - ws.on('error', onClose) - }) - .finally(() => { - ws.off('frame', onOpen) - ws.off('close', onClose) - ws.off('error', onClose) - }) + /** send a binary node */ + const sendNode = (node: BinaryNode) => { + const buff = encodeBinaryNode(node) + return sendRawMessage(buff) + } - if(sendMsg) { - sendRawMessage(sendMsg).catch(onClose) - } + /** await the next incoming message */ + const awaitNextMessage = async(sendMsg?: Uint8Array) => { + if(ws.readyState !== ws.OPEN) { + throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) + } - return result - } + let onOpen: (data: any) => void + let onClose: (err: Error) => void - /** + const result = new Promise((resolve, reject) => { + onOpen = (data: any) => resolve(data) + onClose = reject + ws.on('frame', onOpen) + ws.on('close', onClose) + ws.on('error', onClose) + }) + .finally(() => { + ws.off('frame', onOpen) + ws.off('close', onClose) + ws.off('error', onClose) + }) + + if(sendMsg) { + sendRawMessage(sendMsg).catch(onClose) + } + + return result + } + + /** * Wait for a message with a certain tag to be received * @param tag the message tag to await * @param json query that was sent * @param timeoutMs timeout after which the promise will reject */ const waitForMessage = async(msgId: string, timeoutMs = defaultQueryTimeoutMs) => { - let onRecv: (json) => void - let onErr: (err) => void - try { - const result = await promiseTimeout(timeoutMs, - (resolve, reject) => { - onRecv = resolve - onErr = err => { - reject(err || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })) - } + let onRecv: (json) => void + let onErr: (err) => void + try { + const result = await promiseTimeout(timeoutMs, + (resolve, reject) => { + onRecv = resolve + onErr = err => { + reject(err || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })) + } - ws.on(`TAG:${msgId}`, onRecv) - ws.on('close', onErr) // if the socket closes, you'll never receive the message - ws.off('error', onErr) - }, - ) - return result as any - } finally { - ws.off(`TAG:${msgId}`, onRecv) - ws.off('close', onErr) // if the socket closes, you'll never receive the message - ws.off('error', onErr) - } - } - /** send a query, and wait for its response. auto-generates message ID if not provided */ - const query = async(node: BinaryNode, timeoutMs?: number) => { - if(!node.attrs.id) node.attrs.id = generateMessageTag() - - const msgId = node.attrs.id - const wait = waitForMessage(msgId, timeoutMs) - - await sendNode(node) - - const result = await (wait as Promise) - if('tag' in result) { - assertNodeErrorFree(result) - } - return result - } - /** connection handshake */ - const validateConnection = async () => { - logger.info('connected to WA Web') - - const init = proto.HandshakeMessage.encode({ - clientHello: { ephemeral: ephemeralKeyPair.public } - }).finish() - - const result = await awaitNextMessage(init) - const handshake = proto.HandshakeMessage.decode(result) - - logger.debug('handshake recv from WA Web') - - const keyEnc = noise.processHandshake(handshake, creds.noiseKey) - logger.info('handshake complete') - - let node: Uint8Array - if(!creds.me) { - logger.info('not logged in, attempting registration...') - node = generateRegistrationNode(creds, { version, browser }) - } else { - logger.info('logging in...') - node = generateLoginNode(creds.me!.id, { version, browser }) - } - const payloadEnc = noise.encrypt(node) - await sendRawMessage( - proto.HandshakeMessage.encode({ - clientFinish: { - static: new Uint8Array(keyEnc), - payload: new Uint8Array(payloadEnc), - }, - }).finish() - ) - noise.finishInit() - startKeepAliveRequest() - } - /** get some pre-keys and do something with them */ - const assertingPreKeys = async(range: number, execute: (keys: { [_: number]: any }) => Promise) => { - const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(authState.creds, range) - - const update: Partial = { - nextPreKeyId: Math.max(lastPreKeyId+1, creds.nextPreKeyId), - firstUnuploadedPreKeyId: Math.max(creds.firstUnuploadedPreKeyId, lastPreKeyId+1) - } - if(!creds.serverHasPreKeys) { - update.serverHasPreKeys = true - } - - await authState.keys.set({ 'pre-key': newPreKeys }) - - const preKeys = await getPreKeys(authState.keys, preKeysRange[0], preKeysRange[0] + preKeysRange[1]) - await execute(preKeys) - - ev.emit('creds.update', update) - } - /** generates and uploads a set of pre-keys */ - const uploadPreKeys = async() => { - await assertingPreKeys(30, async preKeys => { - const node: BinaryNode = { - tag: 'iq', - attrs: { - id: generateMessageTag(), - xmlns: 'encrypt', - type: 'set', - to: S_WHATSAPP_NET, - }, - content: [ - { tag: 'registration', attrs: { }, content: encodeBigEndian(creds.registrationId) }, - { tag: 'type', attrs: { }, content: KEY_BUNDLE_TYPE }, - { tag: 'identity', attrs: { }, content: creds.signedIdentityKey.public }, - { tag: 'list', attrs: { }, content: Object.keys(preKeys).map(k => xmppPreKey(preKeys[+k], +k)) }, - xmppSignedPreKey(creds.signedPreKey) - ] - } - await sendNode(node) - - logger.info('uploaded pre-keys') - }) - } - - const onMessageRecieved = (data: Buffer) => { - noise.decodeFrame(data, frame => { - ws.emit('frame', frame) - // if it's a binary node - if(!(frame instanceof Uint8Array)) { - const msgId = frame.attrs.id - - if(logger.level === 'trace') { - logger.trace({ msgId, fromMe: false, frame }, 'communication') - } - - let anyTriggered = false - /* Check if this is a response to a message we sent */ - anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) - /* Check if this is a response to a message we are expecting */ - const l0 = frame.tag - const l1 = frame.attrs || { } - const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : '' - - Object.keys(l1).forEach(key => { - anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered - anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered - anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered - }) - anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered - anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered - anyTriggered = ws.emit('frame', frame) || anyTriggered - - if (!anyTriggered && logger.level === 'debug') { - logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv') - } - } - }) - } - - const end = (error: Error | undefined) => { - logger.info({ error }, 'connection closed') - - clearInterval(keepAliveReq) - clearInterval(qrTimer) - - ws.removeAllListeners('close') - ws.removeAllListeners('error') - ws.removeAllListeners('open') - ws.removeAllListeners('message') - - if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) { - try { ws.close() } catch { } - } - - ev.emit('connection.update', { - connection: 'close', - lastDisconnect: { - error, - date: new Date() - } - }) - ev.removeAllListeners('connection.update') + ws.on(`TAG:${msgId}`, onRecv) + ws.on('close', onErr) // if the socket closes, you'll never receive the message + ws.off('error', onErr) + }, + ) + return result as any + } finally { + ws.off(`TAG:${msgId}`, onRecv) + ws.off('close', onErr) // if the socket closes, you'll never receive the message + ws.off('error', onErr) + } } - const waitForSocketOpen = async() => { - if(ws.readyState === ws.OPEN) return - if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) { - throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) - } - let onOpen: () => void - let onClose: (err: Error) => void - await new Promise((resolve, reject) => { - onOpen = () => resolve(undefined) - onClose = reject - ws.on('open', onOpen) - ws.on('close', onClose) - ws.on('error', onClose) - }) - .finally(() => { - ws.off('open', onOpen) - ws.off('close', onClose) - ws.off('error', onClose) - }) - } + /** send a query, and wait for its response. auto-generates message ID if not provided */ + const query = async(node: BinaryNode, timeoutMs?: number) => { + if(!node.attrs.id) { + node.attrs.id = generateMessageTag() + } - const startKeepAliveRequest = () => ( - keepAliveReq = setInterval(() => { - if (!lastDateRecv) lastDateRecv = new Date() - const diff = Date.now() - lastDateRecv.getTime() - /* + const msgId = node.attrs.id + const wait = waitForMessage(msgId, timeoutMs) + + await sendNode(node) + + const result = await (wait as Promise) + if('tag' in result) { + assertNodeErrorFree(result) + } + + return result + } + + /** connection handshake */ + const validateConnection = async() => { + logger.info('connected to WA Web') + + const init = proto.HandshakeMessage.encode({ + clientHello: { ephemeral: ephemeralKeyPair.public } + }).finish() + + const result = await awaitNextMessage(init) + const handshake = proto.HandshakeMessage.decode(result) + + logger.debug('handshake recv from WA Web') + + const keyEnc = noise.processHandshake(handshake, creds.noiseKey) + logger.info('handshake complete') + + let node: Uint8Array + if(!creds.me) { + logger.info('not logged in, attempting registration...') + node = generateRegistrationNode(creds, { version, browser }) + } else { + logger.info('logging in...') + node = generateLoginNode(creds.me!.id, { version, browser }) + } + + const payloadEnc = noise.encrypt(node) + await sendRawMessage( + proto.HandshakeMessage.encode({ + clientFinish: { + static: new Uint8Array(keyEnc), + payload: new Uint8Array(payloadEnc), + }, + }).finish() + ) + noise.finishInit() + startKeepAliveRequest() + } + + /** get some pre-keys and do something with them */ + const assertingPreKeys = async(range: number, execute: (keys: { [_: number]: any }) => Promise) => { + const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(authState.creds, range) + + const update: Partial = { + nextPreKeyId: Math.max(lastPreKeyId+1, creds.nextPreKeyId), + firstUnuploadedPreKeyId: Math.max(creds.firstUnuploadedPreKeyId, lastPreKeyId+1) + } + if(!creds.serverHasPreKeys) { + update.serverHasPreKeys = true + } + + await authState.keys.set({ 'pre-key': newPreKeys }) + + const preKeys = await getPreKeys(authState.keys, preKeysRange[0], preKeysRange[0] + preKeysRange[1]) + await execute(preKeys) + + ev.emit('creds.update', update) + } + + /** generates and uploads a set of pre-keys */ + const uploadPreKeys = async() => { + await assertingPreKeys(30, async preKeys => { + const node: BinaryNode = { + tag: 'iq', + attrs: { + id: generateMessageTag(), + xmlns: 'encrypt', + type: 'set', + to: S_WHATSAPP_NET, + }, + content: [ + { tag: 'registration', attrs: { }, content: encodeBigEndian(creds.registrationId) }, + { tag: 'type', attrs: { }, content: KEY_BUNDLE_TYPE }, + { tag: 'identity', attrs: { }, content: creds.signedIdentityKey.public }, + { tag: 'list', attrs: { }, content: Object.keys(preKeys).map(k => xmppPreKey(preKeys[+k], +k)) }, + xmppSignedPreKey(creds.signedPreKey) + ] + } + await sendNode(node) + + logger.info('uploaded pre-keys') + }) + } + + const onMessageRecieved = (data: Buffer) => { + noise.decodeFrame(data, frame => { + ws.emit('frame', frame) + // if it's a binary node + if(!(frame instanceof Uint8Array)) { + const msgId = frame.attrs.id + + if(logger.level === 'trace') { + logger.trace({ msgId, fromMe: false, frame }, 'communication') + } + + let anyTriggered = false + /* Check if this is a response to a message we sent */ + anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) + /* Check if this is a response to a message we are expecting */ + const l0 = frame.tag + const l1 = frame.attrs || { } + const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : '' + + Object.keys(l1).forEach(key => { + anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered + anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered + anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered + }) + anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered + anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered + anyTriggered = ws.emit('frame', frame) || anyTriggered + + if(!anyTriggered && logger.level === 'debug') { + logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv') + } + } + }) + } + + const end = (error: Error | undefined) => { + logger.info({ error }, 'connection closed') + + clearInterval(keepAliveReq) + clearInterval(qrTimer) + + ws.removeAllListeners('close') + ws.removeAllListeners('error') + ws.removeAllListeners('open') + ws.removeAllListeners('message') + + if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) { + try { + ws.close() + } catch{ } + } + + ev.emit('connection.update', { + connection: 'close', + lastDisconnect: { + error, + date: new Date() + } + }) + ev.removeAllListeners('connection.update') + } + + const waitForSocketOpen = async() => { + if(ws.readyState === ws.OPEN) { + return + } + + if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) { + throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }) + } + + let onOpen: () => void + let onClose: (err: Error) => void + await new Promise((resolve, reject) => { + onOpen = () => resolve(undefined) + onClose = reject + ws.on('open', onOpen) + ws.on('close', onClose) + ws.on('error', onClose) + }) + .finally(() => { + ws.off('open', onOpen) + ws.off('close', onClose) + ws.off('error', onClose) + }) + } + + const startKeepAliveRequest = () => ( + keepAliveReq = setInterval(() => { + if(!lastDateRecv) { + lastDateRecv = new Date() + } + + const diff = Date.now() - lastDateRecv.getTime() + /* check if it's been a suspicious amount of time since the server responded with our last seen it could be that the network is down */ - if (diff > keepAliveIntervalMs+5000) { - end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost })) - } else if(ws.readyState === ws.OPEN) { - // if its all good, send a keep alive request - query( - { - tag: 'iq', - attrs: { - id: generateMessageTag(), - to: S_WHATSAPP_NET, - type: 'get', - xmlns: 'w:p', - }, - content: [{ tag: 'ping', attrs: { } }] - }, - keepAliveIntervalMs - ) - .then(() => { - lastDateRecv = new Date() - logger.trace('recv keep alive') - }) - .catch(err => end(err)) - } else { - logger.warn('keep alive called when WS not open') - } - }, keepAliveIntervalMs) - ) - /** i have no idea why this exists. pls enlighten me */ - const sendPassiveIq = (tag: 'passive' | 'active') => ( - sendNode({ - tag: 'iq', - attrs: { - to: S_WHATSAPP_NET, - xmlns: 'passive', - type: 'set', - id: generateMessageTag(), - }, - content: [ - { tag, attrs: { } } - ] - }) - ) - /** logout & invalidate connection */ - const logout = async() => { - const jid = authState.creds.me?.id - if(jid) { - await sendNode({ - tag: 'iq', - attrs: { - to: S_WHATSAPP_NET, - type: 'set', - id: generateMessageTag(), - xmlns: 'md' - }, - content: [ - { - tag: 'remove-companion-device', - attrs: { - jid: jid, - reason: 'user_initiated' - } - } - ] - }) - } + if(diff > keepAliveIntervalMs+5000) { + end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost })) + } else if(ws.readyState === ws.OPEN) { + // if its all good, send a keep alive request + query( + { + tag: 'iq', + attrs: { + id: generateMessageTag(), + to: S_WHATSAPP_NET, + type: 'get', + xmlns: 'w:p', + }, + content: [{ tag: 'ping', attrs: { } }] + }, + keepAliveIntervalMs + ) + .then(() => { + lastDateRecv = new Date() + logger.trace('recv keep alive') + }) + .catch(err => end(err)) + } else { + logger.warn('keep alive called when WS not open') + } + }, keepAliveIntervalMs) + ) + /** i have no idea why this exists. pls enlighten me */ + const sendPassiveIq = (tag: 'passive' | 'active') => ( + sendNode({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + xmlns: 'passive', + type: 'set', + id: generateMessageTag(), + }, + content: [ + { tag, attrs: { } } + ] + }) + ) + /** logout & invalidate connection */ + const logout = async() => { + const jid = authState.creds.me?.id + if(jid) { + await sendNode({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + type: 'set', + id: generateMessageTag(), + xmlns: 'md' + }, + content: [ + { + tag: 'remove-companion-device', + attrs: { + jid: jid, + reason: 'user_initiated' + } + } + ] + }) + } - end(new Boom('Intentional Logout', { statusCode: DisconnectReason.loggedOut })) - } + end(new Boom('Intentional Logout', { statusCode: DisconnectReason.loggedOut })) + } ws.on('message', onMessageRecieved) ws.on('open', validateConnection) ws.on('error', end) ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed }))) - // the server terminated the connection - ws.on('CB:xmlstreamend', () => { - end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })) - }) - // QR gen - ws.on('CB:iq,type:set,pair-device', async (stanza: BinaryNode) => { - const iq: BinaryNode = { - tag: 'iq', - attrs: { - to: S_WHATSAPP_NET, - type: 'result', - id: stanza.attrs.id, - } - } - await sendNode(iq) + // the server terminated the connection + ws.on('CB:xmlstreamend', () => { + end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })) + }) + // QR gen + ws.on('CB:iq,type:set,pair-device', async(stanza: BinaryNode) => { + const iq: BinaryNode = { + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + type: 'result', + id: stanza.attrs.id, + } + } + await sendNode(iq) - const refs = ((stanza.content[0] as BinaryNode).content as BinaryNode[]).map(n => n.content as string) - const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64') - const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64') - const advB64 = creds.advSecretKey + const refs = ((stanza.content[0] as BinaryNode).content as BinaryNode[]).map(n => n.content as string) + const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64') + const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64') + const advB64 = creds.advSecretKey - let qrMs = 60_000 // time to let a QR live - const genPairQR = () => { - const ref = refs.shift() - if(!ref) { - end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut })) - return - } + let qrMs = 60_000 // time to let a QR live + const genPairQR = () => { + const ref = refs.shift() + if(!ref) { + end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut })) + return + } - const qr = [ref, noiseKeyB64, identityKeyB64, advB64].join(',') + const qr = [ref, noiseKeyB64, identityKeyB64, advB64].join(',') - ev.emit('connection.update', { qr }) + ev.emit('connection.update', { qr }) - qrTimer = setTimeout(genPairQR, qrMs) - qrMs = 20_000 // shorter subsequent qrs - } + qrTimer = setTimeout(genPairQR, qrMs) + qrMs = 20_000 // shorter subsequent qrs + } - genPairQR() - }) - // device paired for the first time - // if device pairs successfully, the server asks to restart the connection - ws.on('CB:iq,,pair-success', async(stanza: BinaryNode) => { - logger.debug('pair success recv') - try { - const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds) + genPairQR() + }) + // device paired for the first time + // if device pairs successfully, the server asks to restart the connection + ws.on('CB:iq,,pair-success', async(stanza: BinaryNode) => { + logger.debug('pair success recv') + try { + const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds) - logger.debug('pairing configured successfully') + logger.debug('pairing configured successfully') - const waiting = awaitNextMessage() - await sendNode(reply) + const waiting = awaitNextMessage() + await sendNode(reply) - const value = (await waiting) as BinaryNode - if(value.tag === 'stream:error') { - if(value.attrs?.code !== '515') { - throw new Boom('Authentication failed', { statusCode: +(value.attrs.code || 500) }) - } - } + const value = (await waiting) as BinaryNode + if(value.tag === 'stream:error') { + if(value.attrs?.code !== '515') { + throw new Boom('Authentication failed', { statusCode: +(value.attrs.code || 500) }) + } + } - logger.info({ jid: updatedCreds.me!.id }, 'registered connection, restart server') + logger.info({ jid: updatedCreds.me!.id }, 'registered connection, restart server') - ev.emit('creds.update', updatedCreds) - ev.emit('connection.update', { isNewLogin: true, qr: undefined }) + ev.emit('creds.update', updatedCreds) + ev.emit('connection.update', { isNewLogin: true, qr: undefined }) - end(new Boom('Restart Required', { statusCode: DisconnectReason.restartRequired })) - } catch(error) { - logger.info({ trace: error.stack }, 'error in pairing') - end(error) - } - }) - // login complete - ws.on('CB:success', async() => { - if(!creds.serverHasPreKeys) { - await uploadPreKeys() - } - await sendPassiveIq('active') + end(new Boom('Restart Required', { statusCode: DisconnectReason.restartRequired })) + } catch(error) { + logger.info({ trace: error.stack }, 'error in pairing') + end(error) + } + }) + // login complete + ws.on('CB:success', async() => { + if(!creds.serverHasPreKeys) { + await uploadPreKeys() + } - logger.info('opened connection to WA') - clearTimeout(qrTimer) // will never happen in all likelyhood -- but just in case WA sends success on first try + await sendPassiveIq('active') - ev.emit('connection.update', { connection: 'open' }) - }) + logger.info('opened connection to WA') + clearTimeout(qrTimer) // will never happen in all likelyhood -- but just in case WA sends success on first try + + ev.emit('connection.update', { connection: 'open' }) + }) - ws.on('CB:ib,,offline', (node: BinaryNode) => { - const child = getBinaryNodeChild(node, 'offline') - const offlineCount = +child.attrs.count + ws.on('CB:ib,,offline', (node: BinaryNode) => { + const child = getBinaryNodeChild(node, 'offline') + const offlineCount = +child.attrs.count - logger.info(`got ${offlineCount} offline messages/notifications`) + logger.info(`got ${offlineCount} offline messages/notifications`) - ev.emit('connection.update', { receivedPendingNotifications: true }) - }) + ev.emit('connection.update', { receivedPendingNotifications: true }) + }) - ws.on('CB:stream:error', (node: BinaryNode) => { - logger.error({ error: node }, `stream errored out`) + ws.on('CB:stream:error', (node: BinaryNode) => { + logger.error({ error: node }, 'stream errored out') - const statusCode = +(node.attrs.code || DisconnectReason.restartRequired) - end(new Boom('Stream Errored', { statusCode, data: node })) - }) - // stream fail, possible logout - ws.on('CB:failure', (node: BinaryNode) => { - const reason = +(node.attrs.reason || 500) - end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs })) - }) + const statusCode = +(node.attrs.code || DisconnectReason.restartRequired) + end(new Boom('Stream Errored', { statusCode, data: node })) + }) + // stream fail, possible logout + ws.on('CB:failure', (node: BinaryNode) => { + const reason = +(node.attrs.reason || 500) + end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs })) + }) - ws.on('CB:ib,,downgrade_webclient', () => { - end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch })) - }) + ws.on('CB:ib,,downgrade_webclient', () => { + end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch })) + }) - process.nextTick(() => { - ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined }) - }) - // update credentials when required - ev.on('creds.update', update => Object.assign(creds, update)) + process.nextTick(() => { + ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined }) + }) + // update credentials when required + ev.on('creds.update', update => Object.assign(creds, update)) - if(printQRInTerminal) { + if(printQRInTerminal) { printQRIfNecessaryListener(ev, logger) } return { - type: 'md' as 'md', - ws, - ev, - authState: { - creds, - // add capability - keys: addTransactionCapability(authState.keys, logger) - }, - get user () { - return authState.creds.me - }, - assertingPreKeys, - generateMessageTag, - query, - waitForMessage, - waitForSocketOpen, + type: 'md' as 'md', + ws, + ev, + authState: { + creds, + // add capability + keys: addTransactionCapability(authState.keys, logger) + }, + get user() { + return authState.creds.me + }, + assertingPreKeys, + generateMessageTag, + query, + waitForMessage, + waitForSocketOpen, sendRawMessage, - sendNode, - logout, - end, - /** Waits for the connection to WA to reach a state */ - waitForConnectionUpdate: bindWaitForConnectionUpdate(ev) + sendNode, + logout, + end, + /** Waits for the connection to WA to reach a state */ + waitForConnectionUpdate: bindWaitForConnectionUpdate(ev) } } + export type Socket = ReturnType \ No newline at end of file diff --git a/src/Store/make-ordered-dictionary.ts b/src/Store/make-ordered-dictionary.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/Tests/test.media-download.ts b/src/Tests/test.media-download.ts index baa29d5..81e7b4a 100644 --- a/src/Tests/test.media-download.ts +++ b/src/Tests/test.media-download.ts @@ -1,7 +1,7 @@ -import { MediaType, DownloadableMessage } from '../Types' -import { downloadContentFromMessage } from '../Utils' -import { proto } from '../../WAProto' import { readFileSync } from 'fs' +import { proto } from '../../WAProto' +import { DownloadableMessage, MediaType } from '../Types' +import { downloadContentFromMessage } from '../Utils' jest.setTimeout(20_000) @@ -41,7 +41,7 @@ describe('Media Download Tests', () => { const readPipe = await downloadContentFromMessage(message, type) let buffer = Buffer.alloc(0) - for await(const read of readPipe) { + for await (const read of readPipe) { buffer = Buffer.concat([ buffer, read ]) } @@ -61,7 +61,7 @@ describe('Media Download Tests', () => { const readPipe = await downloadContentFromMessage(message, type, range) let buffer = Buffer.alloc(0) - for await(const read of readPipe) { + for await (const read of readPipe) { buffer = Buffer.concat([ buffer, read ]) } diff --git a/src/Types/Auth.ts b/src/Types/Auth.ts index cd88192..30cc1ce 100644 --- a/src/Types/Auth.ts +++ b/src/Types/Auth.ts @@ -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 SignedKeyPair = { keyPair: KeyPair, signature: Uint8Array, keyId: number } diff --git a/src/Types/Chat.ts b/src/Types/Chat.ts index 06b7e50..3b98d84 100644 --- a/src/Types/Chat.ts +++ b/src/Types/Chat.ts @@ -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 */ export type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused' diff --git a/src/Types/Events.ts b/src/Types/Events.ts index dc7a4d0..039416c 100644 --- a/src/Types/Events.ts +++ b/src/Types/Events.ts @@ -1,12 +1,10 @@ -import type EventEmitter from "events" - +import type EventEmitter from 'events' import { AuthenticationCreds } from './Auth' import { Chat, PresenceData } from './Chat' import { Contact } from './Contact' -import { ConnectionState } from './State' - 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 = { /** connection state has been updated -- WS closed, opened, connecting etc. */ diff --git a/src/Types/GroupMetadata.ts b/src/Types/GroupMetadata.ts index 276636f..c4c0ff7 100644 --- a/src/Types/GroupMetadata.ts +++ b/src/Types/GroupMetadata.ts @@ -1,4 +1,4 @@ -import { Contact } from "./Contact"; +import { Contact } from './Contact' export type GroupParticipant = (Contact & { isAdmin?: boolean; isSuperAdmin?: boolean, admin?: 'admin' | 'superadmin' | null }) diff --git a/src/Types/Legacy.ts b/src/Types/Legacy.ts index c30e006..ad86ca7 100644 --- a/src/Types/Legacy.ts +++ b/src/Types/Legacy.ts @@ -1,6 +1,6 @@ -import { CommonSocketConfig } from "./Socket" -import { CommonBaileysEventEmitter } from "./Events" -import { BinaryNode } from "../WABinary" +import { BinaryNode } from '../WABinary' +import { CommonBaileysEventEmitter } from './Events' +import { CommonSocketConfig } from './Socket' export interface LegacyAuthenticationCreds { clientID: string diff --git a/src/Types/Message.ts b/src/Types/Message.ts index 277c00d..4ee130b 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -1,10 +1,9 @@ -import type { ReadStream } from "fs" -import type { Logger } from "pino" -import type { URL } from "url" -import type NodeCache from "node-cache" -import type { GroupMetadata } from "./GroupMetadata" -import type { Readable } from "stream" +import type NodeCache from 'node-cache' +import type { Logger } from 'pino' +import type { Readable } from 'stream' +import type { URL } from 'url' import { proto } from '../../WAProto' +import type { GroupMetadata } from './GroupMetadata' // export the WAMessage Prototypes export { proto as WAProto } diff --git a/src/Types/Socket.ts b/src/Types/Socket.ts index 8b2c403..aee0639 100644 --- a/src/Types/Socket.ts +++ b/src/Types/Socket.ts @@ -1,9 +1,9 @@ -import type { Agent } from "https" -import type { Logger } from "pino" -import type { URL } from "url" +import type { Agent } from 'https' 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 WABrowserDescription = [string, string, string] diff --git a/src/Types/State.ts b/src/Types/State.ts index e7f0cec..5774c50 100644 --- a/src/Types/State.ts +++ b/src/Types/State.ts @@ -1,4 +1,4 @@ -import { Contact } from "./Contact" +import { Contact } from './Contact' export type WAConnectionState = 'open' | 'connecting' | 'close' diff --git a/src/Types/index.ts b/src/Types/index.ts index c04d218..c0c0a11 100644 --- a/src/Types/index.ts +++ b/src/Types/index.ts @@ -9,9 +9,8 @@ export * from './Socket' export * from './Events' import type NodeCache from 'node-cache' - -import { AuthenticationState } from './Auth' import { proto } from '../../WAProto' +import { AuthenticationState } from './Auth' import { CommonSocketConfig } from './Socket' export type SocketConfig = CommonSocketConfig & { diff --git a/src/Utils/auth-utils.ts b/src/Utils/auth-utils.ts index 9230147..9013859 100644 --- a/src/Utils/auth-utils.ts +++ b/src/Utils/auth-utils.ts @@ -2,9 +2,9 @@ import { Boom } from '@hapi/boom' import { randomBytes } from 'crypto' import type { Logger } from 'pino' 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 { generateRegistrationId, BufferJSON } from './generics' +import { BufferJSON, generateRegistrationId } from './generics' const KEY_MAP: { [T in keyof SignalDataTypeMap]: string } = { 'pre-key': 'preKeys', @@ -46,6 +46,7 @@ export const addTransactionCapability = (state: SignalKeyStore, logger: Logger): if(value) { dict[id] = value } + return dict }, { } ) @@ -55,7 +56,7 @@ export const addTransactionCapability = (state: SignalKeyStore, logger: Logger): }, set: data => { 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) { transactionCache[key] = transactionCache[key] || { } Object.assign(transactionCache[key], data[key]) @@ -69,7 +70,7 @@ export const addTransactionCapability = (state: SignalKeyStore, logger: Logger): }, isInTransaction: () => inTransaction, prefetch: (type, ids) => { - logger.trace({ type, ids }, `prefetching`) + logger.trace({ type, ids }, 'prefetching') return prefetch(type, ids) }, transaction: async(work) => { @@ -128,17 +129,17 @@ export const useSingleFileAuthState = (filename: string, logger?: Logger): { sta ) } - if(existsSync(filename)) { - const result = JSON.parse( - readFileSync(filename, { encoding: 'utf-8' }), - BufferJSON.reviver - ) + if(existsSync(filename)) { + const result = JSON.parse( + readFileSync(filename, { encoding: 'utf-8' }), + BufferJSON.reviver + ) creds = result.creds keys = result.keys - } else { - creds = initAuthCreds() - keys = { } - } + } else { + creds = initAuthCreds() + keys = { } + } return { state: { @@ -153,8 +154,10 @@ export const useSingleFileAuthState = (filename: string, logger?: Logger): { sta if(type === 'app-state-sync-key') { value = proto.AppStateSyncKeyData.fromObject(value) } + dict[id] = value } + return dict }, { } ) @@ -165,6 +168,7 @@ export const useSingleFileAuthState = (filename: string, logger?: Logger): { sta keys[key] = keys[key] || { } Object.assign(keys[key], data[_key]) } + saveState() } } diff --git a/src/Utils/chat-utils.ts b/src/Utils/chat-utils.ts index fd2fc10..1ae606c 100644 --- a/src/Utils/chat-utils.ts +++ b/src/Utils/chat-utils.ts @@ -1,523 +1,541 @@ 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 { LT_HASH_ANTI_TAMPERING } from './lt-hash' +import { ChatModification, ChatMutation, LastMessageList, LTHashState, WAPatchCreate, WAPatchName } from '../Types' import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren } from '../WABinary' +import { aesDecrypt, aesEncrypt, hkdf, hmacSign } from './crypto' import { toNumber } from './generics' +import { LT_HASH_ANTI_TAMPERING } from './lt-hash' import { downloadContentFromMessage, } from './messages-media' type FetchAppStateSyncKey = (keyId: string) => Promise | proto.IAppStateSyncKeyData const mutationKeys = (keydata: Uint8Array) => { - const expanded = hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' }) - return { - indexKey: expanded.slice(0, 32), - valueEncryptionKey: expanded.slice(32, 64), - valueMacKey: expanded.slice(64, 96), - snapshotMacKey: expanded.slice(96, 128), - patchMacKey: expanded.slice(128, 160) - } + const expanded = hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' }) + return { + indexKey: expanded.slice(0, 32), + valueEncryptionKey: expanded.slice(32, 64), + valueMacKey: expanded.slice(64, 96), + snapshotMacKey: expanded.slice(96, 128), + patchMacKey: expanded.slice(128, 160) + } } const generateMac = (operation: proto.SyncdMutation.SyncdMutationSyncdOperation, data: Buffer, keyId: Uint8Array | string, key: Buffer) => { - const getKeyData = () => { - let r: number - switch (operation) { - case proto.SyncdMutation.SyncdMutationSyncdOperation.SET: - r = 0x01 - break - case proto.SyncdMutation.SyncdMutationSyncdOperation.REMOVE: - r = 0x02 - break - } - const buff = Buffer.from([r]) - return Buffer.concat([ buff, Buffer.from(keyId as any, 'base64') ]) - } - const keyData = getKeyData() + const getKeyData = () => { + let r: number + switch (operation) { + case proto.SyncdMutation.SyncdMutationSyncdOperation.SET: + r = 0x01 + break + case proto.SyncdMutation.SyncdMutationSyncdOperation.REMOVE: + r = 0x02 + break + } - const last = Buffer.alloc(8) // 8 bytes - last.set([ keyData.length ], last.length-1) + const buff = Buffer.from([r]) + return Buffer.concat([ buff, Buffer.from(keyId as any, 'base64') ]) + } - const total = Buffer.concat([ keyData, data, last ]) - const hmac = hmacSign(total, key, 'sha512') + const keyData = getKeyData() - return hmac.slice(0, 32) + const last = Buffer.alloc(8) // 8 bytes + last.set([ keyData.length ], last.length-1) + + const total = Buffer.concat([ keyData, data, last ]) + const hmac = hmacSign(total, key, 'sha512') + + return hmac.slice(0, 32) } -const to64BitNetworkOrder = function(e) { - const t = new ArrayBuffer(8) - new DataView(t).setUint32(4, e, !1) - return Buffer.from(t) +const to64BitNetworkOrder = (e: number) => { + const t = new ArrayBuffer(8) + new DataView(t).setUint32(4, e, !1) + return Buffer.from(t) } type Mac = { indexMac: Uint8Array, valueMac: Uint8Array, operation: proto.SyncdMutation.SyncdMutationSyncdOperation } const makeLtHashGenerator = ({ indexValueMap, hash }: Pick) => { - indexValueMap = { ...indexValueMap } - const addBuffs: ArrayBuffer[] = [] - const subBuffs: ArrayBuffer[] = [] + indexValueMap = { ...indexValueMap } + const addBuffs: ArrayBuffer[] = [] + const subBuffs: ArrayBuffer[] = [] - return { - mix: ({ indexMac, valueMac, operation }: Mac) => { - const indexMacBase64 = Buffer.from(indexMac).toString('base64') - const prevOp = indexValueMap[indexMacBase64] - if(operation === proto.SyncdMutation.SyncdMutationSyncdOperation.REMOVE) { - if(!prevOp) { - throw new Boom('tried remove, but no previous op', { data: { indexMac, valueMac } }) - } - // remove from index value mac, since this mutation is erased - delete indexValueMap[indexMacBase64] - } else { - addBuffs.push(new Uint8Array(valueMac).buffer) - // add this index into the history map - indexValueMap[indexMacBase64] = { valueMac } - } - if(prevOp) { - subBuffs.push(new Uint8Array(prevOp.valueMac).buffer) - } - }, - finish: () => { - const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(new Uint8Array(hash).buffer, addBuffs, subBuffs) - const buffer = Buffer.from(result) + return { + mix: ({ indexMac, valueMac, operation }: Mac) => { + const indexMacBase64 = Buffer.from(indexMac).toString('base64') + const prevOp = indexValueMap[indexMacBase64] + if(operation === proto.SyncdMutation.SyncdMutationSyncdOperation.REMOVE) { + if(!prevOp) { + throw new Boom('tried remove, but no previous op', { data: { indexMac, valueMac } }) + } - return { - hash: buffer, - indexValueMap - } - } - } + // remove from index value mac, since this mutation is erased + delete indexValueMap[indexMacBase64] + } else { + addBuffs.push(new Uint8Array(valueMac).buffer) + // add this index into the history map + indexValueMap[indexMacBase64] = { valueMac } + } + + if(prevOp) { + subBuffs.push(new Uint8Array(prevOp.valueMac).buffer) + } + }, + finish: () => { + const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(new Uint8Array(hash).buffer, addBuffs, subBuffs) + const buffer = Buffer.from(result) + + return { + hash: buffer, + indexValueMap + } + } + } } const generateSnapshotMac = (lthash: Uint8Array, version: number, name: WAPatchName, key: Buffer) => { - const total = Buffer.concat([ - lthash, - to64BitNetworkOrder(version), - Buffer.from(name, 'utf-8') - ]) - return hmacSign(total, key, 'sha256') + const total = Buffer.concat([ + lthash, + to64BitNetworkOrder(version), + Buffer.from(name, 'utf-8') + ]) + return hmacSign(total, key, 'sha256') } const generatePatchMac = (snapshotMac: Uint8Array, valueMacs: Uint8Array[], version: number, type: WAPatchName, key: Buffer) => { - const total = Buffer.concat([ - snapshotMac, - ...valueMacs, - to64BitNetworkOrder(version), - Buffer.from(type, 'utf-8') - ]) - return hmacSign(total, key) + const total = Buffer.concat([ + snapshotMac, + ...valueMacs, + to64BitNetworkOrder(version), + Buffer.from(type, 'utf-8') + ]) + return hmacSign(total, key) } export const newLTHashState = (): LTHashState => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} }) export const encodeSyncdPatch = async( - { type, index, syncAction, apiVersion, operation }: WAPatchCreate, - myAppStateKeyId: string, - state: LTHashState, - getAppStateSyncKey: FetchAppStateSyncKey + { type, index, syncAction, apiVersion, operation }: WAPatchCreate, + myAppStateKeyId: string, + state: LTHashState, + getAppStateSyncKey: FetchAppStateSyncKey ) => { - const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined - if(!key) { - throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 }) - } - const encKeyId = Buffer.from(myAppStateKeyId, 'base64') + const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined + if(!key) { + throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 }) + } - state = { ...state, indexValueMap: { ...state.indexValueMap } } + const encKeyId = Buffer.from(myAppStateKeyId, 'base64') - const indexBuffer = Buffer.from(JSON.stringify(index)) - const dataProto = proto.SyncActionData.fromObject({ - index: indexBuffer, - value: syncAction, - padding: new Uint8Array(0), - version: apiVersion - }) - const encoded = proto.SyncActionData.encode(dataProto).finish() + state = { ...state, indexValueMap: { ...state.indexValueMap } } - const keyValue = mutationKeys(key!.keyData!) + const indexBuffer = Buffer.from(JSON.stringify(index)) + const dataProto = proto.SyncActionData.fromObject({ + index: indexBuffer, + value: syncAction, + padding: new Uint8Array(0), + version: apiVersion + }) + const encoded = proto.SyncActionData.encode(dataProto).finish() - const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey) - const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey) - const indexMac = hmacSign(indexBuffer, keyValue.indexKey) + const keyValue = mutationKeys(key!.keyData!) - // update LT hash - const generator = makeLtHashGenerator(state) - generator.mix({ indexMac, valueMac, operation }) - Object.assign(state, generator.finish()) + const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey) + const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey) + const indexMac = hmacSign(indexBuffer, keyValue.indexKey) - state.version += 1 + // update LT hash + const generator = makeLtHashGenerator(state) + generator.mix({ indexMac, valueMac, operation }) + Object.assign(state, generator.finish()) - const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey) + state.version += 1 - const patch: proto.ISyncdPatch = { - patchMac: generatePatchMac(snapshotMac, [valueMac], state.version, type, keyValue.patchMacKey), - snapshotMac: snapshotMac, - keyId: { id: encKeyId }, - mutations: [ - { - operation: operation, - record: { - index: { - blob: indexMac - }, - value: { - blob: Buffer.concat([ encValue, valueMac ]) - }, - keyId: { id: encKeyId } - } - } - ] - } + const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey) - const base64Index = indexMac.toString('base64') - state.indexValueMap[base64Index] = { valueMac } + const patch: proto.ISyncdPatch = { + patchMac: generatePatchMac(snapshotMac, [valueMac], state.version, type, keyValue.patchMacKey), + snapshotMac: snapshotMac, + keyId: { id: encKeyId }, + mutations: [ + { + operation: operation, + record: { + index: { + blob: indexMac + }, + value: { + blob: Buffer.concat([ encValue, valueMac ]) + }, + keyId: { id: encKeyId } + } + } + ] + } - return { patch, state } + const base64Index = indexMac.toString('base64') + state.indexValueMap[base64Index] = { valueMac } + + return { patch, state } } export const decodeSyncdMutations = async( - msgMutations: (proto.ISyncdMutation | proto.ISyncdRecord)[], - initialState: LTHashState, - getAppStateSyncKey: FetchAppStateSyncKey, - validateMacs: boolean + msgMutations: (proto.ISyncdMutation | proto.ISyncdRecord)[], + initialState: LTHashState, + getAppStateSyncKey: FetchAppStateSyncKey, + validateMacs: boolean ) => { - const keyCache: { [_: string]: ReturnType } = { } - const getKey = async(keyId: Uint8Array) => { - const base64Key = Buffer.from(keyId!).toString('base64') - let key = keyCache[base64Key] - if(!key) { - const keyEnc = await getAppStateSyncKey(base64Key) - if(!keyEnc) { - throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 404, data: { msgMutations } }) - } - const result = mutationKeys(keyEnc.keyData!) - keyCache[base64Key] = result - key = result - } - return key - } + const keyCache: { [_: string]: ReturnType } = { } + const getKey = async(keyId: Uint8Array) => { + const base64Key = Buffer.from(keyId!).toString('base64') + let key = keyCache[base64Key] + if(!key) { + const keyEnc = await getAppStateSyncKey(base64Key) + if(!keyEnc) { + throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 404, data: { msgMutations } }) + } - const ltGenerator = makeLtHashGenerator(initialState) + const result = mutationKeys(keyEnc.keyData!) + keyCache[base64Key] = result + key = result + } - const mutations: ChatMutation[] = [] - // indexKey used to HMAC sign record.index.blob - // valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32] - // the remaining record.value.blob[0:-32] is the mac, it the HMAC sign of key.keyId + decoded proto data + length of bytes in keyId - for(const msgMutation of msgMutations!) { - // if it's a syncdmutation, get the operation property - // otherwise, if it's only a record -- it'll be a SET mutation - const operation = 'operation' in msgMutation ? msgMutation.operation : proto.SyncdMutation.SyncdMutationSyncdOperation.SET - const record = ('record' in msgMutation && !!msgMutation.record) ? msgMutation.record : msgMutation as proto.ISyncdRecord + return key + } + + const ltGenerator = makeLtHashGenerator(initialState) + + const mutations: ChatMutation[] = [] + // indexKey used to HMAC sign record.index.blob + // valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32] + // the remaining record.value.blob[0:-32] is the mac, it the HMAC sign of key.keyId + decoded proto data + length of bytes in keyId + for(const msgMutation of msgMutations!) { + // if it's a syncdmutation, get the operation property + // otherwise, if it's only a record -- it'll be a SET mutation + const operation = 'operation' in msgMutation ? msgMutation.operation : proto.SyncdMutation.SyncdMutationSyncdOperation.SET + const record = ('record' in msgMutation && !!msgMutation.record) ? msgMutation.record : msgMutation as proto.ISyncdRecord - const key = await getKey(record.keyId!.id!) - const content = Buffer.from(record.value!.blob!) - const encContent = content.slice(0, -32) - const ogValueMac = content.slice(-32) - if(validateMacs) { - const contentHmac = generateMac(operation, encContent, record.keyId!.id!, key.valueMacKey) - if(Buffer.compare(contentHmac, ogValueMac) !== 0) { - throw new Boom('HMAC content verification failed') - } - } + const key = await getKey(record.keyId!.id!) + const content = Buffer.from(record.value!.blob!) + const encContent = content.slice(0, -32) + const ogValueMac = content.slice(-32) + if(validateMacs) { + const contentHmac = generateMac(operation, encContent, record.keyId!.id!, key.valueMacKey) + if(Buffer.compare(contentHmac, ogValueMac) !== 0) { + throw new Boom('HMAC content verification failed') + } + } - const result = aesDecrypt(encContent, key.valueEncryptionKey) - const syncAction = proto.SyncActionData.decode(result) + const result = aesDecrypt(encContent, key.valueEncryptionKey) + const syncAction = proto.SyncActionData.decode(result) - if(validateMacs) { - const hmac = hmacSign(syncAction.index, key.indexKey) - if(Buffer.compare(hmac, record.index!.blob) !== 0) { - throw new Boom('HMAC index verification failed') - } - } + if(validateMacs) { + const hmac = hmacSign(syncAction.index, key.indexKey) + if(Buffer.compare(hmac, record.index!.blob) !== 0) { + throw new Boom('HMAC index verification failed') + } + } - const indexStr = Buffer.from(syncAction.index).toString() - mutations.push({ - syncAction, - index: JSON.parse(indexStr), - }) - ltGenerator.mix({ - indexMac: record.index!.blob!, - valueMac: ogValueMac, - operation: operation - }) - } + const indexStr = Buffer.from(syncAction.index).toString() + mutations.push({ + syncAction, + index: JSON.parse(indexStr), + }) + ltGenerator.mix({ + indexMac: record.index!.blob!, + valueMac: ogValueMac, + operation: operation + }) + } - return { mutations, ...ltGenerator.finish() } + return { mutations, ...ltGenerator.finish() } } export const decodeSyncdPatch = async( - msg: proto.ISyncdPatch, - name: WAPatchName, - initialState: LTHashState, - getAppStateSyncKey: FetchAppStateSyncKey, - validateMacs: boolean + msg: proto.ISyncdPatch, + name: WAPatchName, + initialState: LTHashState, + getAppStateSyncKey: FetchAppStateSyncKey, + validateMacs: boolean ) => { - if(validateMacs) { - const base64Key = Buffer.from(msg.keyId!.id).toString('base64') - const mainKeyObj = await getAppStateSyncKey(base64Key) - const mainKey = mutationKeys(mainKeyObj.keyData!) - const mutationmacs = msg.mutations!.map(mutation => mutation.record!.value!.blob!.slice(-32)) + if(validateMacs) { + const base64Key = Buffer.from(msg.keyId!.id).toString('base64') + const mainKeyObj = await getAppStateSyncKey(base64Key) + const mainKey = mutationKeys(mainKeyObj.keyData!) + const mutationmacs = msg.mutations!.map(mutation => mutation.record!.value!.blob!.slice(-32)) - const patchMac = generatePatchMac(msg.snapshotMac, mutationmacs, toNumber(msg.version!.version), name, mainKey.patchMacKey) - if(Buffer.compare(patchMac, msg.patchMac) !== 0) { - throw new Boom('Invalid patch mac') - } - } + const patchMac = generatePatchMac(msg.snapshotMac, mutationmacs, toNumber(msg.version!.version), name, mainKey.patchMacKey) + if(Buffer.compare(patchMac, msg.patchMac) !== 0) { + throw new Boom('Invalid patch mac') + } + } - const result = await decodeSyncdMutations(msg!.mutations!, initialState, getAppStateSyncKey, validateMacs) - return result + const result = await decodeSyncdMutations(msg!.mutations!, initialState, getAppStateSyncKey, validateMacs) + return result } export const extractSyncdPatches = async(result: BinaryNode) => { - const syncNode = getBinaryNodeChild(result, 'sync') - const collectionNodes = getBinaryNodeChildren(syncNode, 'collection') + const syncNode = getBinaryNodeChild(result, 'sync') + const collectionNodes = getBinaryNodeChildren(syncNode, 'collection') - const final = { } as { [T in WAPatchName]: { patches: proto.ISyncdPatch[], hasMorePatches: boolean, snapshot?: proto.ISyncdSnapshot } } - await Promise.all( - collectionNodes.map( - async collectionNode => { - const patchesNode = getBinaryNodeChild(collectionNode, 'patches') + const final = { } as { [T in WAPatchName]: { patches: proto.ISyncdPatch[], hasMorePatches: boolean, snapshot?: proto.ISyncdSnapshot } } + await Promise.all( + collectionNodes.map( + async collectionNode => { + const patchesNode = getBinaryNodeChild(collectionNode, 'patches') - const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch') - const snapshotNode = getBinaryNodeChild(collectionNode, 'snapshot') + const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch') + const snapshotNode = getBinaryNodeChild(collectionNode, 'snapshot') - const syncds: proto.ISyncdPatch[] = [] - const name = collectionNode.attrs.name as WAPatchName + const syncds: proto.ISyncdPatch[] = [] + 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 - if(snapshotNode && !!snapshotNode.content) { - if(!Buffer.isBuffer(snapshotNode)) { - snapshotNode.content = Buffer.from(Object.values(snapshotNode.content)) - } - const blobRef = proto.ExternalBlobReference.decode( + let snapshot: proto.ISyncdSnapshot | undefined = undefined + if(snapshotNode && !!snapshotNode.content) { + if(!Buffer.isBuffer(snapshotNode)) { + snapshotNode.content = Buffer.from(Object.values(snapshotNode.content)) + } + + const blobRef = proto.ExternalBlobReference.decode( snapshotNode.content! as Buffer - ) - const data = await downloadExternalBlob(blobRef) - snapshot = proto.SyncdSnapshot.decode(data) - } + ) + const data = await downloadExternalBlob(blobRef) + snapshot = proto.SyncdSnapshot.decode(data) + } - for(let { content } of patches) { - if(content) { - if(!Buffer.isBuffer(content)) { - content = Buffer.from(Object.values(content)) - } - const syncd = proto.SyncdPatch.decode(content! as Uint8Array) - if(!syncd.version) { - syncd.version = { version: +collectionNode.attrs.version+1 } - } - syncds.push(syncd) - } - } + for(let { content } of patches) { + if(content) { + if(!Buffer.isBuffer(content)) { + content = Buffer.from(Object.values(content)) + } + + const syncd = proto.SyncdPatch.decode(content! as Uint8Array) + if(!syncd.version) { + syncd.version = { version: +collectionNode.attrs.version+1 } + } + + syncds.push(syncd) + } + } - final[name] = { patches: syncds, hasMorePatches, snapshot } - } - ) - ) + final[name] = { patches: syncds, hasMorePatches, snapshot } + } + ) + ) - return final + return final } export const downloadExternalBlob = async(blob: proto.IExternalBlobReference) => { - const stream = await downloadContentFromMessage(blob, 'md-app-state') + const stream = await downloadContentFromMessage(blob, 'md-app-state') let buffer = Buffer.from([]) - for await(const chunk of stream) { + for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]) } - return buffer + + return buffer } export const downloadExternalPatch = async(blob: proto.IExternalBlobReference) => { - const buffer = await downloadExternalBlob(blob) + const buffer = await downloadExternalBlob(blob) const syncData = proto.SyncdMutations.decode(buffer) return syncData } export const decodeSyncdSnapshot = async( - name: WAPatchName, - snapshot: proto.ISyncdSnapshot, - getAppStateSyncKey: FetchAppStateSyncKey, - minimumVersionNumber: number | undefined, - validateMacs: boolean = true + name: WAPatchName, + snapshot: proto.ISyncdSnapshot, + getAppStateSyncKey: FetchAppStateSyncKey, + minimumVersionNumber: number | undefined, + validateMacs: boolean = true ) => { - const newState = newLTHashState() - newState.version = toNumber(snapshot.version!.version!) + const newState = newLTHashState() + newState.version = toNumber(snapshot.version!.version!) - let { hash, indexValueMap, mutations } = await decodeSyncdMutations(snapshot.records!, newState, getAppStateSyncKey, validateMacs) - newState.hash = hash - newState.indexValueMap = indexValueMap + const { hash, indexValueMap, mutations } = await decodeSyncdMutations(snapshot.records!, newState, getAppStateSyncKey, validateMacs) + newState.hash = hash + newState.indexValueMap = indexValueMap - if(validateMacs) { - const base64Key = Buffer.from(snapshot.keyId!.id!).toString('base64') - const keyEnc = await getAppStateSyncKey(base64Key) - if(!keyEnc) { - throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 500 }) - } - const result = mutationKeys(keyEnc.keyData!) - const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey) - if(Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) { - throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`, { statusCode: 500 }) - } - } + if(validateMacs) { + const base64Key = Buffer.from(snapshot.keyId!.id!).toString('base64') + const keyEnc = await getAppStateSyncKey(base64Key) + if(!keyEnc) { + throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 500 }) + } - const areMutationsRequired = typeof minimumVersionNumber === 'undefined' || newState.version > minimumVersionNumber - if(!areMutationsRequired) { - mutations = [] - } + const result = mutationKeys(keyEnc.keyData!) + const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey) + if(Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) { + throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`, { statusCode: 500 }) + } + } - return { - state: newState, - mutations - } + const areMutationsRequired = typeof minimumVersionNumber === 'undefined' || newState.version > minimumVersionNumber + if(!areMutationsRequired) { + // clear array + mutations.splice(0, mutations.length) + } + + return { + state: newState, + mutations + } } export const decodePatches = async( - name: WAPatchName, - syncds: proto.ISyncdPatch[], - initial: LTHashState, - getAppStateSyncKey: FetchAppStateSyncKey, - minimumVersionNumber?: number, - validateMacs: boolean = true + name: WAPatchName, + syncds: proto.ISyncdPatch[], + initial: LTHashState, + getAppStateSyncKey: FetchAppStateSyncKey, + minimumVersionNumber?: number, + validateMacs: boolean = true ) => { - const successfulMutations: ChatMutation[] = [] + const successfulMutations: ChatMutation[] = [] - const newState: LTHashState = { - ...initial, - indexValueMap: { ...initial.indexValueMap } - } + const newState: LTHashState = { + ...initial, + indexValueMap: { ...initial.indexValueMap } + } - for(const syncd of syncds) { - const { version, keyId, snapshotMac } = syncd - if(syncd.externalMutations) { - const ref = await downloadExternalPatch(syncd.externalMutations) - syncd.mutations.push(...ref.mutations) - } + for(const syncd of syncds) { + const { version, keyId, snapshotMac } = syncd + if(syncd.externalMutations) { + const ref = await downloadExternalPatch(syncd.externalMutations) + syncd.mutations.push(...ref.mutations) + } - const patchVersion = toNumber(version.version!) + const patchVersion = toNumber(version.version!) - newState.version = patchVersion + newState.version = patchVersion - const decodeResult = await decodeSyncdPatch(syncd, name, newState, getAppStateSyncKey, validateMacs) + const decodeResult = await decodeSyncdPatch(syncd, name, newState, getAppStateSyncKey, validateMacs) - newState.hash = decodeResult.hash - newState.indexValueMap = decodeResult.indexValueMap - if(typeof minimumVersionNumber === 'undefined' || patchVersion > minimumVersionNumber) { - successfulMutations.push(...decodeResult.mutations) - } + newState.hash = decodeResult.hash + newState.indexValueMap = decodeResult.indexValueMap + if(typeof minimumVersionNumber === 'undefined' || patchVersion > minimumVersionNumber) { + successfulMutations.push(...decodeResult.mutations) + } - if(validateMacs) { - const base64Key = Buffer.from(keyId!.id!).toString('base64') - const keyEnc = await getAppStateSyncKey(base64Key) - if(!keyEnc) { - throw new Boom(`failed to find key "${base64Key}" to decode mutation`) - } - const result = mutationKeys(keyEnc.keyData!) - const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey) - if(Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) { - throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`) - } - } - } - return { - newMutations: successfulMutations, - state: newState - } + if(validateMacs) { + const base64Key = Buffer.from(keyId!.id!).toString('base64') + const keyEnc = await getAppStateSyncKey(base64Key) + if(!keyEnc) { + throw new Boom(`failed to find key "${base64Key}" to decode mutation`) + } + + const result = mutationKeys(keyEnc.keyData!) + const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey) + if(Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) { + throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`) + } + } + } + + return { + newMutations: successfulMutations, + state: newState + } } export const chatModificationToAppPatch = ( - mod: ChatModification, - jid: string + mod: ChatModification, + jid: string ) => { - const OP = proto.SyncdMutation.SyncdMutationSyncdOperation - const getMessageRange = (lastMessages: LastMessageList) => { - if(!lastMessages?.length) { - throw new Boom('Expected last message to be not from me', { statusCode: 400 }) - } - const lastMsg = lastMessages[lastMessages.length-1] - if(lastMsg.key.fromMe) { - throw new Boom('Expected last message in array to be not from me', { statusCode: 400 }) - } - const messageRange: proto.ISyncActionMessageRange = { - lastMessageTimestamp: lastMsg?.messageTimestamp, - messages: lastMessages - } - return messageRange - } - let patch: WAPatchCreate - if('mute' in mod) { - patch = { - syncAction: { - muteAction: { - muted: !!mod.mute, - muteEndTimestamp: mod.mute || undefined - } - }, - index: ['mute', jid], - type: 'regular_high', - apiVersion: 2, - operation: OP.SET - } - } else if('archive' in mod) { - patch = { - syncAction: { - archiveChatAction: { - archived: !!mod.archive, - messageRange: getMessageRange(mod.lastMessages) - } - }, - index: ['archive', jid], - type: 'regular_low', - apiVersion: 3, - operation: OP.SET - } - } else if('markRead' in mod) { - patch = { - syncAction: { - markChatAsReadAction: { - read: mod.markRead, - messageRange: getMessageRange(mod.lastMessages) - } - }, - index: ['markChatAsRead', jid], - type: 'regular_low', - apiVersion: 3, - operation: OP.SET - } - } else if('clear' in mod) { - if(mod.clear === 'all') { - throw new Boom('not supported') - } else { - const key = mod.clear.messages[0] - patch = { - syncAction: { - deleteMessageForMeAction: { - deleteMedia: false - } - }, - index: ['deleteMessageForMe', jid, key.id, key.fromMe ? '1' : '0', '0'], - type: 'regular_high', - apiVersion: 3, - operation: OP.SET - } - } - } else if('pin' in mod) { - patch = { - syncAction: { - pinAction: { - pinned: !!mod.pin - } - }, - index: ['pin_v1', jid], - type: 'regular_low', - apiVersion: 5, - operation: OP.SET - } - } else { - throw new Boom('not supported') - } + const OP = proto.SyncdMutation.SyncdMutationSyncdOperation + const getMessageRange = (lastMessages: LastMessageList) => { + if(!lastMessages?.length) { + throw new Boom('Expected last message to be not from me', { statusCode: 400 }) + } - patch.syncAction.timestamp = Date.now() + const lastMsg = lastMessages[lastMessages.length-1] + if(lastMsg.key.fromMe) { + throw new Boom('Expected last message in array to be not from me', { statusCode: 400 }) + } - return patch + const messageRange: proto.ISyncActionMessageRange = { + lastMessageTimestamp: lastMsg?.messageTimestamp, + messages: lastMessages + } + return messageRange + } + + let patch: WAPatchCreate + if('mute' in mod) { + patch = { + syncAction: { + muteAction: { + muted: !!mod.mute, + muteEndTimestamp: mod.mute || undefined + } + }, + index: ['mute', jid], + type: 'regular_high', + apiVersion: 2, + operation: OP.SET + } + } else if('archive' in mod) { + patch = { + syncAction: { + archiveChatAction: { + archived: !!mod.archive, + messageRange: getMessageRange(mod.lastMessages) + } + }, + index: ['archive', jid], + type: 'regular_low', + apiVersion: 3, + operation: OP.SET + } + } else if('markRead' in mod) { + patch = { + syncAction: { + markChatAsReadAction: { + read: mod.markRead, + messageRange: getMessageRange(mod.lastMessages) + } + }, + index: ['markChatAsRead', jid], + type: 'regular_low', + apiVersion: 3, + operation: OP.SET + } + } else if('clear' in mod) { + if(mod.clear === 'all') { + throw new Boom('not supported') + } else { + const key = mod.clear.messages[0] + patch = { + syncAction: { + deleteMessageForMeAction: { + deleteMedia: false + } + }, + index: ['deleteMessageForMe', jid, key.id, key.fromMe ? '1' : '0', '0'], + type: 'regular_high', + apiVersion: 3, + operation: OP.SET + } + } + } else if('pin' in mod) { + patch = { + syncAction: { + pinAction: { + pinned: !!mod.pin + } + }, + index: ['pin_v1', jid], + type: 'regular_low', + apiVersion: 5, + operation: OP.SET + } + } else { + throw new Boom('not supported') + } + + patch.syncAction.timestamp = Date.now() + + return patch } \ No newline at end of file diff --git a/src/Utils/crypto.ts b/src/Utils/crypto.ts index babffd0..81b4e74 100644 --- a/src/Utils/crypto.ts +++ b/src/Utils/crypto.ts @@ -1,5 +1,5 @@ -import * as curveJs from 'curve25519-js' import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto' +import * as curveJs from 'curve25519-js' import { KeyPair } from '../Types' export const Curve = { @@ -23,70 +23,77 @@ export const Curve = { } export const signedKeyPair = (keyPair: KeyPair, keyId: number) => { - const signKeys = Curve.generateKeyPair() - const pubKey = new Uint8Array(33) - pubKey.set([5], 0) - pubKey.set(signKeys.public, 1) + const signKeys = Curve.generateKeyPair() + const pubKey = new Uint8Array(33) + pubKey.set([5], 0) + pubKey.set(signKeys.public, 1) - const signature = Curve.sign(keyPair.private, pubKey) + const signature = Curve.sign(keyPair.private, pubKey) - return { keyPair: signKeys, signature, keyId } + return { keyPair: signKeys, signature, keyId } } /** decrypt AES 256 CBC; where the IV is prefixed to the 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 */ export function aesDecryptWithIV(buffer: Buffer, key: Buffer, IV: Buffer) { - const aes = createDecipheriv('aes-256-cbc', key, IV) - return Buffer.concat([aes.update(buffer), aes.final()]) + const aes = createDecipheriv('aes-256-cbc', key, IV) + return Buffer.concat([aes.update(buffer), aes.final()]) } + // encrypt AES 256 CBC; where a random IV is prefixed to the buffer export function aesEncrypt(buffer: Buffer | Uint8Array, key: Buffer) { - const IV = randomBytes(16) - const aes = createCipheriv('aes-256-cbc', key, IV) - return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer + const IV = randomBytes(16) + const aes = createCipheriv('aes-256-cbc', key, IV) + return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer } + // encrypt AES 256 CBC with a given IV export function aesEncrypWithIV(buffer: Buffer, key: Buffer, IV: Buffer) { - const aes = createCipheriv('aes-256-cbc', key, IV) - return Buffer.concat([aes.update(buffer), aes.final()]) // prefix IV to the buffer + const aes = createCipheriv('aes-256-cbc', key, IV) + return Buffer.concat([aes.update(buffer), aes.final()]) // prefix IV to the buffer } + // sign HMAC using SHA 256 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) { - return createHash('sha256').update(buffer).digest() + return createHash('sha256').update(buffer).digest() } + // HKDF key expansion // from: https://github.com/benadida/node-hkdf export function hkdf(buffer: Uint8Array, expandedLength: number, { info, salt }: { salt?: Buffer, info?: string }) { - const hashAlg = 'sha256' - const hashLength = 32 - salt = salt || Buffer.alloc(hashLength) - // now we compute the PRK - const prk = createHmac(hashAlg, salt).update(buffer).digest() + const hashAlg = 'sha256' + const hashLength = 32 + salt = salt || Buffer.alloc(hashLength) + // now we compute the PRK + const prk = createHmac(hashAlg, salt).update(buffer).digest() - let prev = Buffer.from([]) - const buffers = [] - const num_blocks = Math.ceil(expandedLength / hashLength) + let prev = Buffer.from([]) + const buffers = [] + const num_blocks = Math.ceil(expandedLength / hashLength) - const infoBuff = Buffer.from(info || []) + const infoBuff = Buffer.from(info || []) - for (var i=0; i { - //const deviceIdentity = (stanza.content as BinaryNodeM[])?.find(m => m.tag === 'device-identity') - //const deviceIdentityBytes = deviceIdentity ? deviceIdentity.content as Buffer : undefined + //const deviceIdentity = (stanza.content as BinaryNodeM[])?.find(m => m.tag === 'device-identity') + //const deviceIdentityBytes = deviceIdentity ? deviceIdentity.content as Buffer : undefined - let msgType: MessageType - let chatId: string - let author: string + let msgType: MessageType + let chatId: string + let author: string - const msgId: string = stanza.attrs.id - const from: string = stanza.attrs.from - const participant: string | undefined = stanza.attrs.participant - const recipient: string | undefined = stanza.attrs.recipient + const msgId: string = stanza.attrs.id + const from: string = stanza.attrs.from + const participant: string | undefined = stanza.attrs.participant + const recipient: string | undefined = stanza.attrs.recipient - const isMe = (jid: string) => areJidsSameUser(jid, auth.creds.me!.id) + const isMe = (jid: string) => areJidsSameUser(jid, auth.creds.me!.id) - if(isJidUser(from)) { - if(recipient) { - if(!isMe(from)) { - throw new Boom('') - } - chatId = recipient - } else { - chatId = from - } - msgType = 'chat' - author = from - } else if(isJidGroup(from)) { - if(!participant) { - throw new Boom('No participant in group message') - } - msgType = 'group' - author = participant - chatId = from - } else if(isJidBroadcast(from)) { - if(!participant) { - throw new Boom('No participant in group message') - } - const isParticipantMe = isMe(participant) - if(isJidStatusBroadcast(from)) { - msgType = isParticipantMe ? 'direct_peer_status' : 'other_status' - } else { - msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast' - } - chatId = from - author = participant - } + if(isJidUser(from)) { + if(recipient) { + if(!isMe(from)) { + throw new Boom('') + } - const sender = msgType === 'chat' ? author : chatId + chatId = recipient + } else { + chatId = from + } - const fromMe = isMe(stanza.attrs.participant || stanza.attrs.from) - const pushname = stanza.attrs.notify + msgType = 'chat' + author = from + } else if(isJidGroup(from)) { + if(!participant) { + throw new Boom('No participant in group message') + } + + msgType = 'group' + author = participant + chatId = from + } else if(isJidBroadcast(from)) { + if(!participant) { + throw new Boom('No participant in group message') + } + + const isParticipantMe = isMe(participant) + if(isJidStatusBroadcast(from)) { + msgType = isParticipantMe ? 'direct_peer_status' : 'other_status' + } else { + msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast' + } + + chatId = from + author = participant + } + + const sender = msgType === 'chat' ? author : chatId + + const fromMe = isMe(stanza.attrs.participant || stanza.attrs.from) + const pushname = stanza.attrs.notify - const key: WAMessageKey = { - remoteJid: chatId, - fromMe, - id: msgId, - participant - } + const key: WAMessageKey = { + remoteJid: chatId, + fromMe, + id: msgId, + participant + } - const fullMessage: proto.IWebMessageInfo = { - key, - messageTimestamp: +stanza.attrs.t, - pushName: pushname - } + const fullMessage: proto.IWebMessageInfo = { + key, + messageTimestamp: +stanza.attrs.t, + pushName: pushname + } - if(key.fromMe) { - fullMessage.status = proto.WebMessageInfo.WebMessageInfoStatus.SERVER_ACK - } + if(key.fromMe) { + fullMessage.status = proto.WebMessageInfo.WebMessageInfoStatus.SERVER_ACK + } - if(Array.isArray(stanza.content)) { - for(const { tag, attrs, content } of stanza.content) { - if(tag !== 'enc') continue - if(!(content instanceof Uint8Array)) continue + if(Array.isArray(stanza.content)) { + for(const { tag, attrs, content } of stanza.content) { + if(tag !== 'enc') { + continue + } - let msgBuffer: Buffer + if(!(content instanceof Uint8Array)) { + continue + } - try { - const e2eType = attrs.type - switch(e2eType) { - case 'skmsg': - msgBuffer = await decryptGroupSignalProto(sender, author, content, auth) - break - case 'pkmsg': - case 'msg': - const user = isJidUser(sender) ? sender : author - msgBuffer = await decryptSignalProto(user, e2eType, content as Buffer, auth) - break - } - let msg: proto.IMessage = proto.Message.decode(unpadRandomMax16(msgBuffer)) - msg = msg.deviceSentMessage?.message || msg - if(msg.senderKeyDistributionMessage) { - await processSenderKeyMessage(author, msg.senderKeyDistributionMessage, auth) - } + let msgBuffer: Buffer - if(fullMessage.message) Object.assign(fullMessage.message, msg) - else fullMessage.message = msg - } catch(error) { - fullMessage.messageStubType = proto.WebMessageInfo.WebMessageInfoStubType.CIPHERTEXT - fullMessage.messageStubParameters = [error.message] - } - } - } + try { + const e2eType = attrs.type + switch (e2eType) { + case 'skmsg': + msgBuffer = await decryptGroupSignalProto(sender, author, content, auth) + break + case 'pkmsg': + case 'msg': + const user = isJidUser(sender) ? sender : author + msgBuffer = await decryptSignalProto(user, e2eType, content as Buffer, auth) + break + } - return fullMessage + let msg: proto.IMessage = proto.Message.decode(unpadRandomMax16(msgBuffer)) + msg = msg.deviceSentMessage?.message || msg + if(msg.senderKeyDistributionMessage) { + await processSenderKeyMessage(author, msg.senderKeyDistributionMessage, auth) + } + + if(fullMessage.message) { + Object.assign(fullMessage.message, msg) + } else { + fullMessage.message = msg + } + } catch(error) { + fullMessage.messageStubType = proto.WebMessageInfo.WebMessageInfoStubType.CIPHERTEXT + fullMessage.messageStubParameters = [error.message] + } + } + } + + return fullMessage } \ No newline at end of file diff --git a/src/Utils/generics.ts b/src/Utils/generics.ts index 54a37ff..566c377 100644 --- a/src/Utils/generics.ts +++ b/src/Utils/generics.ts @@ -2,217 +2,233 @@ import { Boom } from '@hapi/boom' import { randomBytes } from 'crypto' import { platform, release } from 'os' import { Logger } from 'pino' -import { ConnectionState } from '..' import { proto } from '../../WAProto' import { CommonBaileysEventEmitter, DisconnectReason } from '../Types' import { Binary } from '../WABinary' +import { ConnectionState } from '..' const PLATFORM_MAP = { - 'aix': 'AIX', - 'darwin': 'Mac OS', - 'win32': 'Windows', - 'android': 'Android' + 'aix': 'AIX', + 'darwin': 'Mac OS', + 'win32': 'Windows', + 'android': 'Android' } export const Browsers = { - ubuntu: browser => ['Ubuntu', browser, '18.04'] as [string, string, string], - macOS: browser => ['Mac OS', browser, '10.15.3'] as [string, string, string], - baileys: browser => ['Baileys', browser, '4.0.0'] as [string, string, string], - /** The appropriate browser based on your OS & release */ - appropriate: browser => [ PLATFORM_MAP[platform()] || 'Ubuntu', browser, release() ] as [string, string, string] + ubuntu: browser => ['Ubuntu', browser, '18.04'] as [string, string, string], + macOS: browser => ['Mac OS', browser, '10.15.3'] as [string, string, string], + baileys: browser => ['Baileys', browser, '4.0.0'] as [string, string, string], + /** The appropriate browser based on your OS & release */ + appropriate: browser => [ PLATFORM_MAP[platform()] || 'Ubuntu', browser, release() ] as [string, string, string] } export const BufferJSON = { - replacer: (k, value: any) => { - if(Buffer.isBuffer(value) || value instanceof Uint8Array || value?.type === 'Buffer') { - return { type: 'Buffer', data: Buffer.from(value?.data || value).toString('base64') } - } - return value - }, - reviver: (_, value: any) => { - if(typeof value === 'object' && !!value && (value.buffer === true || value.type === 'Buffer')) { - const val = value.data || value.value - return typeof val === 'string' ? Buffer.from(val, 'base64') : Buffer.from(val) - } - return value - } + replacer: (k, value: any) => { + if(Buffer.isBuffer(value) || value instanceof Uint8Array || value?.type === 'Buffer') { + return { type: 'Buffer', data: Buffer.from(value?.data || value).toString('base64') } + } + + return value + }, + reviver: (_, value: any) => { + if(typeof value === 'object' && !!value && (value.buffer === true || value.type === 'Buffer')) { + const val = value.data || value.value + return typeof val === 'string' ? Buffer.from(val, 'base64') : Buffer.from(val) + } + + return value + } } -export const writeRandomPadMax16 = function(e: Binary) { - function r(e: Binary, t: number) { - for (var r = 0; r < t; r++) - e.writeUint8(t) - } +export const writeRandomPadMax16 = (e: Binary) => { + function r(e: Binary, t: number) { + for(var r = 0; r < t; r++) { + e.writeUint8(t) + } + } - var t = randomBytes(1) - r(e, 1 + (15 & t[0])) - return e + var t = randomBytes(1) + r(e, 1 + (15 & t[0])) + return e } export const unpadRandomMax16 = (e: Uint8Array | Buffer) => { - const t = new Uint8Array(e); - if (0 === t.length) { - throw new Error('unpadPkcs7 given empty bytes'); - } + const t = new Uint8Array(e) + if(0 === t.length) { + throw new Error('unpadPkcs7 given empty bytes') + } - var r = t[t.length - 1]; - if (r > t.length) { - throw new Error(`unpad given ${t.length} bytes, but pad is ${r}`); - } + var r = t[t.length - 1] + if(r > t.length) { + 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) => ( - Buffer.from( - writeRandomPadMax16( - new Binary(proto.Message.encode(message).finish()) - ).readByteArray() - ) + Buffer.from( + writeRandomPadMax16( + new Binary(proto.Message.encode(message).finish()) + ).readByteArray() + ) ) export const generateRegistrationId = () => ( - Uint16Array.from(randomBytes(2))[0] & 0x3fff + Uint16Array.from(randomBytes(2))[0] & 0x3fff ) export const encodeInt = (e: number, t: number) => { - for (var r = t, a = new Uint8Array(e), i = e - 1; i >= 0; i--) { - a[i] = 255 & r - r >>>= 8 - } - return a + for(var r = t, a = new Uint8Array(e), i = e - 1; i >= 0; i--) { + a[i] = 255 & r + r >>>= 8 + } + + return a } + export const encodeBigEndian = (e: number, t=4) => { - let r = e; - let a = new Uint8Array(t); - for (let i = t - 1; i >= 0; i--) { - a[i] = 255 & r - r >>>= 8 - } - return a + let r = e + const a = new Uint8Array(t) + for(let i = t - 1; i >= 0; i--) { + a[i] = 255 & r + r >>>= 8 + } + + return a } export const toNumber = (t: Long | number) => ((typeof t === 'object' && 'toNumber' in t) ? t.toNumber() : t) -export function shallowChanges (old: T, current: T, {lookForDeletedKeys}: {lookForDeletedKeys: boolean}): Partial { - let changes: Partial = {} - for (let key in current) { - if (old[key] !== current[key]) { - changes[key] = current[key] || null - } - } - if (lookForDeletedKeys) { - for (let key in old) { - if (!changes[key] && old[key] !== current[key]) { - changes[key] = current[key] || null - } - } - } - return changes +export function shallowChanges (old: T, current: T, { lookForDeletedKeys }: {lookForDeletedKeys: boolean}): Partial { + const changes: Partial = {} + for(const key in current) { + if(old[key] !== current[key]) { + changes[key] = current[key] || null + } + } + + if(lookForDeletedKeys) { + for(const key in old) { + if(!changes[key] && old[key] !== current[key]) { + changes[key] = current[key] || null + } + } + } + + return changes } + /** unix timestamp of a date in seconds */ export const unixTimestampSeconds = (date: Date = new Date()) => Math.floor(date.getTime()/1000) export type DebouncedTimeout = ReturnType export const debouncedTimeout = (intervalMs: number = 1000, task: () => void = undefined) => { - let timeout: NodeJS.Timeout - return { - start: (newIntervalMs?: number, newTask?: () => void) => { - task = newTask || task - intervalMs = newIntervalMs || intervalMs - timeout && clearTimeout(timeout) - timeout = setTimeout(task, intervalMs) - }, - cancel: () => { - timeout && clearTimeout(timeout) - timeout = undefined - }, - setTask: (newTask: () => void) => task = newTask, - setInterval: (newInterval: number) => intervalMs = newInterval - } + let timeout: NodeJS.Timeout + return { + start: (newIntervalMs?: number, newTask?: () => void) => { + task = newTask || task + intervalMs = newIntervalMs || intervalMs + timeout && clearTimeout(timeout) + timeout = setTimeout(task, intervalMs) + }, + cancel: () => { + timeout && clearTimeout(timeout) + timeout = undefined + }, + setTask: (newTask: () => void) => task = newTask, + setInterval: (newInterval: number) => intervalMs = newInterval + } } export const delay = (ms: number) => delayCancellable (ms).delay export const delayCancellable = (ms: number) => { - const stack = new Error().stack - let timeout: NodeJS.Timeout - let reject: (error) => void - const delay: Promise = new Promise((resolve, _reject) => { - timeout = setTimeout(resolve, ms) - reject = _reject - }) - const cancel = () => { - clearTimeout (timeout) - reject( - new Boom('Cancelled', { - statusCode: 500, - data: { - stack - } - }) - ) - } - return { delay, cancel } + const stack = new Error().stack + let timeout: NodeJS.Timeout + let reject: (error) => void + const delay: Promise = new Promise((resolve, _reject) => { + timeout = setTimeout(resolve, ms) + reject = _reject + }) + const cancel = () => { + clearTimeout (timeout) + reject( + new Boom('Cancelled', { + statusCode: 500, + data: { + stack + } + }) + ) + } + + return { delay, cancel } } + export async function promiseTimeout(ms: number, promise: (resolve: (v?: T)=>void, reject: (error) => void) => void) { - if (!ms) return new Promise (promise) - const stack = new Error().stack - // Create a promise that rejects in milliseconds - let {delay, cancel} = delayCancellable (ms) - const p = new Promise ((resolve, reject) => { - delay - .then(() => reject( - new Boom('Timed Out', { - statusCode: DisconnectReason.timedOut, - data: { - stack - } - }) - )) - .catch (err => reject(err)) + if(!ms) { + return new Promise (promise) + } + + const stack = new Error().stack + // Create a promise that rejects in milliseconds + const { delay, cancel } = delayCancellable (ms) + const p = new Promise ((resolve, reject) => { + delay + .then(() => reject( + new Boom('Timed Out', { + statusCode: DisconnectReason.timedOut, + data: { + stack + } + }) + )) + .catch (err => reject(err)) - promise (resolve, reject) - }) - .finally (cancel) - return p as Promise + promise (resolve, reject) + }) + .finally (cancel) + return p as Promise } + // generate a random ID to attach to a message export const generateMessageID = () => 'BAE5' + randomBytes(6).toString('hex').toUpperCase() export const bindWaitForConnectionUpdate = (ev: CommonBaileysEventEmitter) => ( - async(check: (u: Partial) => boolean, timeoutMs?: number) => { - let listener: (item: Partial) => void - await ( - promiseTimeout( - timeoutMs, - (resolve, reject) => { - listener = (update) => { - if(check(update)) { - resolve() - } else if(update.connection == 'close') { + async(check: (u: Partial) => boolean, timeoutMs?: number) => { + let listener: (item: Partial) => void + await ( + promiseTimeout( + timeoutMs, + (resolve, reject) => { + listener = (update) => { + if(check(update)) { + resolve() + } else if(update.connection === 'close') { reject(update.lastDisconnect?.error || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })) } } - ev.on('connection.update', listener) - } - ) - .finally(() => ( - ev.off('connection.update', listener) - )) - ) - } + + ev.on('connection.update', listener) + } + ) + .finally(() => ( + ev.off('connection.update', listener) + )) + ) + } ) export const printQRIfNecessaryListener = (ev: CommonBaileysEventEmitter, logger: Logger) => { - ev.on('connection.update', async({ qr }) => { - if(qr) { - const QR = await import('qrcode-terminal') - .catch(err => { - logger.error('QR code terminal not added as dependency') - }) - QR?.generate(qr, { small: true }) - } - }) + ev.on('connection.update', async({ qr }) => { + if(qr) { + const QR = await import('qrcode-terminal') + .catch(err => { + logger.error('QR code terminal not added as dependency') + }) + QR?.generate(qr, { small: true }) + } + }) } \ No newline at end of file diff --git a/src/Utils/history.ts b/src/Utils/history.ts index d53e84d..1caf4d5 100644 --- a/src/Utils/history.ts +++ b/src/Utils/history.ts @@ -1,17 +1,18 @@ -import { downloadContentFromMessage } from "./messages-media" -import { proto } from "../../WAProto" import { promisify } from 'util' -import { inflate } from "zlib" -import { Chat, Contact } from "../Types" +import { inflate } from 'zlib' +import { proto } from '../../WAProto' +import { Chat, Contact } from '../Types' +import { downloadContentFromMessage } from './messages-media' const inflatePromise = promisify(inflate) export const downloadHistory = async(msg: proto.IHistorySyncNotification) => { const stream = await downloadContentFromMessage(msg, 'history') let buffer = Buffer.from([]) - for await(const chunk of stream) { + for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]) } + // decompress buffer buffer = await inflatePromise(buffer) @@ -24,45 +25,47 @@ export const processHistoryMessage = (item: proto.IHistorySync, historyCache: Se const messages: proto.IWebMessageInfo[] = [] const contacts: Contact[] = [] const chats: Chat[] = [] - switch(item.syncType) { - case proto.HistorySync.HistorySyncHistorySyncType.INITIAL_BOOTSTRAP: - case proto.HistorySync.HistorySyncHistorySyncType.RECENT: - for(const chat of item.conversations) { - const contactId = `c:${chat.id}` - if(chat.name && !historyCache.has(contactId)) { - contacts.push({ - id: chat.id, - name: chat.name - }) - historyCache.add(contactId) - } + switch (item.syncType) { + case proto.HistorySync.HistorySyncHistorySyncType.INITIAL_BOOTSTRAP: + case proto.HistorySync.HistorySyncHistorySyncType.RECENT: + for(const chat of item.conversations) { + const contactId = `c:${chat.id}` + if(chat.name && !historyCache.has(contactId)) { + contacts.push({ + id: chat.id, + name: chat.name + }) + historyCache.add(contactId) + } - for(const { message } of chat.messages || []) { - const uqId = `${message?.key.remoteJid}:${message.key.id}` - if(message && !historyCache.has(uqId)) { - messages.push(message) - historyCache.add(uqId) - } - } - - delete chat.messages - if(!historyCache.has(chat.id)) { - chats.push(chat) - historyCache.add(chat.id) + for(const { message } of chat.messages || []) { + const uqId = `${message?.key.remoteJid}:${message.key.id}` + if(message && !historyCache.has(uqId)) { + messages.push(message) + historyCache.add(uqId) } } - break - case proto.HistorySync.HistorySyncHistorySyncType.PUSH_NAME: - for(const c of item.pushnames) { - const contactId = `c:${c.id}` - if(historyCache.has(contactId)) { - contacts.push({ notify: c.pushname, id: c.id }) - historyCache.add(contactId) - } + + delete chat.messages + if(!historyCache.has(chat.id)) { + chats.push(chat) + historyCache.add(chat.id) } + } + break - case proto.HistorySync.HistorySyncHistorySyncType.INITIAL_STATUS_V3: - // TODO + case proto.HistorySync.HistorySyncHistorySyncType.PUSH_NAME: + for(const c of item.pushnames) { + const contactId = `c:${c.id}` + if(historyCache.has(contactId)) { + contacts.push({ notify: c.pushname, id: c.id }) + historyCache.add(contactId) + } + } + + break + case proto.HistorySync.HistorySyncHistorySyncType.INITIAL_STATUS_V3: + // TODO break } diff --git a/src/Utils/legacy-msgs.ts b/src/Utils/legacy-msgs.ts index 590b65d..3c874da 100644 --- a/src/Utils/legacy-msgs.ts +++ b/src/Utils/legacy-msgs.ts @@ -1,75 +1,82 @@ import { Boom } from '@hapi/boom' import { randomBytes } from 'crypto' -import { decodeBinaryNodeLegacy, jidNormalizedUser } from "../WABinary" -import { aesDecrypt, hmacSign, hkdf, Curve } from "./crypto" +import { AuthenticationCreds, Contact, CurveKeyPair, DisconnectReason, LegacyAuthenticationCreds, WATag } from '../Types' +import { decodeBinaryNodeLegacy, jidNormalizedUser } from '../WABinary' +import { aesDecrypt, Curve, hkdf, hmacSign } from './crypto' import { BufferJSON } from './generics' -import { DisconnectReason, WATag, LegacyAuthenticationCreds, AuthenticationCreds, CurveKeyPair, Contact } from "../Types" export const newLegacyAuthCreds = () => ({ - clientID: randomBytes(16).toString('base64') + clientID: randomBytes(16).toString('base64') }) as LegacyAuthenticationCreds export const decodeWAMessage = ( - message: Buffer | string, - auth: { macKey: Buffer, encKey: Buffer }, - fromMe: boolean=false + message: Buffer | string, + auth: { macKey: Buffer, encKey: Buffer }, + fromMe: boolean=false ) => { - 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 + 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 (message[commaIndex+1] === ',') commaIndex += 1 - let data = message.slice(commaIndex+1, message.length) + if(message[commaIndex+1] === ',') { + commaIndex += 1 + } + + let data = message.slice(commaIndex+1, message.length) - // get the message tag. - // If a query was done, the server will respond with the same message tag we sent the query with - const messageTag: string = message.slice(0, commaIndex).toString() - let json: any - let tags: WATag - if(data.length) { - const possiblyEnc = (data.length > 32 && data.length % 16 === 0) - if(typeof data === 'string' || !possiblyEnc) { - json = JSON.parse(data.toString()) // parse the JSON - } else { - try { - json = JSON.parse(data.toString()) - } catch { - const { macKey, encKey } = auth || {} - if (!macKey || !encKey) { - throw new Boom('recieved encrypted buffer when auth creds unavailable', { data: message, statusCode: DisconnectReason.badSession }) - } - /* + // get the message tag. + // If a query was done, the server will respond with the same message tag we sent the query with + const messageTag: string = message.slice(0, commaIndex).toString() + let json: any + let tags: WATag + if(data.length) { + const possiblyEnc = (data.length > 32 && data.length % 16 === 0) + if(typeof data === 'string' || !possiblyEnc) { + json = JSON.parse(data.toString()) // parse the JSON + } else { + try { + json = JSON.parse(data.toString()) + } catch{ + const { macKey, encKey } = auth || {} + if(!macKey || !encKey) { + 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. Such a message can only be decrypted if we're connected successfully to the servers & have encryption keys */ - if (fromMe) { - tags = [data[0], data[1]] - data = data.slice(2, data.length) - } + if(fromMe) { + tags = [data[0], data[1]] + data = data.slice(2, data.length) + } - const checksum = data.slice(0, 32) // the first 32 bytes of the buffer are the HMAC sign of the message - data = data.slice(32, data.length) // the actual message - const computedChecksum = hmacSign(data, macKey) // compute the sign of the message we recieved using our macKey + const checksum = data.slice(0, 32) // the first 32 bytes of the buffer are the HMAC sign of the message + data = data.slice(32, data.length) // the actual message + const computedChecksum = hmacSign(data, macKey) // compute the sign of the message we recieved using our macKey - if (checksum.equals(computedChecksum)) { - // the checksum the server sent, must match the one we computed for the message to be valid - const decrypted = aesDecrypt(data, encKey) // decrypt using AES - json = decodeBinaryNodeLegacy(decrypted, { index: 0 }) // decode the binary message into a JSON array - } else { - throw new Boom('Bad checksum', { - data: { - received: checksum.toString('hex'), - computed: computedChecksum.toString('hex'), - data: data.slice(0, 80).toString(), - tag: messageTag, - message: message.slice(0, 80).toString() - }, - statusCode: DisconnectReason.badSession - }) - } - } - } - } - return [messageTag, json, tags] as const + if(checksum.equals(computedChecksum)) { + // the checksum the server sent, must match the one we computed for the message to be valid + const decrypted = aesDecrypt(data, encKey) // decrypt using AES + json = decodeBinaryNodeLegacy(decrypted, { index: 0 }) // decode the binary message into a JSON array + } else { + throw new Boom('Bad checksum', { + data: { + received: checksum.toString('hex'), + computed: computedChecksum.toString('hex'), + data: data.slice(0, 80).toString(), + tag: messageTag, + message: message.slice(0, 80).toString() + }, + statusCode: DisconnectReason.badSession + }) + } + } + } + } + + return [messageTag, json, tags] as const } /** @@ -82,104 +89,110 @@ export const validateNewConnection = ( auth: LegacyAuthenticationCreds, curveKeys: CurveKeyPair ) => { - // set metadata: one's WhatsApp ID [cc][number]@s.whatsapp.net, name on WhatsApp, info about the phone - const onValidationSuccess = () => { + // set metadata: one's WhatsApp ID [cc][number]@s.whatsapp.net, name on WhatsApp, info about the phone + const onValidationSuccess = () => { const user: Contact = { - id: jidNormalizedUser(json.wid), - name: json.pushname - } + id: jidNormalizedUser(json.wid), + name: json.pushname + } 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 (json.clientToken && json.clientToken !== auth.clientToken) { + if(json.clientToken && json.clientToken !== auth.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 } } + return onValidationSuccess() - } - const secret = Buffer.from(json.secret, 'base64') - if (secret.length !== 144) { + } + + const secret = Buffer.from(json.secret, 'base64') + if(secret.length !== 144) { throw new Error ('incorrect secret length received: ' + secret.length) - } + } - // generate shared key from our private key & the secret shared by the server - const sharedKey = Curve.sharedKey(curveKeys.private, secret.slice(0, 32)) - // expand the key to 80 bytes using HKDF - const expandedKey = hkdf(sharedKey as Buffer, 80, { }) + // generate shared key from our private key & the secret shared by the server + const sharedKey = Curve.sharedKey(curveKeys.private, secret.slice(0, 32)) + // expand the key to 80 bytes using HKDF + const expandedKey = hkdf(sharedKey as Buffer, 80, { }) - // perform HMAC validation. - const hmacValidationKey = expandedKey.slice(32, 64) - const hmacValidationMessage = Buffer.concat([secret.slice(0, 32), secret.slice(64, secret.length)]) + // perform HMAC validation. + const hmacValidationKey = expandedKey.slice(32, 64) + const hmacValidationMessage = Buffer.concat([secret.slice(0, 32), secret.slice(64, secret.length)]) - const hmac = hmacSign(hmacValidationMessage, hmacValidationKey) + const hmac = hmacSign(hmacValidationMessage, hmacValidationKey) - if (!hmac.equals(secret.slice(32, 64))) { + if(!hmac.equals(secret.slice(32, 64))) { // if the checksums didn't match throw new Boom('HMAC validation failed', { statusCode: 400 }) - } + } - // computed HMAC should equal secret[32:64] - // expandedKey[64:] + secret[64:] are the keys, encrypted using AES, that are used to encrypt/decrypt the messages recieved from WhatsApp - // they are encrypted using key: expandedKey[0:32] - const encryptedAESKeys = Buffer.concat([ + // computed HMAC should equal secret[32:64] + // expandedKey[64:] + secret[64:] are the keys, encrypted using AES, that are used to encrypt/decrypt the messages recieved from WhatsApp + // they are encrypted using key: expandedKey[0:32] + const encryptedAESKeys = Buffer.concat([ expandedKey.slice(64, expandedKey.length), secret.slice(64, secret.length), - ]) - const decryptedKeys = aesDecrypt(encryptedAESKeys, expandedKey.slice(0, 32)) - // set the credentials - auth = { + ]) + const decryptedKeys = aesDecrypt(encryptedAESKeys, expandedKey.slice(0, 32)) + // set the credentials + auth = { encKey: decryptedKeys.slice(0, 32), // first 32 bytes form the key to encrypt/decrypt messages macKey: decryptedKeys.slice(32, 64), // last 32 bytes from the key to sign messages clientToken: json.clientToken, serverToken: json.serverToken, clientID: auth.clientID, - } - return onValidationSuccess() + } + return onValidationSuccess() } export const computeChallengeResponse = (challenge: string, auth: LegacyAuthenticationCreds) => { const bytes = Buffer.from(challenge, 'base64') // decode the base64 encoded challenge string const signed = hmacSign(bytes, auth.macKey).toString('base64') // sign the challenge string with our macKey - return['admin', 'challenge', signed, auth.serverToken, auth.clientID] // prepare to send this signed string with the serverToken & clientID + return ['admin', 'challenge', signed, auth.serverToken, auth.clientID] // prepare to send this signed string with the serverToken & clientID } export const useSingleFileLegacyAuthState = (file: string) => { - // require fs here so that in case "fs" is not available -- the app does not crash + // require fs here so that in case "fs" is not available -- the app does not crash const { readFileSync, writeFileSync, existsSync } = require('fs') - let state: LegacyAuthenticationCreds + let state: LegacyAuthenticationCreds - if(existsSync(file)) { - state = JSON.parse( - readFileSync(file, { encoding: 'utf-8' }), - BufferJSON.reviver - ) - if(typeof state.encKey === 'string') { - state.encKey = Buffer.from(state.encKey, 'base64') - } - if(typeof state.macKey === 'string') { - state.macKey = Buffer.from(state.macKey, 'base64') - } - } else { - state = newLegacyAuthCreds() - } + if(existsSync(file)) { + state = JSON.parse( + readFileSync(file, { encoding: 'utf-8' }), + BufferJSON.reviver + ) + if(typeof state.encKey === 'string') { + state.encKey = Buffer.from(state.encKey, 'base64') + } - return { - state, - saveState: () => { - const str = JSON.stringify(state, BufferJSON.replacer, 2) - writeFileSync(file, str) - } - } + if(typeof state.macKey === 'string') { + state.macKey = Buffer.from(state.macKey, 'base64') + } + } else { + state = newLegacyAuthCreds() + } + + return { + state, + saveState: () => { + const str = JSON.stringify(state, BufferJSON.replacer, 2) + writeFileSync(file, str) + } + } } export const getAuthenticationCredsType = (creds: LegacyAuthenticationCreds | AuthenticationCreds) => { - if('clientID' in creds && !!creds.clientID) { - return 'legacy' - } - if('noiseKey' in creds && !!creds.noiseKey) { - return 'md' - } + if('clientID' in creds && !!creds.clientID) { + return 'legacy' + } + + if('noiseKey' in creds && !!creds.noiseKey) { + return 'md' + } } \ No newline at end of file diff --git a/src/Utils/lt-hash.ts b/src/Utils/lt-hash.ts index 0eeaaa4..bc9b057 100644 --- a/src/Utils/lt-hash.ts +++ b/src/Utils/lt-hash.ts @@ -6,7 +6,7 @@ import { hkdf } from './crypto' * if the same series of mutations was made sequentially. */ -const o = 128; +const o = 128 class d { @@ -16,41 +16,45 @@ class d { this.salt = e } add(e, t) { - var r = this; + var r = this for(const item of t) { e = r._addSingle(e, item) } + return e } subtract(e, t) { - var r = this; + var r = this for(const item of t) { e = r._subtractSingle(e, item) } + return e } subtractThenAdd(e, t, r) { - var n = this; + var n = this return n.add(n.subtract(e, r), t) } _addSingle(e, t) { - var r = this; - const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer; - return r.performPointwiseWithOverflow(e, n, ((e,t)=>e + t)) + var r = this + const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer + return r.performPointwiseWithOverflow(e, n, ((e, t) => e + t)) } _subtractSingle(e, t) { - var r = this; + var r = this - const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer; - return r.performPointwiseWithOverflow(e, n, ((e,t)=>e - t)) + const n = new Uint8Array(hkdf(Buffer.from(t), o, { info: r.salt })).buffer + return r.performPointwiseWithOverflow(e, n, ((e, t) => e - t)) } performPointwiseWithOverflow(e, t, r) { const n = new DataView(e) , i = new DataView(t) , a = new ArrayBuffer(n.byteLength) - , s = new DataView(a); - for (let e = 0; e < n.byteLength; e += 2) - s.setUint16(e, r(n.getUint16(e, !0), i.getUint16(e, !0)), !0); + , s = new DataView(a) + for(let e = 0; e < n.byteLength; e += 2) { + s.setUint16(e, r(n.getUint16(e, !0), i.getUint16(e, !0)), !0) + } + return a } } diff --git a/src/Utils/make-mutex.ts b/src/Utils/make-mutex.ts index 76f4148..8903b26 100644 --- a/src/Utils/make-mutex.ts +++ b/src/Utils/make-mutex.ts @@ -3,17 +3,20 @@ export default () => { let task = Promise.resolve() as Promise return { mutex(code: () => Promise):Promise { - task = (async () => { + task = (async() => { // wait for the previous task to complete // 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 return code() - })() - // we replace the existing task, appending the new piece of execution to it - // so the next task will have to wait for this one to finish - return task + })() + // we replace the existing task, appending the new piece of execution to it + // so the next task will have to wait for this one to finish + return task }, } - } +} \ No newline at end of file diff --git a/src/Utils/messages-media.ts b/src/Utils/messages-media.ts index ab4a0c6..fab4930 100644 --- a/src/Utils/messages-media.ts +++ b/src/Utils/messages-media.ts @@ -1,303 +1,341 @@ -import type { Logger } from 'pino' -import type { IAudioMetadata } from 'music-metadata' 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 { 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 getImageProcessingLibrary = async() => { - const [jimp, sharp] = await Promise.all([ - (async() => { - const jimp = await ( - import('jimp') - .catch(() => { }) - ) - return jimp - })(), - (async() => { - const sharp = await ( - import('sharp') - .catch(() => { }) - ) - return sharp - })() - ]) - if(sharp) return { sharp } - if(jimp) return { jimp } + const [jimp, sharp] = await Promise.all([ + (async() => { + const jimp = await ( + import('jimp') + .catch(() => { }) + ) + return jimp + })(), + (async() => { + const sharp = await ( + import('sharp') + .catch(() => { }) + ) + return sharp + })() + ]) + if(sharp) { + return { sharp } + } - throw new Boom('No image processing library available') + if(jimp) { + return { jimp } + } + + throw new Boom('No image processing library available') } export const hkdfInfoKey = (type: MediaType) => { - let str: string = type - if(type === 'sticker') str = 'image' - if(type === 'md-app-state') str = 'App State' + let str: string = type + if(type === 'sticker') { + str = 'image' + } + + if(type === 'md-app-state') { + str = 'App State' + } - let hkdfInfo = str[0].toUpperCase() + str.slice(1) + const hkdfInfo = str[0].toUpperCase() + str.slice(1) return `WhatsApp ${hkdfInfo} Keys` } + /** generates all the keys required to encrypt/decrypt & sign a media message */ export function getMediaKeys(buffer, mediaType: MediaType) { - if (typeof buffer === 'string') { - buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64') - } - // expand using HKDF to 112 bytes, also pass in the relevant app info - const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) }) - return { - iv: expandedMediaKey.slice(0, 16), - cipherKey: expandedMediaKey.slice(16, 48), - macKey: expandedMediaKey.slice(48, 80), - } + if(typeof buffer === 'string') { + buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64') + } + + // expand using HKDF to 112 bytes, also pass in the relevant app info + const expandedMediaKey = hkdf(buffer, 112, { info: hkdfInfoKey(mediaType) }) + return { + iv: expandedMediaKey.slice(0, 16), + cipherKey: expandedMediaKey.slice(16, 48), + macKey: expandedMediaKey.slice(48, 80), + } } + /** Extracts video thumb using FFMPEG */ -const extractVideoThumb = async ( - path: string, - destPath: string, - time: string, - size: { width: number; height: number }, -) => - new Promise((resolve, reject) => { - const cmd = `ffmpeg -ss ${time} -i ${path} -y -s ${size.width}x${size.height} -vframes 1 -f image2 ${destPath}` - exec(cmd, (err) => { - if (err) reject(err) - else resolve() - }) - }) as Promise +const extractVideoThumb = async( + path: string, + destPath: string, + time: string, + size: { width: number; height: number }, +) => new Promise((resolve, reject) => { + const cmd = `ffmpeg -ss ${time} -i ${path} -y -s ${size.width}x${size.height} -vframes 1 -f image2 ${destPath}` + exec(cmd, (err) => { + if(err) { + reject(err) + } else { + resolve() + } + }) +}) as Promise -export const extractImageThumb = async (bufferOrFilePath: Readable | Buffer | string) => { - if(bufferOrFilePath instanceof Readable) { - bufferOrFilePath = await toBuffer(bufferOrFilePath) - } - const lib = await getImageProcessingLibrary() - if('sharp' in lib) { - const result = await lib.sharp!.default(bufferOrFilePath) - .resize(32, 32) - .jpeg({ quality: 50 }) - .toBuffer() - return result - } else { - const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp +export const extractImageThumb = async(bufferOrFilePath: Readable | Buffer | string) => { + if(bufferOrFilePath instanceof Readable) { + bufferOrFilePath = await toBuffer(bufferOrFilePath) + } - const jimp = await read(bufferOrFilePath as any) - const result = await jimp - .quality(50) - .resize(32, 32, RESIZE_BILINEAR) - .getBufferAsync(MIME_JPEG) - return result - } + const lib = await getImageProcessingLibrary() + if('sharp' in lib) { + const result = await lib.sharp!.default(bufferOrFilePath) + .resize(32, 32) + .jpeg({ quality: 50 }) + .toBuffer() + return result + } else { + const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp + + const jimp = await read(bufferOrFilePath as any) + const result = await jimp + .quality(50) + .resize(32, 32, RESIZE_BILINEAR) + .getBufferAsync(MIME_JPEG) + return result + } } -export const generateProfilePicture = async (mediaUpload: WAMediaUpload) => { - let bufferOrFilePath: Buffer | string - if(Buffer.isBuffer(mediaUpload)) { - bufferOrFilePath = mediaUpload - } else if('url' in mediaUpload) { - bufferOrFilePath = mediaUpload.url.toString() - } else { - bufferOrFilePath = await toBuffer(mediaUpload.stream) - } - const lib = await getImageProcessingLibrary() - let img: Promise - if('sharp' in lib) { - img = lib.sharp!.default(bufferOrFilePath) - .resize(640, 640) - .jpeg({ - quality: 50, - }) - .toBuffer() - } else { - const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp - const jimp = await read(bufferOrFilePath as any) - const min = Math.min(jimp.getWidth(), jimp.getHeight()) - const cropped = jimp.crop(0, 0, min, min) +export const generateProfilePicture = async(mediaUpload: WAMediaUpload) => { + let bufferOrFilePath: Buffer | string + if(Buffer.isBuffer(mediaUpload)) { + bufferOrFilePath = mediaUpload + } else if('url' in mediaUpload) { + bufferOrFilePath = mediaUpload.url.toString() + } else { + bufferOrFilePath = await toBuffer(mediaUpload.stream) + } - img = cropped - .quality(50) - .resize(640, 640, RESIZE_BILINEAR) - .getBufferAsync(MIME_JPEG) - } + const lib = await getImageProcessingLibrary() + let img: Promise + if('sharp' in lib) { + img = lib.sharp!.default(bufferOrFilePath) + .resize(640, 640) + .jpeg({ + quality: 50, + }) + .toBuffer() + } else { + const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp + const jimp = await read(bufferOrFilePath as any) + const min = Math.min(jimp.getWidth(), jimp.getHeight()) + const cropped = jimp.crop(0, 0, min, min) + + img = cropped + .quality(50) + .resize(640, 640, RESIZE_BILINEAR) + .getBufferAsync(MIME_JPEG) + } - return { - img: await img, - } + return { + img: await img, + } } + /** gets the SHA256 of the given media message */ export const mediaMessageSHA256B64 = (message: WAMessageContent) => { - const media = Object.values(message)[0] as WAGenericMediaMessage - return media?.fileSha256 && Buffer.from(media.fileSha256).toString ('base64') + const media = Object.values(message)[0] as WAGenericMediaMessage + return media?.fileSha256 && Buffer.from(media.fileSha256).toString ('base64') } -export async function getAudioDuration (buffer: Buffer | string | Readable) { - const musicMetadata = await import('music-metadata') - let metadata: IAudioMetadata - if(Buffer.isBuffer(buffer)) { - metadata = await musicMetadata.parseBuffer(buffer, null, { duration: true }) - } else if(typeof buffer === 'string') { - const rStream = createReadStream(buffer) - metadata = await musicMetadata.parseStream(rStream, null, { duration: true }) - rStream.close() - } else { - metadata = await musicMetadata.parseStream(buffer, null, { duration: true }) - } - return metadata.format.duration; + +export async function getAudioDuration(buffer: Buffer | string | Readable) { + const musicMetadata = await import('music-metadata') + let metadata: IAudioMetadata + if(Buffer.isBuffer(buffer)) { + metadata = await musicMetadata.parseBuffer(buffer, null, { duration: true }) + } else if(typeof buffer === 'string') { + const rStream = createReadStream(buffer) + metadata = await musicMetadata.parseStream(rStream, null, { duration: true }) + rStream.close() + } else { + metadata = await musicMetadata.parseStream(buffer, null, { duration: true }) + } + + return metadata.format.duration } + export const toReadable = (buffer: Buffer) => { - const readable = new Readable({ read: () => {} }) - readable.push(buffer) - readable.push(null) - return readable + const readable = new Readable({ read: () => {} }) + readable.push(buffer) + readable.push(null) + return readable } + export const toBuffer = async(stream: Readable) => { - let buff = Buffer.alloc(0) - for await(const chunk of stream) { - buff = Buffer.concat([ buff, chunk ]) - } - return buff + let buff = Buffer.alloc(0) + for await (const chunk of stream) { + buff = Buffer.concat([ buff, chunk ]) + } + + return buff } -export const getStream = async (item: WAMediaUpload) => { - if(Buffer.isBuffer(item)) 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://')) { - return { stream: await getHttpStream(item.url), type: 'remote' } - } - return { stream: createReadStream(item.url), type: 'file' } + +export const getStream = async(item: WAMediaUpload) => { + if(Buffer.isBuffer(item)) { + 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://')) { + return { stream: await getHttpStream(item.url), type: 'remote' } + } + + return { stream: createReadStream(item.url), type: 'file' } } + /** generates a thumbnail for a given media, if required */ export async function generateThumbnail( - file: string, - mediaType: 'video' | 'image', - options: { + file: string, + mediaType: 'video' | 'image', + options: { logger?: Logger } ) { - let thumbnail: string - if(mediaType === 'image') { - const buff = await extractImageThumb(file) - thumbnail = buff.toString('base64') - } else if(mediaType === 'video') { - const imgFilename = join(getTmpFilesDirectory(), generateMessageID() + '.jpg') - try { - await extractVideoThumb(file, imgFilename, '00:00:00', { width: 32, height: 32 }) - const buff = await fs.readFile(imgFilename) - thumbnail = buff.toString('base64') + let thumbnail: string + if(mediaType === 'image') { + const buff = await extractImageThumb(file) + thumbnail = buff.toString('base64') + } else if(mediaType === 'video') { + const imgFilename = join(getTmpFilesDirectory(), generateMessageID() + '.jpg') + try { + await extractVideoThumb(file, imgFilename, '00:00:00', { width: 32, height: 32 }) + const buff = await fs.readFile(imgFilename) + thumbnail = buff.toString('base64') - await fs.unlink(imgFilename) - } catch (err) { - options.logger?.debug('could not generate video thumb: ' + err) - } - } + await fs.unlink(imgFilename) + } catch(err) { + options.logger?.debug('could not generate video thumb: ' + err) + } + } - return thumbnail + return thumbnail } + export const getHttpStream = async(url: string | URL, options: AxiosRequestConfig & { isStream?: true } = {}) => { - const { default: axios } = await import('axios') - const fetched = await axios.get(url.toString(), { ...options, responseType: 'stream' }) - return fetched.data as Readable -} + const { default: axios } = await import('axios') + const fetched = await axios.get(url.toString(), { ...options, responseType: 'stream' }) + return fetched.data as Readable +} + export const encryptedStream = async( - media: WAMediaUpload, - mediaType: MediaType, - saveOriginalFileIfRequired = true, - logger?: Logger + media: WAMediaUpload, + mediaType: MediaType, + saveOriginalFileIfRequired = true, + logger?: Logger ) => { - const { stream, type } = await getStream(media) + const { stream, type } = await getStream(media) - logger?.debug('fetched media stream') + logger?.debug('fetched media stream') - const mediaKey = Crypto.randomBytes(32) - const {cipherKey, iv, macKey} = getMediaKeys(mediaKey, mediaType) - // random name - //const encBodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageID() + '.enc') - // const encWriteStream = createWriteStream(encBodyPath) - const encWriteStream = new Readable({ read: () => {} }) + const mediaKey = Crypto.randomBytes(32) + const { cipherKey, iv, macKey } = getMediaKeys(mediaKey, mediaType) + // random name + //const encBodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageID() + '.enc') + // const encWriteStream = createWriteStream(encBodyPath) + const encWriteStream = new Readable({ read: () => {} }) - let bodyPath: string - let writeStream: WriteStream - let didSaveToTmpPath = false - if(type === 'file') { - bodyPath = (media as any).url - } else if(saveOriginalFileIfRequired) { - bodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageID()) - writeStream = createWriteStream(bodyPath) - didSaveToTmpPath = true - } + let bodyPath: string + let writeStream: WriteStream + let didSaveToTmpPath = false + if(type === 'file') { + bodyPath = (media as any).url + } else if(saveOriginalFileIfRequired) { + bodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageID()) + writeStream = createWriteStream(bodyPath) + didSaveToTmpPath = true + } - let fileLength = 0 - const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv) - let hmac = Crypto.createHmac('sha256', macKey).update(iv) - let sha256Plain = Crypto.createHash('sha256') - let sha256Enc = Crypto.createHash('sha256') + let fileLength = 0 + const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv) + let hmac = Crypto.createHmac('sha256', macKey).update(iv) + let sha256Plain = Crypto.createHash('sha256') + let sha256Enc = Crypto.createHash('sha256') - const onChunk = (buff: Buffer) => { - sha256Enc = sha256Enc.update(buff) - hmac = hmac.update(buff) - encWriteStream.push(buff) - } + const onChunk = (buff: Buffer) => { + sha256Enc = sha256Enc.update(buff) + hmac = hmac.update(buff) + encWriteStream.push(buff) + } - try { - for await(const data of stream) { - fileLength += data.length - sha256Plain = sha256Plain.update(data) - if(writeStream) { - if(!writeStream.write(data)) await once(writeStream, 'drain') - } - onChunk(aes.update(data)) - } - onChunk(aes.final()) + try { + for await (const data of stream) { + fileLength += data.length + sha256Plain = sha256Plain.update(data) + if(writeStream) { + if(!writeStream.write(data)) { + await once(writeStream, 'drain') + } + } + + onChunk(aes.update(data)) + } + + onChunk(aes.final()) - const mac = hmac.digest().slice(0, 10) - sha256Enc = sha256Enc.update(mac) + const mac = hmac.digest().slice(0, 10) + sha256Enc = sha256Enc.update(mac) - const fileSha256 = sha256Plain.digest() - const fileEncSha256 = sha256Enc.digest() + const fileSha256 = sha256Plain.digest() + const fileEncSha256 = sha256Enc.digest() - encWriteStream.push(mac) - encWriteStream.push(null) + encWriteStream.push(mac) + encWriteStream.push(null) - writeStream && writeStream.end() - stream.destroy() + writeStream && writeStream.end() + stream.destroy() - logger?.debug('encrypted data successfully') + logger?.debug('encrypted data successfully') - return { - mediaKey, - encWriteStream, - bodyPath, - mac, - fileEncSha256, - fileSha256, - fileLength, - didSaveToTmpPath - } - } catch(error) { - encWriteStream.destroy(error) - writeStream.destroy(error) - aes.destroy(error) - hmac.destroy(error) - sha256Plain.destroy(error) - sha256Enc.destroy(error) - stream.destroy(error) + return { + mediaKey, + encWriteStream, + bodyPath, + mac, + fileEncSha256, + fileSha256, + fileLength, + didSaveToTmpPath + } + } catch(error) { + encWriteStream.destroy(error) + writeStream.destroy(error) + aes.destroy(error) + hmac.destroy(error) + sha256Plain.destroy(error) + sha256Enc.destroy(error) + stream.destroy(error) - throw error - } + throw error + } } const DEF_HOST = 'mmg.whatsapp.net' const AES_CHUNK_SIZE = 16 const toSmallestChunkSize = (num: number) => { - return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE + return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE } type MediaDownloadOptions = { @@ -306,103 +344,106 @@ type MediaDownloadOptions = { } export const downloadContentFromMessage = async( - { mediaKey, directPath, url }: DownloadableMessage, - type: MediaType, - { startByte, endByte }: MediaDownloadOptions = { } + { mediaKey, directPath, url }: DownloadableMessage, + type: MediaType, + { startByte, endByte }: MediaDownloadOptions = { } ) => { - const downloadUrl = url || `https://${DEF_HOST}${directPath}` - let bytesFetched = 0 - let startChunk = 0 - let firstBlockIsIV = false - // if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV - if(startByte) { - const chunk = toSmallestChunkSize(startByte || 0) - if(chunk) { - startChunk = chunk-AES_CHUNK_SIZE - bytesFetched = chunk + const downloadUrl = url || `https://${DEF_HOST}${directPath}` + let bytesFetched = 0 + let startChunk = 0 + let firstBlockIsIV = false + // if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV + if(startByte) { + const chunk = toSmallestChunkSize(startByte || 0) + if(chunk) { + startChunk = chunk-AES_CHUNK_SIZE + bytesFetched = chunk - firstBlockIsIV = true - } - } - const endChunk = endByte ? toSmallestChunkSize(endByte || 0)+AES_CHUNK_SIZE : undefined + firstBlockIsIV = true + } + } - const headers: { [_: string]: string } = { - Origin: DEFAULT_ORIGIN, - } - if(startChunk || endChunk) { - headers.Range = `bytes=${startChunk}-` - if(endChunk) headers.Range += endChunk - } + const endChunk = endByte ? toSmallestChunkSize(endByte || 0)+AES_CHUNK_SIZE : undefined - // download the message - const fetched = await getHttpStream( - downloadUrl, - { - headers, - maxBodyLength: Infinity, - maxContentLength: Infinity, - } - ) + const headers: { [_: string]: string } = { + Origin: DEFAULT_ORIGIN, + } + if(startChunk || endChunk) { + headers.Range = `bytes=${startChunk}-` + if(endChunk) { + headers.Range += endChunk + } + } - let remainingBytes = Buffer.from([]) - const { cipherKey, iv } = getMediaKeys(mediaKey, type) + // download the message + const fetched = await getHttpStream( + downloadUrl, + { + headers, + maxBodyLength: Infinity, + maxContentLength: Infinity, + } + ) - let aes: Crypto.Decipher + let remainingBytes = Buffer.from([]) + const { cipherKey, iv } = getMediaKeys(mediaKey, type) - const pushBytes = (bytes: Buffer, push: (bytes: Buffer) => void) => { - if(startByte || endByte) { - const start = bytesFetched >= startByte ? undefined : Math.max(startByte-bytesFetched, 0) - const end = bytesFetched+bytes.length < endByte ? undefined : Math.max(endByte-bytesFetched, 0) + let aes: Crypto.Decipher + + const pushBytes = (bytes: Buffer, push: (bytes: Buffer) => void) => { + if(startByte || endByte) { + const start = bytesFetched >= startByte ? undefined : Math.max(startByte-bytesFetched, 0) + const end = bytesFetched+bytes.length < endByte ? undefined : Math.max(endByte-bytesFetched, 0) - push(bytes.slice(start, end)) + push(bytes.slice(start, end)) - bytesFetched += bytes.length - } else { - push(bytes) - } - } + bytesFetched += bytes.length + } else { + push(bytes) + } + } - const output = new Transform({ - transform(chunk, _, callback) { - let data = Buffer.concat([remainingBytes, chunk]) + const output = new Transform({ + transform(chunk, _, callback) { + let data = Buffer.concat([remainingBytes, chunk]) - const decryptLength = toSmallestChunkSize(data.length) - remainingBytes = data.slice(decryptLength) - data = data.slice(0, decryptLength) + const decryptLength = toSmallestChunkSize(data.length) + remainingBytes = data.slice(decryptLength) + data = data.slice(0, decryptLength) - if(!aes) { - let ivValue = iv - if(firstBlockIsIV) { - ivValue = data.slice(0, AES_CHUNK_SIZE) - data = data.slice(AES_CHUNK_SIZE) - } + if(!aes) { + let ivValue = iv + if(firstBlockIsIV) { + ivValue = data.slice(0, AES_CHUNK_SIZE) + data = data.slice(AES_CHUNK_SIZE) + } - aes = Crypto.createDecipheriv("aes-256-cbc", cipherKey, ivValue) - // if an end byte that is not EOF is specified - // stop auto padding (PKCS7) -- otherwise throws an error for decryption - if(endByte) { - aes.setAutoPadding(false) - } + aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue) + // if an end byte that is not EOF is specified + // stop auto padding (PKCS7) -- otherwise throws an error for decryption + if(endByte) { + aes.setAutoPadding(false) + } - } + } - try { - pushBytes(aes.update(data), b => this.push(b)) - callback() - } catch(error) { - callback(error) - } - }, - final(callback) { - try { - pushBytes(aes.final(), b => this.push(b)) - callback() - } catch(error) { - callback(error) - } - }, - }) - return fetched.pipe(output, { end: true }) + try { + pushBytes(aes.update(data), b => this.push(b)) + callback() + } catch(error) { + callback(error) + } + }, + final(callback) { + try { + pushBytes(aes.final(), b => this.push(b)) + callback() + } catch(error) { + callback(error) + } + }, + }) + return fetched.pipe(output, { end: true }) } /** @@ -410,121 +451,130 @@ export const downloadContentFromMessage = async( * @param message the media message you want to decode */ export async function decryptMediaMessageBuffer(message: WAMessageContent): Promise { - /* + /* One can infer media type from the key in the message it is usually written as [mediaType]Message. Eg. imageMessage, audioMessage etc. */ - const type = Object.keys(message)[0] as MessageType - if( + const type = Object.keys(message)[0] as MessageType + if( !type || type === 'conversation' || type === 'extendedTextMessage' ) { - throw new Boom(`no media message for "${type}"`, { statusCode: 400 }) - } - if (type === 'locationMessage' || type === 'liveLocationMessage') { - const buffer = Buffer.from(message[type].jpegThumbnail) - const readable = new Readable({ read: () => {} }) - readable.push(buffer) - readable.push(null) - return readable - } - let messageContent: WAGenericMediaMessage - if (message.productMessage) { - const product = message.productMessage.product?.productImage - if (!product) throw new Boom('product has no image', { statusCode: 400 }) - messageContent = product - } else { - messageContent = message[type] - } - return downloadContentFromMessage(messageContent, type.replace('Message', '') as MediaType) + throw new Boom(`no media message for "${type}"`, { statusCode: 400 }) + } + + if(type === 'locationMessage' || type === 'liveLocationMessage') { + const buffer = Buffer.from(message[type].jpegThumbnail) + const readable = new Readable({ read: () => {} }) + readable.push(buffer) + readable.push(null) + return readable + } + + let messageContent: WAGenericMediaMessage + if(message.productMessage) { + const product = message.productMessage.product?.productImage + if(!product) { + throw new Boom('product has no image', { statusCode: 400 }) + } + + messageContent = product + } else { + messageContent = message[type] + } + + return downloadContentFromMessage(messageContent, type.replace('Message', '') as MediaType) } + export function extensionForMediaMessage(message: WAMessageContent) { - const getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1] - const type = Object.keys(message)[0] as MessageType - let extension: string - if( + const getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1] + const type = Object.keys(message)[0] as MessageType + let extension: string + if( type === 'locationMessage' || type === 'liveLocationMessage' || type === 'productMessage' ) { - extension = '.jpeg' - } else { - const messageContent = message[type] as + extension = '.jpeg' + } else { + const messageContent = message[type] as | WAProto.VideoMessage | WAProto.ImageMessage | WAProto.AudioMessage | WAProto.DocumentMessage - extension = getExtension (messageContent.mimetype) - } - return extension + extension = getExtension (messageContent.mimetype) + } + + return extension } export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger }: CommonSocketConfig, refreshMediaConn: (force: boolean) => Promise): WAMediaUploadFunction => { - return async(stream, { mediaType, fileEncSha256B64, timeoutMs }) => { + return async(stream, { mediaType, fileEncSha256B64, timeoutMs }) => { const { default: axios } = await import('axios') - // send a query JSON to obtain the url & auth token to upload our media + // send a query JSON to obtain the url & auth token to upload our media let uploadInfo = await refreshMediaConn(false) let urls: { mediaUrl: string, directPath: string } - const hosts = [ ...customUploadHosts, ...uploadInfo.hosts ] + const hosts = [ ...customUploadHosts, ...uploadInfo.hosts ] - let chunks: Buffer[] = [] - for await(const chunk of stream) { - chunks.push(chunk) - } + const chunks: Buffer[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } - let reqBody = Buffer.concat(chunks) + let reqBody = Buffer.concat(chunks) - for (let { hostname, maxContentLengthBytes } of hosts) { - logger.debug(`uploading to "${hostname}"`) + for(const { hostname, maxContentLengthBytes } of hosts) { + logger.debug(`uploading to "${hostname}"`) const auth = encodeURIComponent(uploadInfo.auth) // the auth token const url = `https://${hostname}${MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}` let result: any try { - if(maxContentLengthBytes && reqBody.length > maxContentLengthBytes) { - throw new Boom(`Body too large for "${hostname}"`, { statusCode: 413 }) - } + if(maxContentLengthBytes && reqBody.length > maxContentLengthBytes) { + throw new Boom(`Body too large for "${hostname}"`, { statusCode: 413 }) + } const body = await axios.post( - url, - reqBody, + url, + reqBody, { headers: { 'Content-Type': 'application/octet-stream', 'Origin': DEFAULT_ORIGIN }, httpsAgent: fetchAgent, - timeout: timeoutMs, - responseType: 'json', - maxBodyLength: Infinity, - maxContentLength: Infinity, + timeout: timeoutMs, + responseType: 'json', + maxBodyLength: Infinity, + maxContentLength: Infinity, } ) - result = body.data + result = body.data if(result?.url || result?.directPath) { - urls = { - mediaUrl: result.url, - directPath: result.direct_path - } - break - } else { + urls = { + mediaUrl: result.url, + directPath: result.direct_path + } + break + } else { uploadInfo = await refreshMediaConn(true) throw new Error(`upload failed, reason: ${JSON.stringify(result)}`) } - } catch (error) { - if(axios.isAxiosError(error)) { - result = error.response?.data - } + } catch(error) { + if(axios.isAxiosError(error)) { + result = error.response?.data + } const isLast = hostname === hosts[uploadInfo.hosts.length-1]?.hostname 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 - reqBody = undefined + + // clear buffer just to be sure we're releasing the memory + reqBody = undefined if(!urls) { throw new Boom( diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index 31655f0..9b4cc0a 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -1,11 +1,12 @@ import { Boom } from '@hapi/boom' -import { promises as fs } from "fs" +import { promises as fs } from 'fs' 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 { AnyMediaMessageContent, AnyMessageContent, MediaGenerationOptions, + MediaType, MessageContentGenerationOptions, MessageGenerationOptions, MessageGenerationOptionsFromContent, @@ -13,13 +14,11 @@ import { WAMediaUpload, WAMessage, WAMessageContent, + WAMessageStatus, WAProto, - WATextMessage, - MediaType, - WAMessageStatus -} from "../Types" -import { generateMessageID, unixTimestampSeconds } from "./generics" -import { encryptedStream, generateThumbnail, getAudioDuration } from "./messages-media" + WATextMessage } from '../Types' +import { generateMessageID, unixTimestampSeconds } from './generics' +import { encryptedStream, generateThumbnail, getAudioDuration } from './messages-media' type MediaUploadData = { media: WAMediaUpload @@ -33,20 +32,20 @@ type MediaUploadData = { } const MIMETYPE_MAP: { [T in MediaType]: string } = { - image: 'image/jpeg', - video: 'video/mp4', - document: 'application/pdf', - audio: 'audio/ogg; codecs=opus', - sticker: 'image/webp', + image: 'image/jpeg', + video: 'video/mp4', + document: 'application/pdf', + audio: 'audio/ogg; codecs=opus', + sticker: 'image/webp', history: 'application/x-protobuf', - "md-app-state": 'application/x-protobuf', + 'md-app-state': 'application/x-protobuf', } const MessageTypeProto = { - 'image': WAProto.ImageMessage, - 'video': WAProto.VideoMessage, - 'audio': WAProto.AudioMessage, - 'sticker': WAProto.StickerMessage, + 'image': WAProto.ImageMessage, + 'video': WAProto.VideoMessage, + 'audio': WAProto.AudioMessage, + 'sticker': WAProto.StickerMessage, 'document': WAProto.DocumentMessage, } as const @@ -64,6 +63,7 @@ export const prepareWAMessageMedia = async( mediaType = key } } + const uploadData: MediaUploadData = { ...message, media: message[mediaType] @@ -74,13 +74,14 @@ export const prepareWAMessageMedia = async( ('url' in uploadData.media) && !!uploadData.media.url && !!options.mediaCache && ( - // generate the key - mediaType + ':' + uploadData.media.url!.toString() - ) + // generate the key + mediaType + ':' + uploadData.media.url!.toString() + ) if(mediaType === 'document' && !uploadData.fileName) { uploadData.fileName = 'file' } + if(!uploadData.mimetype) { uploadData.mimetype = MIMETYPE_MAP[mediaType] } @@ -89,7 +90,7 @@ export const prepareWAMessageMedia = async( if(cacheableKey) { const mediaBuff: Buffer = options.mediaCache!.get(cacheableKey) if(mediaBuff) { - logger?.debug({ cacheableKey }, `got media cache hit`) + logger?.debug({ cacheableKey }, 'got media cache hit') const obj = WAProto.Message.decode(mediaBuff) const key = `${mediaType}Message` @@ -117,9 +118,9 @@ export const prepareWAMessageMedia = async( // url safe Base64 encode the SHA256 hash of the body const fileEncSha256B64 = encodeURIComponent( fileEncSha256.toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/\=+$/, '') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/\=+$/, '') ) const [{ mediaUrl, directPath }] = await Promise.all([ @@ -128,34 +129,35 @@ export const prepareWAMessageMedia = async( encWriteStream, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs } ) - logger?.debug(`uploaded media`) + logger?.debug('uploaded media') return result })(), (async() => { try { if(requiresThumbnailComputation) { 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) - logger?.debug(`computed audio duration`) + logger?.debug('computed audio duration') } - } catch (error) { + } catch(error) { logger?.warn({ trace: error.stack }, 'failed to obtain extra info') } })(), ]) - .finally( - async() => { - encWriteStream.destroy() - // remove tmp files - if(didSaveToTmpPath && bodyPath) { - await fs.unlink(bodyPath) - logger?.debug('removed tmp files') + .finally( + async() => { + encWriteStream.destroy() + // remove tmp files + if(didSaveToTmpPath && bodyPath) { + await fs.unlink(bodyPath) + logger?.debug('removed tmp files') + } } - } - ) + ) delete uploadData.media @@ -175,12 +177,13 @@ export const prepareWAMessageMedia = async( }) if(cacheableKey) { - logger.debug({ cacheableKey }, `set cache`) + logger.debug({ cacheableKey }, 'set cache') options.mediaCache!.set(cacheableKey, WAProto.Message.encode(obj).finish()) } return obj } + export const prepareDisappearingMessageSettingContent = (ephemeralExpiration?: number) => { ephemeralExpiration = ephemeralExpiration || 0 const content: WAMessageContent = { @@ -195,6 +198,7 @@ export const prepareDisappearingMessageSettingContent = (ephemeralExpiration?: n } return WAProto.Message.fromObject(content) } + /** * Generate forwarded message content like WA does * @param message the message to forward @@ -205,7 +209,10 @@ export const generateForwardMessageContent = ( forceForward?: boolean ) => { 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 content = proto.Message.decode(proto.Message.encode(message.message).finish()) @@ -213,17 +220,22 @@ export const generateForwardMessageContent = ( let score = content[key].contextInfo?.forwardingScore || 0 score += message.key.fromMe && !forceForward ? 0 : 1 - if (key === 'conversation') { + if(key === 'conversation') { content.extendedTextMessage = { text: content[key] } delete content.conversation 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 } + export const generateWAMessageContent = async( message: AnyMessageContent, options: MessageContentGenerationOptions @@ -231,7 +243,7 @@ export const generateWAMessageContent = async( let m: WAMessageContent = {} if('text' in message) { const extContent = { ...message } as WATextMessage - if (!!options.getUrlInfo && message.text.match(URL_REGEX)) { + if(!!options.getUrlInfo && message.text.match(URL_REGEX)) { try { const data = await options.getUrlInfo(message.text) extContent.canonicalUrl = data['canonical-url'] @@ -240,16 +252,18 @@ export const generateWAMessageContent = async( extContent.description = data.description extContent.title = data.title extContent.previewType = 0 - } catch (error) { // ignore if fails + } catch(error) { // ignore if fails options.logger?.warn({ trace: error.stack }, 'url generation failed') } } + m.extendedTextMessage = extContent } else if('contacts' in message) { const contactLen = message.contacts.contacts.length if(!contactLen) { throw new Boom('require atleast 1 contact', { statusCode: 400 }) - } + } + if(contactLen === 1) { m.contactMessage = WAProto.ContactMessage.fromObject(message.contacts.contacts[0]) } else { @@ -269,8 +283,8 @@ export const generateWAMessageContent = async( ) } else if('disappearingMessagesInChat' in message) { const exp = typeof message.disappearingMessagesInChat === 'boolean' ? - (message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : - message.disappearingMessagesInChat + (message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : + message.disappearingMessagesInChat m = prepareDisappearingMessageSettingContent(exp) } else { m = await prepareWAMessageMedia( @@ -278,6 +292,7 @@ export const generateWAMessageContent = async( options ) } + if('buttons' in message && !!message.buttons) { const buttonsMessage: proto.IButtonsMessage = { buttons: message.buttons!.map(b => ({ ...b, type: proto.Button.ButtonType.RESPONSE })) @@ -289,13 +304,14 @@ export const generateWAMessageContent = async( if('caption' in message) { buttonsMessage.contentText = message.caption } + const type = Object.keys(m)[0].replace('Message', '').toUpperCase() buttonsMessage.headerType = ButtonType[type] Object.assign(buttonsMessage, m) } - if ('footer' in message && !!message.footer) { + if('footer' in message && !!message.footer) { buttonsMessage.footerText = message.footer } @@ -325,7 +341,7 @@ export const generateWAMessageContent = async( m = { templateMessage } } - if ('sections' in message && !!message.sections) { + if('sections' in message && !!message.sections) { const listMessage: proto.IListMessage = { sections: message.sections, buttonText: message.buttonText, @@ -341,19 +357,24 @@ export const generateWAMessageContent = async( if('viewOnce' in message && !!message.viewOnce) { m = { viewOnceMessage: { message: m } } } + if('mentions' in message && message.mentions?.length) { const [messageType] = Object.keys(m) m[messageType].contextInfo = m[messageType] || { } m[messageType].contextInfo.mentionedJid = message.mentions } + return WAProto.Message.fromObject(m) } + export const generateWAMessageFromContent = ( jid: string, message: WAMessageContent, 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 timestamp = unixTimestampSeconds(options.timestamp) @@ -373,6 +394,7 @@ export const generateWAMessageFromContent = ( message[key].contextInfo.remoteJid = quoted.key.remoteJid } } + if( // if we want to send a disappearing message !!options?.ephemeralExpiration && @@ -409,6 +431,7 @@ export const generateWAMessageFromContent = ( } return WAProto.WebMessageInfo.fromObject(messageJSON) } + export const generateWAMessage = async( jid: string, content: AnyMessageContent, @@ -434,6 +457,7 @@ export const getContentType = (content: WAProto.IMessage | undefined) => { return key as keyof typeof content } } + /** * Extract the true message content from a message * Eg. extracts the inner message from a disappearing message/view once message @@ -447,17 +471,18 @@ export const extractMessageContent = (content: WAMessageContent | undefined | nu if(content?.buttonsMessage) { const { buttonsMessage } = content if(buttonsMessage.imageMessage) { - return { imageMessage: buttonsMessage.imageMessage } + return { imageMessage: buttonsMessage.imageMessage } } else if(buttonsMessage.documentMessage) { - return { documentMessage: buttonsMessage.documentMessage } + return { documentMessage: buttonsMessage.documentMessage } } else if(buttonsMessage.videoMessage) { - return { videoMessage: buttonsMessage.videoMessage } + return { videoMessage: buttonsMessage.videoMessage } } else if(buttonsMessage.locationMessage) { - return { locationMessage: buttonsMessage.locationMessage } + return { locationMessage: buttonsMessage.locationMessage } } else { - return { conversation: buttonsMessage.contentText } + return { conversation: buttonsMessage.contentText } } } + return content } @@ -465,6 +490,6 @@ export const extractMessageContent = (content: WAMessageContent | undefined | nu * Returns the device predicted by message ID */ export const getDevice = (id: string) => { - const deviceType = id.length > 21 ? 'android' : id.substring(0, 2) == '3A' ? 'ios' : 'web' - return deviceType + const deviceType = id.length > 21 ? 'android' : id.substring(0, 2) === '3A' ? 'ios' : 'web' + return deviceType } diff --git a/src/Utils/noise-handler.ts b/src/Utils/noise-handler.ts index 28039cf..def4be1 100644 --- a/src/Utils/noise-handler.ts +++ b/src/Utils/noise-handler.ts @@ -1,15 +1,15 @@ -import { sha256, Curve, hkdf } from "./crypto"; -import { Binary } from "../WABinary"; -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 { Boom } from '@hapi/boom' +import { createCipheriv, createDecipheriv } from 'crypto' 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 iv = new ArrayBuffer(12); - new DataView(iv).setUint32(8, counter); + const iv = new ArrayBuffer(12) + new DataView(iv).setUint32(8, counter) return new Uint8Array(iv) } @@ -18,9 +18,10 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key const authenticate = (data: Uint8Array) => { if(!isFinished) { - hash = sha256( Buffer.from(Binary.build(hash, data).readByteArray()) ) + hash = sha256(Buffer.from(Binary.build(hash, data).readByteArray())) } } + const encrypt = (plaintext: Uint8Array) => { const authTagLength = 128 >> 3 const cipher = createCipheriv('aes-256-gcm', encKey, generateIV(writeCounter), { authTagLength }) @@ -33,6 +34,7 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key authenticate(result) return result } + const decrypt = (ciphertext: Uint8Array) => { // before the handshake is finished, we use the same counter // 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()]) - if(isFinished) readCounter += 1 - else writeCounter += 1 + if(isFinished) { + readCounter += 1 + } else { + writeCounter += 1 + } authenticate(ciphertext) return result } + const localHKDF = (data: Uint8Array) => { const key = hkdf(Buffer.from(data), 64, { salt, info: '' }) return [key.slice(0, 32), key.slice(32)] } + const mixIntoKey = (data: Uint8Array) => { const [write, read] = localHKDF(data) salt = write @@ -66,15 +73,16 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key readCounter = 0 writeCounter = 0 } + const finishInit = () => { - const [write, read] = localHKDF(new Uint8Array(0)) - encKey = write + const [write, read] = localHKDF(new Uint8Array(0)) + encKey = write decKey = read hash = Buffer.from([]) readCounter = 0 writeCounter = 0 isFinished = true - } + } const data = Binary.build(NOISE_MODE).readBuffer() let hash = Buffer.from(data.byteLength === 32 ? data : sha256(Buffer.from(data))) @@ -123,11 +131,12 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key if(isFinished) { data = encrypt(data) } + const introSize = sentIntro ? 0 : NOISE_WA_HEADER.length outBinary.ensureAdditionalCapacity(introSize + 3 + data.byteLength) - if (!sentIntro) { + if(!sentIntro) { outBinary.writeByteArray(NOISE_WA_HEADER) sentIntro = true } @@ -146,6 +155,7 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key const getBytesSize = () => { return (inBinary.readUint8() << 16) | inBinary.readUint16() } + const peekSize = () => { 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() frame = decodeBinaryNode(unpacked) } + onFrame(frame) } + inBinary.peek(peekSize) } } diff --git a/src/Utils/signal.ts b/src/Utils/signal.ts index 88533d8..7b0dfa2 100644 --- a/src/Utils/signal.ts +++ b/src/Utils/signal.ts @@ -1,10 +1,10 @@ import * as libsignal from 'libsignal' -import { encodeBigEndian } from "./generics" -import { Curve } from "./crypto" -import { SenderKeyDistributionMessage, GroupSessionBuilder, SenderKeyRecord, SenderKeyName, GroupCipher } from '../../WASignalGroup' -import { SignalIdentity, SignalKeyStore, SignedKeyPair, KeyPair, SignalAuthState, AuthenticationCreds } from "../Types/Auth" -import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildUInt, jidDecode, JidWithDevice, getBinaryNodeChildren } from "../WABinary" -import { proto } from "../../WAProto" +import { proto } from '../../WAProto' +import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage, SenderKeyName, SenderKeyRecord } from '../../WASignalGroup' +import { AuthenticationCreds, KeyPair, SignalAuthState, SignalIdentity, SignalKeyStore, SignedKeyPair } from '../Types/Auth' +import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildUInt, jidDecode, JidWithDevice } from '../WABinary' +import { Curve } from './crypto' +import { encodeBigEndian } from './generics' export const generateSignalPubKey = (pubKey: Uint8Array | Buffer) => { const newPub = Buffer.alloc(33) @@ -38,12 +38,13 @@ export const getPreKeys = async({ get }: SignalKeyStore, min: number, limit: num for(let id = min; id < limit;id++) { idList.push(id.toString()) } + return get('pre-key', idList) } export const generateOrGetPreKeys = (creds: AuthenticationCreds, range: number) => { const avaliable = creds.nextPreKeyId - creds.firstUnuploadedPreKeyId - const remaining = range - avaliable + const remaining = range - avaliable const lastPreKeyId = creds.nextPreKeyId + remaining - 1 const newPreKeys: { [id: number]: KeyPair } = { } if(remaining > 0) { @@ -51,6 +52,7 @@ export const generateOrGetPreKeys = (creds: AuthenticationCreds, range: number) newPreKeys[i] = Curve.generateKeyPair() } } + return { newPreKeys, lastPreKeyId, @@ -83,7 +85,7 @@ export const xmppPreKey = (pair: KeyPair, id: number): BinaryNode => ( ) export const signalStorage = ({ creds, keys }: SignalAuthState) => ({ - loadSession: async (id: string) => { + loadSession: async(id: string) => { const { [id]: sess } = await keys.get('session', [id]) if(sess) { return libsignal.SessionRecord.deserialize(sess) @@ -115,7 +117,9 @@ export const signalStorage = ({ creds, keys }: SignalAuthState) => ({ }, loadSenderKey: async(keyId: string) => { 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) => { await keys.set({ 'sender-key': { [keyId]: key.serialize() } }) @@ -144,7 +148,7 @@ export const processSenderKeyMessage = async( item: proto.ISenderKeyDistributionMessage, auth: SignalAuthState ) => { - const builder = new GroupSessionBuilder(signalStorage(auth)) + const builder = new GroupSessionBuilder(signalStorage(auth)) const senderName = jidToSignalSenderKeyName(item.groupId, authorJid) const senderMsg = new SenderKeyDistributionMessage(null, null, null, null, item.axolotlSenderKeyDistributionMessage) @@ -153,6 +157,7 @@ export const processSenderKeyMessage = async( const record = new SenderKeyRecord() await auth.keys.set({ 'sender-key': { [senderName]: record } }) } + await builder.process(senderName, senderMsg) } @@ -160,14 +165,15 @@ export const decryptSignalProto = async(user: string, type: 'pkmsg' | 'msg', msg const addr = jidToSignalProtocolAddress(user) const session = new libsignal.SessionCipher(signalStorage(auth), addr) let result: Buffer - switch(type) { - case 'pkmsg': - result = await session.decryptPreKeyWhisperMessage(msg) + switch (type) { + case 'pkmsg': + result = await session.decryptPreKeyWhisperMessage(msg) break - case 'msg': - result = await session.decryptWhisperMessage(msg) + case 'msg': + result = await session.decryptWhisperMessage(msg) break } + return result } @@ -205,17 +211,18 @@ export const encryptSenderKeyMsgSignalProto = async(group: string, data: Uint8Ar export const parseAndInjectE2ESessions = async(node: BinaryNode, auth: SignalAuthState) => { const extractKey = (key: BinaryNode) => ( key ? ({ - keyId: getBinaryNodeChildUInt(key, 'id', 3), - publicKey: generateSignalPubKey( + keyId: getBinaryNodeChildUInt(key, 'id', 3), + publicKey: generateSignalPubKey( getBinaryNodeChildBuffer(key, 'value') ), - signature: getBinaryNodeChildBuffer(key, 'signature'), - }) : undefined + signature: getBinaryNodeChildBuffer(key, 'signature'), + }) : undefined ) const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user') for(const node of nodes) { assertNodeErrorFree(node) } + await Promise.all( nodes.map( async node => { @@ -264,5 +271,6 @@ export const extractDeviceJids = (result: BinaryNode, myJid: string, excludeZero } } } + return extracted } \ No newline at end of file diff --git a/src/Utils/validate-connection.ts b/src/Utils/validate-connection.ts index 88468b8..8656493 100644 --- a/src/Utils/validate-connection.ts +++ b/src/Utils/validate-connection.ts @@ -1,152 +1,152 @@ import { Boom } from '@hapi/boom' 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 { encodeInt } from './generics' -import { BinaryNode, S_WHATSAPP_NET, jidDecode, Binary, getAllBinaryNodeChildren } from '../WABinary' import { createSignalIdentity } from './signal' const ENCODED_VERSION = 'S9Kdc4pc4EJryo21snc5cg==' const getUserAgent = ({ version, browser }: Pick) => ({ - appVersion: { - primary: version[0], - secondary: version[1], - tertiary: version[2], - }, - platform: 14, - releaseChannel: 0, - mcc: "000", - mnc: "000", - osVersion: browser[2], - manufacturer: "", - device: browser[1], - osBuildNumber: "0.1", - localeLanguageIso6391: 'en', - localeCountryIso31661Alpha2: 'en', + appVersion: { + primary: version[0], + secondary: version[1], + tertiary: version[2], + }, + platform: 14, + releaseChannel: 0, + mcc: '000', + mnc: '000', + osVersion: browser[2], + manufacturer: '', + device: browser[1], + osBuildNumber: '0.1', + localeLanguageIso6391: 'en', + localeCountryIso31661Alpha2: 'en', }) export const generateLoginNode = (userJid: string, config: Pick) => { - const { user, device } = jidDecode(userJid) - const payload = { - passive: true, - connectType: 1, - connectReason: 1, - userAgent: getUserAgent(config), - webInfo: { webSubPlatform: 0 }, - username: parseInt(user, 10), - device: device, - } - return proto.ClientPayload.encode(payload).finish() + const { user, device } = jidDecode(userJid) + const payload = { + passive: true, + connectType: 1, + connectReason: 1, + userAgent: getUserAgent(config), + webInfo: { webSubPlatform: 0 }, + username: parseInt(user, 10), + device: device, + } + return proto.ClientPayload.encode(payload).finish() } export const generateRegistrationNode = ( - { registrationId, signedPreKey, signedIdentityKey }: SignalCreds, - config: Pick + { registrationId, signedPreKey, signedIdentityKey }: SignalCreds, + config: Pick ) => { - const appVersionBuf = new Uint8Array(Buffer.from(ENCODED_VERSION, "base64")); + const appVersionBuf = new Uint8Array(Buffer.from(ENCODED_VERSION, 'base64')) - const companion = { - os: config.browser[0], - version: { - primary: 10, - secondary: undefined, - tertiary: undefined, - }, - platformType: 1, - requireFullSync: false, - }; + const companion = { + os: config.browser[0], + version: { + primary: 10, + secondary: undefined, + tertiary: undefined, + }, + platformType: 1, + requireFullSync: false, + } - const companionProto = proto.CompanionProps.encode(companion).finish() + const companionProto = proto.CompanionProps.encode(companion).finish() - const registerPayload = { - connectReason: 1, - connectType: 1, - passive: false, - regData: { - buildHash: appVersionBuf, - companionProps: companionProto, - eRegid: encodeInt(4, registrationId), - eKeytype: encodeInt(1, 5), - eIdent: signedIdentityKey.public, - eSkeyId: encodeInt(3, signedPreKey.keyId), - eSkeyVal: signedPreKey.keyPair.public, - eSkeySig: signedPreKey.signature, - }, - userAgent: getUserAgent(config), - webInfo: { - webSubPlatform: 0, - }, - } + const registerPayload = { + connectReason: 1, + connectType: 1, + passive: false, + regData: { + buildHash: appVersionBuf, + companionProps: companionProto, + eRegid: encodeInt(4, registrationId), + eKeytype: encodeInt(1, 5), + eIdent: signedIdentityKey.public, + eSkeyId: encodeInt(3, signedPreKey.keyId), + eSkeyVal: signedPreKey.keyPair.public, + eSkeySig: signedPreKey.signature, + }, + userAgent: getUserAgent(config), + webInfo: { + webSubPlatform: 0, + }, + } - return proto.ClientPayload.encode(registerPayload).finish() + return proto.ClientPayload.encode(registerPayload).finish() } export const configureSuccessfulPairing = ( - stanza: BinaryNode, - { advSecretKey, signedIdentityKey, signalIdentities }: Pick + stanza: BinaryNode, + { advSecretKey, signedIdentityKey, signalIdentities }: Pick ) => { - const [pair] = getAllBinaryNodeChildren(stanza) - const pairContent = Array.isArray(pair.content) ? pair.content : [] + const [pair] = getAllBinaryNodeChildren(stanza) + const pairContent = Array.isArray(pair.content) ? pair.content : [] - const msgId = stanza.attrs.id - const deviceIdentity = pairContent.find(m => m.tag === 'device-identity')?.content - const businessName = pairContent.find(m => m.tag === 'biz')?.attrs?.name - const verifiedName = businessName || '' - const jid = pairContent.find(m => m.tag === 'device')?.attrs?.jid + const msgId = stanza.attrs.id + const deviceIdentity = pairContent.find(m => m.tag === 'device-identity')?.content + const businessName = pairContent.find(m => m.tag === 'biz')?.attrs?.name + const verifiedName = businessName || '' + const jid = pairContent.find(m => m.tag === 'device')?.attrs?.jid - const { details, hmac } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentity as Buffer) + const { details, hmac } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentity as Buffer) - const advSign = hmacSign(details, Buffer.from(advSecretKey, 'base64')) + const advSign = hmacSign(details, Buffer.from(advSecretKey, 'base64')) - if (Buffer.compare(hmac, advSign) !== 0) { - throw new Boom('Invalid pairing') - } + if(Buffer.compare(hmac, advSign) !== 0) { + throw new Boom('Invalid pairing') + } - const account = proto.ADVSignedDeviceIdentity.decode(details) - const { accountSignatureKey, accountSignature } = account + const account = proto.ADVSignedDeviceIdentity.decode(details) + const { accountSignatureKey, accountSignature } = account - const accountMsg = Binary.build(new Uint8Array([6, 0]), account.details, signedIdentityKey.public).readByteArray() - if (!Curve.verify(accountSignatureKey, accountMsg, accountSignature)) { - throw new Boom('Failed to verify account signature') - } + const accountMsg = Binary.build(new Uint8Array([6, 0]), account.details, signedIdentityKey.public).readByteArray() + if(!Curve.verify(accountSignatureKey, accountMsg, accountSignature)) { + throw new Boom('Failed to verify account signature') + } - const deviceMsg = Binary.build(new Uint8Array([6, 1]), account.details, signedIdentityKey.public, account.accountSignatureKey).readByteArray() - account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg) + const deviceMsg = Binary.build(new Uint8Array([6, 1]), account.details, signedIdentityKey.public, account.accountSignatureKey).readByteArray() + account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg) - const identity = createSignalIdentity(jid, accountSignatureKey) + const identity = createSignalIdentity(jid, accountSignatureKey) - const keyIndex = proto.ADVDeviceIdentity.decode(account.details).keyIndex + const keyIndex = proto.ADVDeviceIdentity.decode(account.details).keyIndex - const accountEnc = proto.ADVSignedDeviceIdentity.encode({ - ...account.toJSON(), - accountSignatureKey: undefined - }).finish() + const accountEnc = proto.ADVSignedDeviceIdentity.encode({ + ...account.toJSON(), + accountSignatureKey: undefined + }).finish() - const reply: BinaryNode = { - tag: 'iq', - attrs: { - to: S_WHATSAPP_NET, - type: 'result', - id: msgId, - }, - content: [ - { - tag: 'pair-device-sign', - attrs: { }, - content: [ - { tag: 'device-identity', attrs: { 'key-index': `${keyIndex}` }, content: accountEnc } - ] - } - ] - } + const reply: BinaryNode = { + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + type: 'result', + id: msgId, + }, + content: [ + { + tag: 'pair-device-sign', + attrs: { }, + content: [ + { tag: 'device-identity', attrs: { 'key-index': `${keyIndex}` }, content: accountEnc } + ] + } + ] + } - const authUpdate: Partial = { - account, - me: { id: jid, verifiedName }, - signalIdentities: [...(signalIdentities || []), identity] - } - return { - creds: authUpdate, - reply - } + const authUpdate: Partial = { + account, + me: { id: jid, verifiedName }, + signalIdentities: [...(signalIdentities || []), identity] + } + return { + creds: authUpdate, + reply + } } \ No newline at end of file diff --git a/src/WABinary/Legacy/index.ts b/src/WABinary/Legacy/index.ts index 945eed0..b73cf28 100644 --- a/src/WABinary/Legacy/index.ts +++ b/src/WABinary/Legacy/index.ts @@ -3,214 +3,239 @@ import { BinaryNode } from '../types' import { DoubleByteTokens, SingleByteTokens, Tags } from './constants' export const isLegacyBinaryNode = (buffer: Buffer) => { - switch(buffer[0]) { - case Tags.LIST_EMPTY: - case Tags.LIST_8: - case Tags.LIST_16: - return true - default: - return false + switch (buffer[0]) { + case Tags.LIST_EMPTY: + case Tags.LIST_8: + case Tags.LIST_16: + return true + default: + return false } } function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode { - const checkEOS = (length: number) => { - if (indexRef.index + length > buffer.length) { - throw new Error('end of stream') - } - } - const next = () => { - const value = buffer[indexRef.index] - indexRef.index += 1 - return value - } - const readByte = () => { - checkEOS(1) - return next() - } - const readStringFromChars = (length: number) => { - checkEOS(length) - const value = buffer.slice(indexRef.index, indexRef.index + length) + const checkEOS = (length: number) => { + if(indexRef.index + length > buffer.length) { + throw new Error('end of stream') + } + } - indexRef.index += length - return value.toString('utf-8') - } - const readBytes = (n: number) => { - checkEOS(n) - const value = buffer.slice(indexRef.index, indexRef.index + n) - indexRef.index += n - return value - } - const readInt = (n: number, littleEndian = false) => { - checkEOS(n) - let val = 0 - for (let i = 0; i < n; i++) { - const shift = littleEndian ? i : n - 1 - i - val |= next() << (shift * 8) - } - return val - } - const readInt20 = () => { - checkEOS(3) - return ((next() & 15) << 16) + (next() << 8) + next() - } - const unpackHex = (value: number) => { - if (value >= 0 && value < 16) { - return value < 10 ? '0'.charCodeAt(0) + value : 'A'.charCodeAt(0) + value - 10 - } - throw new Error('invalid hex: ' + value) - } - const unpackNibble = (value: number) => { - if (value >= 0 && value <= 9) { - return '0'.charCodeAt(0) + value - } - switch (value) { - case 10: - return '-'.charCodeAt(0) - case 11: - return '.'.charCodeAt(0) - case 15: - return '\0'.charCodeAt(0) - default: - throw new Error('invalid nibble: ' + value) - } - } - const unpackByte = (tag: number, value: number) => { - if (tag === Tags.NIBBLE_8) { - return unpackNibble(value) - } else if (tag === Tags.HEX_8) { - return unpackHex(value) - } else { - throw new Error('unknown tag: ' + tag) - } - } - const readPacked8 = (tag: number) => { - const startByte = readByte() - let value = '' + const next = () => { + const value = buffer[indexRef.index] + indexRef.index += 1 + return value + } - for (let i = 0; i < (startByte & 127); i++) { - const curByte = readByte() - value += String.fromCharCode(unpackByte(tag, (curByte & 0xf0) >> 4)) - value += String.fromCharCode(unpackByte(tag, curByte & 0x0f)) - } - if (startByte >> 7 !== 0) { - value = value.slice(0, -1) - } - return value - } - const isListTag = (tag: number) => { - return tag === Tags.LIST_EMPTY || tag === Tags.LIST_8 || tag === Tags.LIST_16 - } - const readListSize = (tag: number) => { - switch (tag) { - case Tags.LIST_EMPTY: - return 0 - case Tags.LIST_8: - return readByte() - case Tags.LIST_16: - return readInt(2) - default: - throw new Error('invalid tag for list size: ' + tag) - } - } - const getToken = (index: number) => { - if (index < 3 || index >= SingleByteTokens.length) { - throw new Error('invalid token index: ' + index) - } - return SingleByteTokens[index] - } - const readString = (tag: number) => { - if (tag >= 3 && tag <= 235) { - const token = getToken(tag) - return token// === 's.whatsapp.net' ? 'c.us' : token - } + const readByte = () => { + checkEOS(1) + return next() + } - switch (tag) { - case Tags.DICTIONARY_0: - case Tags.DICTIONARY_1: - case Tags.DICTIONARY_2: - case Tags.DICTIONARY_3: - return getTokenDouble(tag - Tags.DICTIONARY_0, readByte()) - case Tags.LIST_EMPTY: - return null - case Tags.BINARY_8: - return readStringFromChars(readByte()) - case Tags.BINARY_20: - return readStringFromChars(readInt20()) - case Tags.BINARY_32: - return readStringFromChars(readInt(4)) - case Tags.JID_PAIR: - const i = readString(readByte()) - const j = readString(readByte()) - if (typeof i === 'string' && j) { - return i + '@' + j - } - throw new Error('invalid jid pair: ' + i + ', ' + j) - case Tags.HEX_8: - case Tags.NIBBLE_8: - return readPacked8(tag) - default: - throw new Error('invalid string with tag: ' + tag) - } - } - const readList = (tag: number) => ( - [...new Array(readListSize(tag))].map(() => decode(buffer, indexRef)) - ) - const getTokenDouble = (index1: number, index2: number) => { - const n = 256 * index1 + index2 - if (n < 0 || n > DoubleByteTokens.length) { - throw new Error('invalid double token index: ' + n) - } - return DoubleByteTokens[n] - } + const readStringFromChars = (length: number) => { + checkEOS(length) + const value = buffer.slice(indexRef.index, indexRef.index + length) - const listSize = readListSize(readByte()) - const descrTag = readByte() - if (descrTag === Tags.STREAM_END) { - throw new Error('unexpected stream end') - } - const header = readString(descrTag) + indexRef.index += length + return value.toString('utf-8') + } + + const readBytes = (n: number) => { + checkEOS(n) + const value = buffer.slice(indexRef.index, indexRef.index + n) + indexRef.index += n + return value + } + + const readInt = (n: number, littleEndian = false) => { + checkEOS(n) + let val = 0 + for(let i = 0; i < n; i++) { + const shift = littleEndian ? i : n - 1 - i + val |= next() << (shift * 8) + } + + return val + } + + const readInt20 = () => { + checkEOS(3) + return ((next() & 15) << 16) + (next() << 8) + next() + } + + const unpackHex = (value: number) => { + if(value >= 0 && value < 16) { + return value < 10 ? '0'.charCodeAt(0) + value : 'A'.charCodeAt(0) + value - 10 + } + + throw new Error('invalid hex: ' + value) + } + + const unpackNibble = (value: number) => { + if(value >= 0 && value <= 9) { + return '0'.charCodeAt(0) + value + } + + switch (value) { + case 10: + return '-'.charCodeAt(0) + case 11: + return '.'.charCodeAt(0) + case 15: + return '\0'.charCodeAt(0) + default: + throw new Error('invalid nibble: ' + value) + } + } + + const unpackByte = (tag: number, value: number) => { + if(tag === Tags.NIBBLE_8) { + return unpackNibble(value) + } else if(tag === Tags.HEX_8) { + return unpackHex(value) + } else { + throw new Error('unknown tag: ' + tag) + } + } + + const readPacked8 = (tag: number) => { + const startByte = readByte() + let value = '' + + for(let i = 0; i < (startByte & 127); i++) { + const curByte = readByte() + value += String.fromCharCode(unpackByte(tag, (curByte & 0xf0) >> 4)) + value += String.fromCharCode(unpackByte(tag, curByte & 0x0f)) + } + + if(startByte >> 7 !== 0) { + value = value.slice(0, -1) + } + + return value + } + + const isListTag = (tag: number) => { + return tag === Tags.LIST_EMPTY || tag === Tags.LIST_8 || tag === Tags.LIST_16 + } + + const readListSize = (tag: number) => { + switch (tag) { + case Tags.LIST_EMPTY: + return 0 + case Tags.LIST_8: + return readByte() + case Tags.LIST_16: + return readInt(2) + default: + throw new Error('invalid tag for list size: ' + tag) + } + } + + const getToken = (index: number) => { + if(index < 3 || index >= SingleByteTokens.length) { + throw new Error('invalid token index: ' + index) + } + + return SingleByteTokens[index] + } + + const readString = (tag: number) => { + if(tag >= 3 && tag <= 235) { + const token = getToken(tag) + return token// === 's.whatsapp.net' ? 'c.us' : token + } + + switch (tag) { + case Tags.DICTIONARY_0: + case Tags.DICTIONARY_1: + case Tags.DICTIONARY_2: + case Tags.DICTIONARY_3: + return getTokenDouble(tag - Tags.DICTIONARY_0, readByte()) + case Tags.LIST_EMPTY: + return null + case Tags.BINARY_8: + return readStringFromChars(readByte()) + case Tags.BINARY_20: + return readStringFromChars(readInt20()) + case Tags.BINARY_32: + return readStringFromChars(readInt(4)) + case Tags.JID_PAIR: + const i = readString(readByte()) + const j = readString(readByte()) + if(typeof i === 'string' && j) { + return i + '@' + j + } + + throw new Error('invalid jid pair: ' + i + ', ' + j) + case Tags.HEX_8: + case Tags.NIBBLE_8: + return readPacked8(tag) + default: + throw new Error('invalid string with tag: ' + tag) + } + } + + const readList = (tag: number) => ( + [...new Array(readListSize(tag))].map(() => decode(buffer, indexRef)) + ) + const getTokenDouble = (index1: number, index2: number) => { + const n = 256 * index1 + index2 + if(n < 0 || n > DoubleByteTokens.length) { + throw new Error('invalid double token index: ' + n) + } + + return DoubleByteTokens[n] + } + + const listSize = readListSize(readByte()) + const descrTag = readByte() + if(descrTag === Tags.STREAM_END) { + throw new Error('unexpected stream end') + } + + const header = readString(descrTag) const attrs: BinaryNode['attrs'] = { } let data: BinaryNode['content'] - if (listSize === 0 || !header) { - throw new Error('invalid node') - } - // read the attributes in + if(listSize === 0 || !header) { + throw new Error('invalid node') + } + // read the attributes in - const attributesLength = (listSize - 1) >> 1 - for (let i = 0; i < attributesLength; i++) { - const key = readString(readByte()) - const b = readByte() + const attributesLength = (listSize - 1) >> 1 + for(let i = 0; i < attributesLength; i++) { + const key = readString(readByte()) + const b = readByte() - attrs[key] = readString(b) - } + attrs[key] = readString(b) + } + + if(listSize % 2 === 0) { + const tag = readByte() + if(isListTag(tag)) { + data = readList(tag) + } else { + let decoded: Buffer | string + switch (tag) { + case Tags.BINARY_8: + decoded = readBytes(readByte()) + break + case Tags.BINARY_20: + decoded = readBytes(readInt20()) + break + case Tags.BINARY_32: + decoded = readBytes(readInt(4)) + break + default: + decoded = readString(tag) + break + } - if (listSize % 2 === 0) { - const tag = readByte() - if (isListTag(tag)) { - data = readList(tag) - } else { - let decoded: Buffer | string - switch (tag) { - case Tags.BINARY_8: - decoded = readBytes(readByte()) - break - case Tags.BINARY_20: - decoded = readBytes(readInt20()) - break - case Tags.BINARY_32: - decoded = readBytes(readInt(4)) - break - default: - decoded = readString(tag) - break - } data = decoded - } - } + } + } - return { + return { tag: header, attrs, content: data @@ -221,85 +246,97 @@ const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => { const pushByte = (value: number) => buffer.push(value & 0xff) - const pushInt = (value: number, n: number, littleEndian=false) => { - for (let i = 0; i < n; i++) { - const curShift = littleEndian ? i : n - 1 - i - buffer.push((value >> (curShift * 8)) & 0xff) - } - } - const pushBytes = (bytes: Uint8Array | Buffer | number[]) => ( + const pushInt = (value: number, n: number, littleEndian=false) => { + for(let i = 0; i < n; i++) { + const curShift = littleEndian ? i : n - 1 - i + buffer.push((value >> (curShift * 8)) & 0xff) + } + } + + const pushBytes = (bytes: Uint8Array | Buffer | number[]) => ( bytes.forEach (b => buffer.push(b)) ) - const pushInt20 = (value: number) => ( + const pushInt20 = (value: number) => ( pushBytes([(value >> 16) & 0x0f, (value >> 8) & 0xff, value & 0xff]) ) - const writeByteLength = (length: number) => { - if (length >= 4294967296) throw new Error('string too large to encode: ' + length) + const writeByteLength = (length: number) => { + if(length >= 4294967296) { + throw new Error('string too large to encode: ' + length) + } - if (length >= 1 << 20) { - pushByte(Tags.BINARY_32) - pushInt(length, 4) // 32 bit integer - } else if (length >= 256) { - pushByte(Tags.BINARY_20) - pushInt20(length) - } else { - pushByte(Tags.BINARY_8) - pushByte(length) - } - } - const writeStringRaw = (str: string) => { - const bytes = Buffer.from (str, 'utf-8') - writeByteLength(bytes.length) - pushBytes(bytes) - } - const writeToken = (token: number) => { - if (token < 245) { - pushByte(token) - } else if (token <= 500) { - throw new Error('invalid token') - } - } - const writeString = (token: string, i?: boolean) => { - if (token === 'c.us') token = 's.whatsapp.net' + if(length >= 1 << 20) { + pushByte(Tags.BINARY_32) + pushInt(length, 4) // 32 bit integer + } else if(length >= 256) { + pushByte(Tags.BINARY_20) + pushInt20(length) + } else { + pushByte(Tags.BINARY_8) + pushByte(length) + } + } + + const writeStringRaw = (str: string) => { + const bytes = Buffer.from (str, 'utf-8') + writeByteLength(bytes.length) + pushBytes(bytes) + } + + const writeToken = (token: number) => { + if(token < 245) { + pushByte(token) + } else if(token <= 500) { + throw new Error('invalid token') + } + } + + const writeString = (token: string, i?: boolean) => { + if(token === 'c.us') { + token = 's.whatsapp.net' + } - const tokenIndex = SingleByteTokens.indexOf(token) - if (!i && token === 's.whatsapp.net') { - writeToken(tokenIndex) - } else if (tokenIndex >= 0) { - if (tokenIndex < Tags.SINGLE_BYTE_MAX) { - writeToken(tokenIndex) - } else { - const overflow = tokenIndex - Tags.SINGLE_BYTE_MAX - const dictionaryIndex = overflow >> 8 - if (dictionaryIndex < 0 || dictionaryIndex > 3) { - throw new Error('double byte dict token out of range: ' + token + ', ' + tokenIndex) - } - writeToken(Tags.DICTIONARY_0 + dictionaryIndex) - writeToken(overflow % 256) - } - } else if (token) { - const jidSepIndex = token.indexOf('@') - if (jidSepIndex <= 0) { - writeStringRaw(token) - } else { - writeJid(token.slice(0, jidSepIndex), token.slice(jidSepIndex + 1, token.length)) - } - } - } - const writeJid = (left: string, right: string) => { - pushByte(Tags.JID_PAIR) - left && left.length > 0 ? writeString(left) : writeToken(Tags.LIST_EMPTY) - writeString(right) - } - const writeListStart = (listSize: number) => { - if (listSize === 0) { - pushByte(Tags.LIST_EMPTY) - } else if (listSize < 256) { - pushBytes([Tags.LIST_8, listSize]) - } else { - pushBytes([Tags.LIST_16, listSize]) - } - } + const tokenIndex = SingleByteTokens.indexOf(token) + if(!i && token === 's.whatsapp.net') { + writeToken(tokenIndex) + } else if(tokenIndex >= 0) { + if(tokenIndex < Tags.SINGLE_BYTE_MAX) { + writeToken(tokenIndex) + } else { + const overflow = tokenIndex - Tags.SINGLE_BYTE_MAX + const dictionaryIndex = overflow >> 8 + if(dictionaryIndex < 0 || dictionaryIndex > 3) { + throw new Error('double byte dict token out of range: ' + token + ', ' + tokenIndex) + } + + writeToken(Tags.DICTIONARY_0 + dictionaryIndex) + writeToken(overflow % 256) + } + } else if(token) { + const jidSepIndex = token.indexOf('@') + if(jidSepIndex <= 0) { + writeStringRaw(token) + } else { + writeJid(token.slice(0, jidSepIndex), token.slice(jidSepIndex + 1, token.length)) + } + } + } + + const writeJid = (left: string, right: string) => { + pushByte(Tags.JID_PAIR) + left && left.length > 0 ? writeString(left) : writeToken(Tags.LIST_EMPTY) + writeString(right) + } + + const writeListStart = (listSize: number) => { + if(listSize === 0) { + pushByte(Tags.LIST_EMPTY) + } else if(listSize < 256) { + pushBytes([Tags.LIST_8, listSize]) + } else { + pushBytes([Tags.LIST_16, listSize]) + } + } + const validAttributes = Object.keys(attrs).filter(k => ( typeof attrs[k] !== 'undefined' && attrs[k] !== null )) @@ -308,25 +345,27 @@ const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => { writeString(tag) validAttributes.forEach((key) => { - if(typeof attrs[key] === 'string') { - writeString(key) - writeString(attrs[key]) - } + if(typeof attrs[key] === 'string') { + writeString(key) + writeString(attrs[key]) + } }) - if (typeof content === 'string') { + if(typeof content === 'string') { writeString(content, true) - } else if (Buffer.isBuffer(content)) { + } else if(Buffer.isBuffer(content)) { writeByteLength(content.length) pushBytes(content) - } else if (Array.isArray(content)) { + } else if(Array.isArray(content)) { writeListStart(content.length) for(const item of content) { - if(item) encode(item, buffer) + if(item) { + encode(item, buffer) + } } } else if(typeof content === 'undefined' || content === null) { - } else { + } else { throw new Error(`invalid children for header "${tag}": ${content} (${typeof content})`) } diff --git a/src/WABinary/generic-utils.ts b/src/WABinary/generic-utils.ts new file mode 100644 index 0000000..82d601c --- /dev/null +++ b/src/WABinary/generic-utils.ts @@ -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 +} \ No newline at end of file diff --git a/src/WABinary/index.ts b/src/WABinary/index.ts index 0633c4f..3595df2 100644 --- a/src/WABinary/index.ts +++ b/src/WABinary/index.ts @@ -319,6 +319,7 @@ export const getBinaryNodeMessages = ({ content }: BinaryNode) => { return msgs } +export * from './generic-utils' export * from './jid-utils' export { Binary } from '../../WABinary/Binary' export * from './types' diff --git a/src/WABinary/jid-utils.ts b/src/WABinary/jid-utils.ts index 5b09c00..266482f 100644 --- a/src/WABinary/jid-utils.ts +++ b/src/WABinary/jid-utils.ts @@ -1,7 +1,7 @@ export const S_WHATSAPP_NET = '@s.whatsapp.net' export const OFFICIAL_BIZ_JID = '16505361212@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 type JidServer = 'c.us' | 'g.us' | 'broadcast' | 's.whatsapp.net' | 'call' @@ -12,30 +12,32 @@ export type JidWithDevice = { } export const jidEncode = (user: string | number | null, server: JidServer, device?: number, agent?: number) => { - return `${user || ''}${!!agent ? `_${agent}` : ''}${!!device ? `:${device}` : ''}@${server}` + return `${user || ''}${!!agent ? `_${agent}` : ''}${!!device ? `:${device}` : ''}@${server}` } export const jidDecode = (jid: string) => { - let sepIdx = typeof jid === 'string' ? jid.indexOf('@') : -1 - if(sepIdx < 0) { - return undefined - } - const server = jid.slice(sepIdx+1) - const userCombined = jid.slice(0, sepIdx) + const sepIdx = typeof jid === 'string' ? jid.indexOf('@') : -1 + if(sepIdx < 0) { + return undefined + } - const [userAgent, device] = userCombined.split(':') - const [user, agent] = userAgent.split('_') + const server = jid.slice(sepIdx+1) + const userCombined = jid.slice(0, sepIdx) - return { - server, - user, - agent: agent ? +agent : undefined, - device: device ? +device : undefined - } + const [userAgent, device] = userCombined.split(':') + const [user, agent] = userAgent.split('_') + + return { + server, + user, + agent: agent ? +agent : undefined, + device: device ? +device : undefined + } } + /** is the jid a user */ export const areJidsSameUser = (jid1: string, jid2: string) => ( - jidDecode(jid1)?.user === jidDecode(jid2)?.user + jidDecode(jid1)?.user === jidDecode(jid2)?.user ) /** is the jid a user */ export const isJidUser = (jid: string) => (jid?.endsWith('@s.whatsapp.net')) @@ -47,6 +49,6 @@ export const isJidGroup = (jid: string) => (jid?.endsWith('@g.us')) export const isJidStatusBroadcast = (jid: string) => jid === 'status@broadcast' export const jidNormalizedUser = (jid: string) => { - const { user, server } = jidDecode(jid) - return jidEncode(user, server === 'c.us' ? 's.whatsapp.net' : server as JidServer) + const { user, server } = jidDecode(jid) + return jidEncode(user, server === 'c.us' ? 's.whatsapp.net' : server as JidServer) } \ No newline at end of file diff --git a/src/WABinary/types.ts b/src/WABinary/types.ts index 96e35d4..66714f9 100644 --- a/src/WABinary/types.ts +++ b/src/WABinary/types.ts @@ -5,7 +5,7 @@ * This is done for easy serialization, to prevent running into issues with prototypes & * to maintain functional code structure * */ - export type BinaryNode = { +export type BinaryNode = { tag: string attrs: { [key: string]: string } content?: BinaryNode[] | string | Uint8Array diff --git a/src/index.ts b/src/index.ts index 8139a9f..d52f14b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -import makeWASocket from './Socket' import makeWALegacySocket from './LegacySocket' +import makeWASocket from './Socket' export * from '../WAProto' export * from './Utils' diff --git a/yarn.lock b/yarn.lock index dbeb44b..950ff72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,23 @@ # yarn lockfile v1 +"@adiwajshing/eslint-config@git+https://github.com/adiwajshing/eslint-config": + version "1.0.0" + resolved "git+https://github.com/adiwajshing/eslint-config#db16c7427bd6dcf8fba20e0aaa526724e46c83aa" + dependencies: + "@typescript-eslint/eslint-plugin" "^4.33.0" + "@typescript-eslint/parser" "^4.33.0" + eslint-plugin-react "^7.26.1" + eslint-plugin-simple-import-sort "^7.0.0" + eslint-plugin-unused-imports "^1.1.5" + +"@babel/code-frame@7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" @@ -146,6 +163,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8" integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg== +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + "@babel/helper-validator-option@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" @@ -160,6 +182,15 @@ "@babel/traverse" "^7.14.5" "@babel/types" "^7.14.5" +"@babel/highlight@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.7.tgz#81a01d7d675046f0d96f82450d9d9578bdfd6b0b" + integrity sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/highlight@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" @@ -321,6 +352,21 @@ dependencies: "@cspotcode/source-map-consumer" "0.8.0" +"@eslint/eslintrc@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" + integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^13.9.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + "@hapi/boom@^9.1.3": version "9.1.3" resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.3.tgz#22cad56e39b7a4819161a99b1db19eaaa9b6cc6e" @@ -333,6 +379,20 @@ resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131" integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug== +"@humanwhocodes/config-array@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" + integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== + dependencies: + "@humanwhocodes/object-schema" "^1.2.0" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -819,6 +879,27 @@ "@babel/runtime" "^7.7.2" regenerator-runtime "^0.13.3" +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -992,6 +1073,11 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" +"@types/json-schema@^7.0.7": + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" + integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + "@types/long@^4.0.0", "@types/long@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" @@ -1091,6 +1177,76 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/eslint-plugin@^4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" + integrity sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg== + dependencies: + "@typescript-eslint/experimental-utils" "4.33.0" + "@typescript-eslint/scope-manager" "4.33.0" + debug "^4.3.1" + functional-red-black-tree "^1.0.1" + ignore "^5.1.8" + regexpp "^3.1.0" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/experimental-utils@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd" + integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q== + dependencies: + "@types/json-schema" "^7.0.7" + "@typescript-eslint/scope-manager" "4.33.0" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/typescript-estree" "4.33.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/parser@^4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.33.0.tgz#dfe797570d9694e560528d18eecad86c8c744899" + integrity sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA== + dependencies: + "@typescript-eslint/scope-manager" "4.33.0" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/typescript-estree" "4.33.0" + debug "^4.3.1" + +"@typescript-eslint/scope-manager@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz#d38e49280d983e8772e29121cf8c6e9221f280a3" + integrity sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ== + dependencies: + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/visitor-keys" "4.33.0" + +"@typescript-eslint/types@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" + integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== + +"@typescript-eslint/typescript-estree@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz#0dfb51c2908f68c5c08d82aefeaf166a17c24609" + integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA== + dependencies: + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/visitor-keys" "4.33.0" + debug "^4.3.1" + globby "^11.0.3" + is-glob "^4.0.1" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/visitor-keys@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd" + integrity sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg== + dependencies: + "@typescript-eslint/types" "4.33.0" + eslint-visitor-keys "^2.0.0" + abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -1104,6 +1260,11 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" +acorn-jsx@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + acorn-walk@^7.1.1: version "7.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" @@ -1114,7 +1275,7 @@ acorn-walk@^8.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^7.1.1: +acorn@^7.1.1, acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== @@ -1136,6 +1297,31 @@ agent-base@6: dependencies: debug "4" +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.1: + version "8.9.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.9.0.tgz#738019146638824dea25edcf299dcba1b0e7eb18" + integrity sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -1215,6 +1401,36 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +array-includes@^3.1.3, array-includes@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" + integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + get-intrinsic "^1.1.1" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.flatmap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz#908dc82d8a406930fdf38598d51e7411d18d4446" + integrity sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.19.0" + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1390,6 +1606,14 @@ buffer@^5.2.0, buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1565,7 +1789,7 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.3: +cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -1612,6 +1836,13 @@ debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: dependencies: ms "2.1.2" +debug@^4.0.1: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + decimal.js@^10.2.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" @@ -1634,6 +1865,11 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -1644,6 +1880,13 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1679,6 +1922,27 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + dom-walk@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" @@ -1713,6 +1977,48 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" +enquirer@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +es-abstract@^1.19.0, es-abstract@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" + integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.1" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.1" + is-string "^1.0.7" + is-weakref "^1.0.1" + object-inspect "^1.11.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -1728,6 +2034,11 @@ escape-string-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escodegen@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" @@ -1740,11 +2051,159 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" +eslint-plugin-react@^7.26.1: + version "7.28.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz#8f3ff450677571a659ce76efc6d80b6a525adbdf" + integrity sha512-IOlFIRHzWfEQQKcAD4iyYDndHwTQiCMcJVJjxempf203jnNLUnW34AXLrV33+nEXoifJE2ZEGmcjKPL8957eSw== + dependencies: + array-includes "^3.1.4" + array.prototype.flatmap "^1.2.5" + doctrine "^2.1.0" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.0.4" + object.entries "^1.1.5" + object.fromentries "^2.0.5" + object.hasown "^1.1.0" + object.values "^1.1.5" + prop-types "^15.7.2" + resolve "^2.0.0-next.3" + semver "^6.3.0" + string.prototype.matchall "^4.0.6" + +eslint-plugin-simple-import-sort@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-7.0.0.tgz#a1dad262f46d2184a90095a60c66fef74727f0f8" + integrity sha512-U3vEDB5zhYPNfxT5TYR7u01dboFZp+HNpnGhkDB2g/2E4wZ/g1Q9Ton8UwCLfRV9yAKyYqDh62oHOamvkFxsvw== + +eslint-plugin-unused-imports@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-1.1.5.tgz#a2b992ef0faf6c6c75c3815cc47bde76739513c2" + integrity sha512-TeV8l8zkLQrq9LBeYFCQmYVIXMjfHgdRQLw7dEZp4ZB3PeR10Y5Uif11heCsHRmhdRIYMoewr1d9ouUHLbLHew== + dependencies: + eslint-rule-composer "^0.3.0" + +eslint-rule-composer@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" + integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint@^7.0.0: + version "7.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" + integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== + dependencies: + "@babel/code-frame" "7.12.11" + "@eslint/eslintrc" "^0.4.3" + "@humanwhocodes/config-array" "^0.5.0" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + enquirer "^2.3.5" + escape-string-regexp "^4.0.0" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.1" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.1.2" + globals "^13.6.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^6.0.9" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^7.3.0, espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" + esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + estraverse@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" @@ -1797,12 +2256,28 @@ expect@^27.0.6: jest-message-util "^27.0.6" jest-regex-util "^27.0.6" +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@~2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -1817,6 +2292,13 @@ fast-safe-stringify@^2.0.8: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f" integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag== +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + fb-watchman@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" @@ -1824,6 +2306,13 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + file-type@^16.5.0: version "16.5.1" resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.1.tgz#dd697dc5c3a2f4db63af746f38a6322e5e7bc6a5" @@ -1853,11 +2342,24 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + flatstr@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931" integrity sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw== +flatted@^3.1.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.4.tgz#28d9969ea90661b5134259f312ab6aa7929ac5e2" + integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== + follow-redirects@^1.14.4: version "1.14.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd" @@ -1911,6 +2413,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -1935,6 +2442,15 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -1945,6 +2461,14 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + gifwrap@^0.9.2: version "0.9.2" resolved "https://registry.yarnpkg.com/gifwrap/-/gifwrap-0.9.2.tgz#348e286e67d7cf57942172e1e6f05a71cee78489" @@ -1958,6 +2482,13 @@ github-from-package@0.0.0: resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.1.7" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" @@ -1983,6 +2514,25 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== +globals@^13.6.0, globals@^13.9.0: + version "13.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.0.tgz#4d733760304230a0082ed96e21e5c565f898089e" + integrity sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg== + dependencies: + type-fest "^0.20.2" + +globby@^11.0.3: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" @@ -2000,6 +2550,11 @@ handlebars@^4.7.7: optionalDependencies: uglify-js "^3.1.4" +has-bigints@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -2010,6 +2565,18 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -2068,11 +2635,29 @@ ieee754@^1.1.13, ieee754@^1.2.1: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.1.8, ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + image-q@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/image-q/-/image-q-1.1.1.tgz#fc84099664460b90ca862d9300b6bfbbbfbf8056" integrity sha1-/IQJlmRGC5DKhi2TALa/u7+/gFY= +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + import-local@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6" @@ -2104,6 +2689,15 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + interpret@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -2114,6 +2708,26 @@ is-arrayish@^0.3.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-callable@^1.1.4, is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + is-ci@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.0.tgz#c7e7be3c9d8eef7d0fa144390bd1e4b88dc4c994" @@ -2128,6 +2742,18 @@ is-core-module@^2.2.0: dependencies: has "^1.0.3" +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + is-fullwidth-code-point@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" @@ -2150,6 +2776,25 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-negative-zero@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" + integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== + dependencies: + has-tostringtag "^1.0.0" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -2160,16 +2805,50 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-shared-array-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" + integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== + is-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-weakref@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -2661,7 +3340,7 @@ jpeg-js@0.4.2: resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.2.tgz#8b345b1ae4abde64c2da2fe67ea216a114ac279d" integrity sha512-+az2gi/hvex7eLTMTlbRLOhH6P6WFdk2ITI8HJsaH2VqYO0I594zXSYEP+tf4FW+8Cy68ScDXoAsQdyQanv3sw== -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -2712,6 +3391,21 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + json5@2.x, json5@^2.1.2, json5@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" @@ -2728,6 +3422,14 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +"jsx-ast-utils@^2.4.1 || ^3.0.0": + version "3.2.1" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" + integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA== + dependencies: + array-includes "^3.1.3" + object.assign "^4.1.2" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -2738,6 +3440,14 @@ leven@^3.1.0: resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" @@ -2774,6 +3484,16 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + lodash@4.x, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -2784,6 +3504,13 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -2837,6 +3564,11 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" @@ -3006,11 +3738,66 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== -object-assign@^4.1.0: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= +object-inspect@^1.11.0, object-inspect@^1.9.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" + integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== + +object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.entries@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" + integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.fromentries@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251" + integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.hasown@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.0.tgz#7232ed266f34d197d15cac5880232f7a4790afe5" + integrity sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.values@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" + integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + omggif@^1.0.10, omggif@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19" @@ -3049,6 +3836,18 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + p-each-series@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" @@ -3078,6 +3877,13 @@ pako@^1.0.5: resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + parse-bmfont-ascii@^1.0.3: version "1.0.6" resolved "https://registry.yarnpkg.com/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz#11ac3c3ff58f7c2020ab22769079108d4dfa0285" @@ -3126,6 +3932,11 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + peek-readable@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.0.0.tgz#b024ef391c86136eba0ae9df3ff4f966a09e9a7e" @@ -3203,6 +4014,11 @@ prebuild-install@^7.0.0: tar-fs "^2.0.0" tunnel-agent "^0.6.0" +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -3238,7 +4054,7 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -progress@^2.0.3: +progress@^2.0.0, progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -3251,6 +4067,15 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +prop-types@^15.7.2: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + protobufjs@6.8.8: version "6.8.8" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c" @@ -3302,7 +4127,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^2.1.1: +punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== @@ -3312,6 +4137,11 @@ qrcode-terminal@^0.12.0: resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + quick-format-unescaped@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.3.tgz#6d6b66b8207aa2b35eef12be1421bb24c428f652" @@ -3327,6 +4157,11 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + react-is@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" @@ -3373,11 +4208,29 @@ regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +regexp.prototype.flags@^1.3.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz#b3f4c0059af9e47eca9f3f660e51d81307e72307" + integrity sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +regexpp@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -3385,6 +4238,11 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + resolve-from@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" @@ -3398,13 +4256,33 @@ resolve@^1.1.6, resolve@^1.20.0: is-core-module "^2.2.0" path-parse "^1.0.6" -rimraf@^3.0.0: +resolve@^2.0.0-next.3: + version "2.0.0-next.3" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46" + integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -3432,7 +4310,7 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -semver@7.x, semver@^7.3.2, semver@^7.3.5: +semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.5: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== @@ -3493,6 +4371,15 @@ shiki@^0.9.3: onigasm "^2.2.5" vscode-textmate "5.2.0" +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" @@ -3534,6 +4421,15 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + sonic-boom@^1.0.2: version "1.4.1" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e" @@ -3594,7 +4490,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4": +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -3612,6 +4508,36 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string.prototype.matchall@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa" + integrity sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + get-intrinsic "^1.1.1" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + regexp.prototype.flags "^1.3.1" + side-channel "^1.0.4" + +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -3657,6 +4583,11 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -3703,6 +4634,17 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +table@^6.0.9: + version "6.8.0" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" + integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA== + dependencies: + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + tar-fs@^2.0.0, tar-fs@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" @@ -3741,6 +4683,11 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + throat@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" @@ -3831,6 +4778,18 @@ ts-node@^10.0.0: make-error "^1.1.1" yn "3.1.1" +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -3838,6 +4797,13 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -3850,6 +4816,11 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" @@ -3894,6 +4865,16 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.10.tgz#a6bd0d28d38f592c3adb6b180ea6e07e1e540a8d" integrity sha512-57H3ACYFXeo1IaZ1w02sfA71wI60MGco/IQFjOqK+WtKoprh7Go2/yvd2HPtoJILO2Or84ncLccI4xoHMTSbGg== +unbox-primitive@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" + integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.1" + has-symbols "^1.0.2" + which-boxed-primitive "^1.0.2" + universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -3904,6 +4885,13 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + utif@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/utif/-/utif-2.0.1.tgz#9e1582d9bbd20011a6588548ed3266298e711759" @@ -3916,6 +4904,11 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +v8-compile-cache@^2.0.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + v8-to-istanbul@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.0.0.tgz#4229f2a99e367f3f018fa1d5c2b8ec684667c69c" @@ -3982,6 +4975,17 @@ whatwg-url@^8.0.0, whatwg-url@^8.5.0: tr46 "^2.1.0" webidl-conversions "^6.1.0" +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -3996,7 +5000,7 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2 || 3 || 4" -word-wrap@~1.2.3: +word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==