feat: functional legacy socket

This commit is contained in:
Adhiraj Singh
2021-12-17 20:58:33 +05:30
parent d8b415a075
commit c803e22e8a
15 changed files with 695 additions and 66 deletions

64
Example/example-legacy.ts Normal file
View File

@@ -0,0 +1,64 @@
import P from "pino"
import { Boom } from "@hapi/boom"
import { makeWALegacySocket, DisconnectReason, AnyMessageContent, delay, useSingleFileLegacyAuthState } from '../src'
const { state, saveState } = useSingleFileLegacyAuthState('./auth_info.json')
// start a connection
const startSock = () => {
const sock = makeWALegacySocket({
logger: P({ level: 'debug' }),
printQRInTerminal: true,
auth: state
})
const sendMessageWTyping = async(msg: AnyMessageContent, jid: string) => {
await sock.presenceSubscribe(jid)
await delay(500)
await sock.sendPresenceUpdate('composing', jid)
await delay(2000)
await sock.sendPresenceUpdate('paused', jid)
await sock.sendWAMessage(jid, msg)
}
sock.ev.on('messages.upsert', async m => {
console.log(JSON.stringify(m, undefined, 2))
const msg = m.messages[0]
if(!msg.key.fromMe && m.type === 'notify') {
console.log('replying to', m.messages[0].key.remoteJid)
await sock!.chatRead(msg.key, 1)
await sendMessageWTyping({ text: 'Hello there!' }, msg.key.remoteJid)
}
})
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('connection.update', (update) => {
const { connection, lastDisconnect } = update
if(connection === 'close') {
// reconnect if not logged out
if((lastDisconnect.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut) {
startSock()
} else {
console.log('connection closed')
}
}
console.log('connection update', update)
})
// listen for when the auth credentials is updated
sock.ev.on('creds.update', saveState)
return sock
}
startSock()

View File

@@ -24,6 +24,7 @@
"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"
},

View File

@@ -18,10 +18,10 @@ 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<any> = {
version: [2, 2146, 9],
version: [2, 2147, 16],
browser: Browsers.baileys('Chrome'),
waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
connectTimeoutMs: 20_000,
keepAliveIntervalMs: 25_000,
logger: P().child({ class: 'baileys' }),
@@ -33,21 +33,13 @@ const BASE_CONNECTION_CONFIG: CommonSocketConfig<any> = {
export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
...BASE_CONNECTION_CONFIG,
waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
getMessage: async() => undefined
}
export const DEFAULT_LEGACY_CONNECTION_CONFIG: LegacySocketConfig = {
version: [2, 2146, 9],
browser: Browsers.baileys('Chrome'),
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: [],
...BASE_CONNECTION_CONFIG,
waWebSocketUrl: 'wss://web.whatsapp.com/ws',
phoneResponseTimeMs: 20_000,
expectResponseTimeout: 60_000,
pendingRequestTimeoutMs: 60_000

View File

@@ -16,7 +16,7 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
} = config
const ev = new EventEmitter() as LegacyBaileysEventEmitter
let authInfo = initialAuthInfo || newLegacyAuthCreds()
const authInfo = initialAuthInfo || newLegacyAuthCreds()
const state: ConnectionState = {
legacy: {
@@ -73,7 +73,6 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
socket?.end(
new Boom('Logged Out', { statusCode: DisconnectReason.loggedOut })
)
authInfo = undefined
}
/** Waits for the connection to WA to open up */
const waitForConnection = async(waitInfinitely: boolean = false) => {
@@ -221,11 +220,13 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
const {user, auth} = validateNewConnection(response[1], authInfo, curveKeys)// validate the connection
const isNewLogin = user.id !== state.legacy!.user?.id
authInfo = auth
Object.assign(authInfo, auth)
updateEncKeys()
logger.info({ user }, 'logged in')
ev.emit('creds.update', auth)
updateState({
connection: 'open',
legacy: {
@@ -235,7 +236,6 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
isNewLogin,
qr: undefined
})
ev.emit('creds.update', auth)
}
ws.once('open', async() => {
try {

View File

@@ -152,7 +152,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
const chats = data.map(({ attrs }): Chat => {
return {
id: jidNormalizedUser(attrs.jid),
conversationTimestamp: +attrs.t,
conversationTimestamp: attrs.t ? +attrs.t : undefined,
unreadCount: +attrs.count,
archive: attrs.archive === 'true' ? true : undefined,
pin: attrs.pin ? +attrs.pin : undefined,
@@ -353,7 +353,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
* @param jid the ID of the person/group who you are updating
* @param type your presence
*/
updatePresence: (jid: string | undefined, type: WAPresence) => (
sendPresenceUpdate: ( type: WAPresence, jid: string | undefined) => (
sendMessage({
binaryTag: [WAMetric.presence, WAFlag[type]], // weird stuff WA does
json: {
@@ -372,7 +372,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
* Request updates on the presence of a user
* this returns nothing, you'll receive updates in chats.update event
* */
requestPresenceUpdate: async (jid: string) => (
presenceSubscribe: async (jid: string) => (
sendMessage({ json: ['action', 'presence', 'subscribe', jid] })
),
/** Query the status of the person (see groupMetadata() for groups) */

View File

@@ -1,14 +1,12 @@
import { LegacySocketConfig } from '../Types'
import { DEFAULT_LEGACY_CONNECTION_CONFIG } from '../Defaults'
import _makeConnection from './groups'
import _makeLegacySocket from './groups'
// export the last socket layer
const makeConnection = (config: Partial<LegacySocketConfig>) => (
_makeConnection({
const makeLegacySocket = (config: Partial<LegacySocketConfig>) => (
_makeLegacySocket({
...DEFAULT_LEGACY_CONNECTION_CONFIG,
...config
})
)
export type Connection = ReturnType<typeof makeConnection>
export default makeConnection
export default makeLegacySocket

View File

@@ -1,10 +1,9 @@
import { BinaryNode, getBinaryNodeMessages, isJidGroup, jidNormalizedUser, areJidsSameUser } from "../WABinary";
import { Boom } from '@hapi/boom'
import { Chat, WAPresence, WAMessageCursor, WAMessage, LegacySocketConfig, WAMessageKey, ParticipantAction, WAMessageStatus, WAMessageStubType, GroupMetadata, AnyMessageContent, MiscMessageGenerationOptions, WAFlag, WAMetric, WAUrlInfo, MediaConnInfo, MessageUpdateType, MessageInfo, MessageInfoUpdate, WAMediaUploadFunction, MediaType, WAMessageUpdate } from "../Types";
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 { DEFAULT_ORIGIN, MEDIA_PATH_MAP, WA_DEFAULT_EPHEMERAL } from "../Defaults";
import got from "got";
import { WA_DEFAULT_EPHEMERAL } from "../Defaults";
import { proto } from "../../WAProto";
const STATUS_MAP = {
@@ -288,8 +287,9 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
.then(() => emitUpdate(finalState))
.catch(() => emitUpdate(WAMessageStatus.ERROR))
}
onMessage(message, 'append')
if(config.emitOwnEvents) {
onMessage(message, 'append')
}
}
// messages received
@@ -362,7 +362,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
const updates = ids.map<MessageInfoUpdate>(id => ({
key: { ...keyPartial, id },
update: {
[updateKey]: { [jidNormalizedUser(attributes.participant)]: new Date(+attributes.t) }
[updateKey]: { [jidNormalizedUser(attributes.participant || attributes.to)]: new Date(+attributes.t) }
}
}))
ev.emit('message-info.update', updates)
@@ -489,7 +489,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
sendWAMessage: async(
jid: string,
content: AnyMessageContent,
options: MiscMessageGenerationOptions & { waitForAck?: boolean }
options: MiscMessageGenerationOptions & { waitForAck?: boolean } = { waitForAck: true }
) => {
const userJid = getState().legacy.user?.id
if(
@@ -521,7 +521,8 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
logger,
userJid: userJid,
getUrlInfo: generateUrlInfo,
upload: waUploadToServer
upload: waUploadToServer,
mediaCache: config.mediaCache
}
)

View File

@@ -2,7 +2,7 @@ import { Boom } from '@hapi/boom'
import { STATUS_CODES } from "http"
import { promisify } from "util"
import WebSocket from "ws"
import { BinaryNode, encodeBinaryNode } from "../WABinary"
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"
@@ -78,7 +78,7 @@ export const makeSocket = ({
if(!authInfo) {
throw new Boom('No encryption/mac keys to encrypt node with', { statusCode: 400 })
}
const binary = encodeBinaryNode(json) // encode the JSON to the WhatsApp binary format
const binary = encodeBinaryNodeLegacy(json) // encode the JSON to the WhatsApp binary format
const buff = aesEncrypt(binary, authInfo.encKey) // encrypt it using AES and our encKey
const sign = hmacSign(buff, authInfo.macKey) // sign the message using HMAC and our macKey
@@ -115,9 +115,9 @@ export const makeSocket = ({
ws.removeAllListeners('ws-close')
}
const onMessageRecieved = (message: string | Buffer) => {
if(message[0] === '!') {
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 ('utf-8')
const timestamp = message.slice(1, message.length).toString()
lastDateRecv = new Date(parseInt(timestamp))
ws.emit('received-pong')
} else {
@@ -142,9 +142,9 @@ export const makeSocket = ({
/* 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.header || json[0] || ''
const l1 = json?.attributes || json?.[1] || { }
const l2 = json?.data?.[0]?.header || json[2]?.[0] || ''
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

View File

@@ -1,10 +1,9 @@
import { Boom } from "@hapi/boom"
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, DEFAULT_ORIGIN, MEDIA_PATH_MAP } from "../Defaults"
import { WA_DEFAULT_EPHEMERAL } from "../Defaults"
import { makeGroupsSocket } from "./groups"
import NodeCache from "node-cache"

View File

@@ -1,7 +1,8 @@
import { Boom } from '@hapi/boom'
import { randomBytes } from 'crypto'
import { decodeBinaryNode, jidNormalizedUser } from "../WABinary"
import { decodeBinaryNodeLegacy, jidNormalizedUser } from "../WABinary"
import { aesDecrypt, hmacSign, hkdf, Curve } from "./crypto"
import { BufferJSON } from './generics'
import { DisconnectReason, WATag, LegacyAuthenticationCreds, CurveKeyPair, Contact } from "../Types"
export const newLegacyAuthCreds = () => ({
@@ -9,11 +10,10 @@ export const newLegacyAuthCreds = () => ({
}) as LegacyAuthenticationCreds
export const decodeWAMessage = (
message: string | Buffer,
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
@@ -25,10 +25,12 @@ export const decodeWAMessage = (
const messageTag: string = message.slice(0, commaIndex).toString()
let json: any
let tags: WATag
if (data.length > 0) {
if (typeof data === 'string') {
json = JSON.parse(data) // parse the JSON
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 {
const { macKey, encKey } = auth || {}
if (!macKey || !encKey) {
throw new Boom('recieved encrypted buffer when auth creds unavailable', { data: message, statusCode: DisconnectReason.badSession })
@@ -49,7 +51,7 @@ export const decodeWAMessage = (
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 = decodeBinaryNode(decrypted) // decode the binary message into a JSON array
json = decodeBinaryNodeLegacy(decrypted, { index: 0 }) // decode the binary message into a JSON array
} else {
throw new Boom('Bad checksum', {
data: {
@@ -138,5 +140,34 @@ export const validateNewConnection = (
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
const { readFileSync, writeFileSync, existsSync } = require('fs')
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()
}
return {
state,
saveState: () => {
const str = JSON.stringify(state, BufferJSON.replacer, 2)
writeFileSync(file, str)
}
}
}

View File

@@ -0,0 +1,198 @@
export const Tags = {
LIST_EMPTY: 0,
STREAM_END: 2,
DICTIONARY_0: 236,
DICTIONARY_1: 237,
DICTIONARY_2: 238,
DICTIONARY_3: 239,
LIST_8: 248,
LIST_16: 249,
JID_PAIR: 250,
HEX_8: 251,
BINARY_8: 252,
BINARY_20: 253,
BINARY_32: 254,
NIBBLE_8: 255,
SINGLE_BYTE_MAX: 256,
PACKED_MAX: 254,
}
export const DoubleByteTokens = []
export const SingleByteTokens = [
null,
null,
null,
'200',
'400',
'404',
'500',
'501',
'502',
'action',
'add',
'after',
'archive',
'author',
'available',
'battery',
'before',
'body',
'broadcast',
'chat',
'clear',
'code',
'composing',
'contacts',
'count',
'create',
'debug',
'delete',
'demote',
'duplicate',
'encoding',
'error',
'false',
'filehash',
'from',
'g.us',
'group',
'groups_v2',
'height',
'id',
'image',
'in',
'index',
'invis',
'item',
'jid',
'kind',
'last',
'leave',
'live',
'log',
'media',
'message',
'mimetype',
'missing',
'modify',
'name',
'notification',
'notify',
'out',
'owner',
'participant',
'paused',
'picture',
'played',
'presence',
'preview',
'promote',
'query',
'raw',
'read',
'receipt',
'received',
'recipient',
'recording',
'relay',
'remove',
'response',
'resume',
'retry',
's.whatsapp.net',
'seconds',
'set',
'size',
'status',
'subject',
'subscribe',
't',
'text',
'to',
'true',
'type',
'unarchive',
'unavailable',
'url',
'user',
'value',
'web',
'width',
'mute',
'read_only',
'admin',
'creator',
'short',
'update',
'powersave',
'checksum',
'epoch',
'block',
'previous',
'409',
'replaced',
'reason',
'spam',
'modify_tag',
'message_info',
'delivery',
'emoji',
'title',
'description',
'canonical-url',
'matched-text',
'star',
'unstar',
'media_key',
'filename',
'identity',
'unread',
'page',
'page_count',
'search',
'media_message',
'security',
'call_log',
'profile',
'ciphertext',
'invite',
'gif',
'vcard',
'frequent',
'privacy',
'blacklist',
'whitelist',
'verify',
'location',
'document',
'elapsed',
'revoke_invite',
'expiration',
'unsubscribe',
'disable',
'vname',
'old_jid',
'new_jid',
'announcement',
'locked',
'prop',
'label',
'color',
'call',
'offer',
'call-id',
'quick_reply',
'sticker',
'pay_t',
'accept',
'reject',
'sticker_pack',
'invalid',
'canceled',
'missed',
'connected',
'result',
'audio',
'video',
'recent',
]

View File

@@ -0,0 +1,337 @@
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
}
}
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)
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
const attributesLength = (listSize - 1) >> 1
for (let i = 0; i < attributesLength; i++) {
const key = readString(readByte())
const b = readByte()
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
}
data = decoded
}
}
return {
tag: header,
attrs,
content: data
}
}
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[]) => (
bytes.forEach (b => buffer.push(b))
)
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)
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 validAttributes = Object.keys(attrs).filter(k => (
typeof attrs[k] !== 'undefined' && attrs[k] !== null
))
writeListStart(2*validAttributes.length + 1 + (typeof content !== 'undefined' && content !== null ? 1 : 0))
writeString(tag)
validAttributes.forEach((key) => {
if(typeof attrs[key] === 'string') {
writeString(key)
writeString(attrs[key])
}
})
if (typeof content === 'string') {
writeString(content, true)
} else if (Buffer.isBuffer(content)) {
writeByteLength(content.length)
pushBytes(content)
} else if (Array.isArray(content)) {
writeListStart(content.length)
for(const item of content) {
if(item) encode(item, buffer)
}
} else if(typeof content === 'undefined' || content === null) {
} else {
throw new Error(`invalid children for header "${tag}": ${content} (${typeof content})`)
}
return Buffer.from(buffer)
}
export const encodeBinaryNodeLegacy = encode
export const decodeBinaryNodeLegacy = decode

View File

@@ -3,6 +3,7 @@ import { jidDecode, jidEncode } from './jid-utils';
import { Binary, numUtf8Bytes } from '../../WABinary/Binary';
import { Boom } from '@hapi/boom';
import { proto } from '../../WAProto';
import { BinaryNode } from './types';
const LIST1 = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '.', '<27>', '<27>', '<27>', '<27>'];
const LIST2 = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
@@ -210,20 +211,6 @@ function bufferToUInt(e: Uint8Array | Buffer, t: number) {
for (let i = 0; i < t; i++) a = 256 * a + e[i]
return a
}
/**
* the binary node WA uses internally for communication
*
* this is manipulated soley as an object and it does not have any functions.
* This is done for easy serialization, to prevent running into issues with prototypes &
* to maintain functional code structure
* */
export type BinaryNode = {
tag: string
attrs: { [key: string]: string }
content?: BinaryNode[] | string | Uint8Array
}
export type BinaryNodeAttributes = BinaryNode['attrs']
export type BinaryNodeData = BinaryNode['content']
export const decodeBinaryNode = (data: Binary): BinaryNode => {
//U
@@ -333,4 +320,6 @@ export const getBinaryNodeMessages = ({ content }: BinaryNode) => {
}
export * from './jid-utils'
export { Binary } from '../../WABinary/Binary'
export { Binary } from '../../WABinary/Binary'
export * from './types'
export * from './Legacy'

14
src/WABinary/types.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* the binary node WA uses internally for communication
*
* this is manipulated soley as an object and it does not have any functions.
* This is done for easy serialization, to prevent running into issues with prototypes &
* to maintain functional code structure
* */
export type BinaryNode = {
tag: string
attrs: { [key: string]: string }
content?: BinaryNode[] | string | Uint8Array
}
export type BinaryNodeAttributes = BinaryNode['attrs']
export type BinaryNodeData = BinaryNode['content']

View File

@@ -1,4 +1,5 @@
import makeWASocket from './Socket'
import makeWALegacySocket from './LegacySocket'
export * from '../WAProto'
export * from './Utils'
@@ -7,6 +8,10 @@ export * from './Types'
export * from './Defaults'
export * from './WABinary'
export type WALegacySocket = ReturnType<typeof makeWALegacySocket>
export { makeWALegacySocket }
export type WASocket = ReturnType<typeof makeWASocket>
export default makeWASocket