mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
Merge branch 'master' into pr/2472
This commit is contained in:
@@ -4,4 +4,5 @@ coverage
|
|||||||
*.lock
|
*.lock
|
||||||
.eslintrc.json
|
.eslintrc.json
|
||||||
src/WABinary/index.ts
|
src/WABinary/index.ts
|
||||||
WAProto
|
WAProto
|
||||||
|
WASignalGroup
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
import makeWASocket, { AnyMessageContent, delay, DisconnectReason, fetchLatestBaileysVersion, isJidBroadcast, makeCacheableSignalKeyStore, makeInMemoryStore, MessageRetryMap, useMultiFileAuthState } from '../src'
|
import NodeCache from 'node-cache'
|
||||||
|
import makeWASocket, { AnyMessageContent, delay, DisconnectReason, fetchLatestBaileysVersion, makeCacheableSignalKeyStore, makeInMemoryStore, useMultiFileAuthState } from '../src'
|
||||||
import MAIN_LOGGER from '../src/Utils/logger'
|
import MAIN_LOGGER from '../src/Utils/logger'
|
||||||
|
|
||||||
const logger = MAIN_LOGGER.child({ })
|
const logger = MAIN_LOGGER.child({ })
|
||||||
@@ -10,7 +11,7 @@ const doReplies = !process.argv.includes('--no-reply')
|
|||||||
|
|
||||||
// external map to store retry counts of messages when decryption/encryption fails
|
// external map to store retry counts of messages when decryption/encryption fails
|
||||||
// keep this out of the socket itself, so as to prevent a message decryption/encryption loop across socket restarts
|
// keep this out of the socket itself, so as to prevent a message decryption/encryption loop across socket restarts
|
||||||
const msgRetryCounterMap: MessageRetryMap = { }
|
const msgRetryCounterCache = new NodeCache()
|
||||||
|
|
||||||
// the store maintains the data of the WA connection in memory
|
// the store maintains the data of the WA connection in memory
|
||||||
// can be written out to a file & read from it
|
// can be written out to a file & read from it
|
||||||
@@ -37,11 +38,11 @@ const startSock = async() => {
|
|||||||
/** caching makes the store faster to send/recv messages */
|
/** caching makes the store faster to send/recv messages */
|
||||||
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
||||||
},
|
},
|
||||||
msgRetryCounterMap,
|
msgRetryCounterCache,
|
||||||
generateHighQualityLinkPreview: true,
|
generateHighQualityLinkPreview: true,
|
||||||
// ignore all broadcast messages -- to receive the same
|
// ignore all broadcast messages -- to receive the same
|
||||||
// comment the line below out
|
// comment the line below out
|
||||||
shouldIgnoreJid: jid => isJidBroadcast(jid),
|
// shouldIgnoreJid: jid => isJidBroadcast(jid),
|
||||||
// implement to handle retries
|
// implement to handle retries
|
||||||
getMessage: async key => {
|
getMessage: async key => {
|
||||||
if(store) {
|
if(store) {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"roots": [
|
'roots': [
|
||||||
"<rootDir>/src"
|
'<rootDir>/src'
|
||||||
],
|
],
|
||||||
"testMatch": [
|
'testMatch': [
|
||||||
"**/Tests/test.*.+(ts|tsx|js)",
|
'**/Tests/test.*.+(ts|tsx|js)',
|
||||||
],
|
],
|
||||||
"transform": {
|
'transform': {
|
||||||
"^.+\\.(ts|tsx)$": "ts-jest"
|
'^.+\\.(ts|tsx)$': 'ts-jest'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
"build:tsc": "tsc",
|
"build:tsc": "tsc",
|
||||||
"example": "node --inspect -r ts-node/register Example/example.ts",
|
"example": "node --inspect -r ts-node/register Example/example.ts",
|
||||||
"gen:protobuf": "sh WAProto/GenerateStatics.sh",
|
"gen:protobuf": "sh WAProto/GenerateStatics.sh",
|
||||||
"lint": "eslint ./src --ext .js,.ts,.jsx,.tsx",
|
"lint": "eslint . --ext .js,.ts,.jsx,.tsx",
|
||||||
"lint:fix": "eslint ./src --fix --ext .js,.ts,.jsx,.tsx"
|
"lint:fix": "eslint . --fix --ext .js,.ts,.jsx,.tsx"
|
||||||
},
|
},
|
||||||
"author": "Adhiraj Singh",
|
"author": "Adhiraj Singh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/boom": "^9.1.3",
|
"@hapi/boom": "^9.1.3",
|
||||||
"axios": "^0.24.0",
|
"axios": "^1.3.3",
|
||||||
"futoin-hkdf": "^1.5.1",
|
"futoin-hkdf": "^1.5.1",
|
||||||
"libsignal": "git+https://github.com/adiwajshing/libsignal-node",
|
"libsignal": "git+https://github.com/adiwajshing/libsignal-node",
|
||||||
"music-metadata": "^7.12.3",
|
"music-metadata": "^7.12.3",
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"version": [2, 2243, 7]
|
"version": [2, 2308, 7]
|
||||||
}
|
}
|
||||||
@@ -99,4 +99,11 @@ export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[]
|
|||||||
|
|
||||||
export const MIN_PREKEY_COUNT = 5
|
export const MIN_PREKEY_COUNT = 5
|
||||||
|
|
||||||
export const INITIAL_PREKEY_COUNT = 30
|
export const INITIAL_PREKEY_COUNT = 30
|
||||||
|
|
||||||
|
export const DEFAULT_CACHE_TTLS = {
|
||||||
|
SIGNAL_STORE: 5 * 60, // 5 minutes
|
||||||
|
MSG_RETRY: 60 * 60, // 1 hour
|
||||||
|
CALL_OFFER: 5 * 60, // 5 minutes
|
||||||
|
USER_DEVICES: 5 * 60, // 5 minutes
|
||||||
|
}
|
||||||
@@ -339,7 +339,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
name,
|
name,
|
||||||
version: state.version.toString(),
|
version: state.version.toString(),
|
||||||
// return snapshot if being synced from scratch
|
// return snapshot if being synced from scratch
|
||||||
return_snapshot: (!state.version).toString()
|
'return_snapshot': (!state.version).toString()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
|
import NodeCache from 'node-cache'
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults'
|
import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults'
|
||||||
import { MessageReceiptType, MessageRelayOptions, MessageUserReceipt, SocketConfig, WACallEvent, WAMessageKey, WAMessageStubType, WAPatchName } from '../Types'
|
import { MessageReceiptType, MessageRelayOptions, MessageUserReceipt, SocketConfig, WACallEvent, WAMessageKey, WAMessageStubType, WAPatchName } from '../Types'
|
||||||
import { decodeMediaRetryNode, decodeMessageStanza, delay, encodeBigEndian, encodeSignedDeviceIdentity, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils'
|
import { decodeMediaRetryNode, decryptMessageNode, delay, encodeBigEndian, encodeSignedDeviceIdentity, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils'
|
||||||
import { makeMutex } from '../Utils/make-mutex'
|
import { makeMutex } from '../Utils/make-mutex'
|
||||||
import { cleanMessage } from '../Utils/process-message'
|
import { cleanMessage } from '../Utils/process-message'
|
||||||
import { areJidsSameUser, BinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, isJidUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary'
|
import { areJidsSameUser, BinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, isJidUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary'
|
||||||
@@ -36,8 +37,14 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
/** this mutex ensures that each retryRequest will wait for the previous one to finish */
|
/** this mutex ensures that each retryRequest will wait for the previous one to finish */
|
||||||
const retryMutex = makeMutex()
|
const retryMutex = makeMutex()
|
||||||
|
|
||||||
const msgRetryMap = config.msgRetryCounterMap || { }
|
const msgRetryCache = config.msgRetryCounterCache || new NodeCache({
|
||||||
const callOfferData: { [id: string]: WACallEvent } = { }
|
stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
|
||||||
|
useClones: false
|
||||||
|
})
|
||||||
|
const callOfferCache = config.callOfferCache || new NodeCache({
|
||||||
|
stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
|
||||||
|
useClones: false
|
||||||
|
})
|
||||||
|
|
||||||
let sendActiveReceipts = false
|
let sendActiveReceipts = false
|
||||||
|
|
||||||
@@ -90,15 +97,15 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
const sendRetryRequest = async(node: BinaryNode, forceIncludeKeys = false) => {
|
const sendRetryRequest = async(node: BinaryNode, forceIncludeKeys = false) => {
|
||||||
const msgId = node.attrs.id
|
const msgId = node.attrs.id
|
||||||
|
|
||||||
let retryCount = msgRetryMap[msgId] || 0
|
let retryCount = msgRetryCache.get<number>(msgId) || 0
|
||||||
if(retryCount >= 5) {
|
if(retryCount >= 5) {
|
||||||
logger.debug({ retryCount, msgId }, 'reached retry limit, clearing')
|
logger.debug({ retryCount, msgId }, 'reached retry limit, clearing')
|
||||||
delete msgRetryMap[msgId]
|
msgRetryCache.del(msgId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
retryCount += 1
|
retryCount += 1
|
||||||
msgRetryMap[msgId] = retryCount
|
msgRetryCache.set(msgId, retryCount)
|
||||||
|
|
||||||
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
|
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
|
||||||
|
|
||||||
@@ -362,13 +369,14 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
const willSendMessageAgain = (id: string, participant: string) => {
|
const willSendMessageAgain = (id: string, participant: string) => {
|
||||||
const key = `${id}:${participant}`
|
const key = `${id}:${participant}`
|
||||||
const retryCount = msgRetryMap[key] || 0
|
const retryCount = msgRetryCache.get<number>(key) || 0
|
||||||
return retryCount < 5
|
return retryCount < 5
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSendMessageAgainCount = (id: string, participant: string) => {
|
const updateSendMessageAgainCount = (id: string, participant: string) => {
|
||||||
const key = `${id}:${participant}`
|
const key = `${id}:${participant}`
|
||||||
msgRetryMap[key] = (msgRetryMap[key] || 0) + 1
|
const newValue = (msgRetryCache.get<number>(key) || 0) + 1
|
||||||
|
msgRetryCache.set(key, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendMessagesAgain = async(
|
const sendMessagesAgain = async(
|
||||||
@@ -535,7 +543,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMessage = async(node: BinaryNode) => {
|
const handleMessage = async(node: BinaryNode) => {
|
||||||
const { fullMessage: msg, category, author, decrypt } = decodeMessageStanza(node, authState)
|
const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState)
|
||||||
if(shouldIgnoreJid(msg.key.remoteJid!)) {
|
if(shouldIgnoreJid(msg.key.remoteJid!)) {
|
||||||
logger.debug({ key: msg.key }, 'ignored message')
|
logger.debug({ key: msg.key }, 'ignored message')
|
||||||
await sendMessageAck(node)
|
await sendMessageAck(node)
|
||||||
@@ -618,18 +626,20 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
if(status === 'offer') {
|
if(status === 'offer') {
|
||||||
call.isVideo = !!getBinaryNodeChild(infoChild, 'video')
|
call.isVideo = !!getBinaryNodeChild(infoChild, 'video')
|
||||||
call.isGroup = infoChild.attrs.type === 'group'
|
call.isGroup = infoChild.attrs.type === 'group'
|
||||||
callOfferData[call.id] = call
|
callOfferCache.set(call.id, call)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingCall = callOfferCache.get<WACallEvent>(call.id)
|
||||||
|
|
||||||
// use existing call info to populate this event
|
// use existing call info to populate this event
|
||||||
if(callOfferData[call.id]) {
|
if(existingCall) {
|
||||||
call.isVideo = callOfferData[call.id].isVideo
|
call.isVideo = existingCall.isVideo
|
||||||
call.isGroup = callOfferData[call.id].isGroup
|
call.isGroup = existingCall.isGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete data once call has ended
|
// delete data once call has ended
|
||||||
if(status === 'reject' || status === 'accept' || status === 'timeout') {
|
if(status === 'reject' || status === 'accept' || status === 'timeout') {
|
||||||
delete callOfferData[call.id]
|
callOfferCache.del(call.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.emit('call', [call])
|
ev.emit('call', [call])
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
import NodeCache from 'node-cache'
|
import NodeCache from 'node-cache'
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { WA_DEFAULT_EPHEMERAL } from '../Defaults'
|
import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults'
|
||||||
import { AnyMessageContent, MediaConnInfo, MessageReceiptType, MessageRelayOptions, MiscMessageGenerationOptions, SocketConfig, WAMessageKey } from '../Types'
|
import { AnyMessageContent, MediaConnInfo, MessageReceiptType, MessageRelayOptions, MiscMessageGenerationOptions, SocketConfig, WAMessageKey } from '../Types'
|
||||||
import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, encryptSenderKeyMsgSignalProto, encryptSignalProto, extractDeviceJids, generateMessageID, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, jidToSignalProtocolAddress, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils'
|
import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, encryptSenderKeyMsgSignalProto, encryptSignalProto, extractDeviceJids, generateMessageID, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, jidToSignalProtocolAddress, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils'
|
||||||
import { getUrlInfo } from '../Utils/link-preview'
|
import { getUrlInfo } from '../Utils/link-preview'
|
||||||
@@ -32,7 +32,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
} = sock
|
} = sock
|
||||||
|
|
||||||
const userDevicesCache = config.userDevicesCache || new NodeCache({
|
const userDevicesCache = config.userDevicesCache || new NodeCache({
|
||||||
stdTTL: 300, // 5 minutes
|
stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, // 5 minutes
|
||||||
useClones: false
|
useClones: false
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -53,7 +53,10 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
const mediaConnNode = getBinaryNodeChild(result, 'media_conn')
|
const mediaConnNode = getBinaryNodeChild(result, 'media_conn')
|
||||||
const node: MediaConnInfo = {
|
const node: MediaConnInfo = {
|
||||||
hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(
|
hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(
|
||||||
item => item.attrs as any
|
({ attrs }) => ({
|
||||||
|
hostname: attrs.hostname,
|
||||||
|
maxContentLengthBytes: +attrs.maxContentLengthBytes,
|
||||||
|
})
|
||||||
),
|
),
|
||||||
auth: mediaConnNode!.attrs.auth,
|
auth: mediaConnNode!.attrs.auth,
|
||||||
ttl: +mediaConnNode!.attrs.ttl,
|
ttl: +mediaConnNode!.attrs.ttl,
|
||||||
@@ -144,8 +147,9 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
for(let jid of jids) {
|
for(let jid of jids) {
|
||||||
const user = jidDecode(jid)?.user
|
const user = jidDecode(jid)?.user
|
||||||
jid = jidNormalizedUser(jid)
|
jid = jidNormalizedUser(jid)
|
||||||
if(userDevicesCache.has(user!) && useCache) {
|
|
||||||
const devices = userDevicesCache.get<JidWithDevice[]>(user!)!
|
const devices = userDevicesCache.get<JidWithDevice[]>(user!)
|
||||||
|
if(devices && useCache) {
|
||||||
deviceResults.push(...devices)
|
deviceResults.push(...devices)
|
||||||
|
|
||||||
logger.trace({ user }, 'using cache for devices')
|
logger.trace({ user }, 'using cache for devices')
|
||||||
@@ -319,7 +323,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
// only send to the specific device that asked for a retry
|
// only send to the specific device that asked for a retry
|
||||||
// otherwise the message is sent out to every device that should be a recipient
|
// otherwise the message is sent out to every device that should be a recipient
|
||||||
if(!isGroup) {
|
if(!isGroup) {
|
||||||
additionalAttributes = { ...additionalAttributes, device_fanout: 'false' }
|
additionalAttributes = { ...additionalAttributes, 'device_fanout': 'false' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user, device } = jidDecode(participant.jid)!
|
const { user, device } = jidDecode(participant.jid)!
|
||||||
@@ -636,6 +640,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
),
|
),
|
||||||
upload: waUploadToServer,
|
upload: waUploadToServer,
|
||||||
mediaCache: config.mediaCache,
|
mediaCache: config.mediaCache,
|
||||||
|
options: config.options,
|
||||||
...options,
|
...options,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,12 +32,13 @@ export const makeSocket = ({
|
|||||||
}: SocketConfig) => {
|
}: SocketConfig) => {
|
||||||
const ws = new WebSocket(waWebSocketUrl, undefined, {
|
const ws = new WebSocket(waWebSocketUrl, undefined, {
|
||||||
origin: DEFAULT_ORIGIN,
|
origin: DEFAULT_ORIGIN,
|
||||||
headers: options.headers,
|
headers: options.headers as {},
|
||||||
handshakeTimeout: connectTimeoutMs,
|
handshakeTimeout: connectTimeoutMs,
|
||||||
timeout: connectTimeoutMs,
|
timeout: connectTimeoutMs,
|
||||||
agent
|
agent
|
||||||
})
|
})
|
||||||
ws.setMaxListeners(0)
|
ws.setMaxListeners(0)
|
||||||
|
|
||||||
const ev = makeEventBuffer(logger)
|
const ev = makeEventBuffer(logger)
|
||||||
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
||||||
const ephemeralKeyPair = Curve.generateKeyPair()
|
const ephemeralKeyPair = Curve.generateKeyPair()
|
||||||
@@ -65,7 +66,17 @@ export const makeSocket = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bytes = noise.encodeFrame(data)
|
const bytes = noise.encodeFrame(data)
|
||||||
await sendPromise.call(ws, bytes) as Promise<void>
|
await promiseTimeout<void>(
|
||||||
|
connectTimeoutMs,
|
||||||
|
async(resolve, reject) => {
|
||||||
|
try {
|
||||||
|
await sendPromise.call(ws, bytes)
|
||||||
|
resolve()
|
||||||
|
} catch(error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** send a binary node */
|
/** send a binary node */
|
||||||
@@ -97,7 +108,7 @@ export const makeSocket = ({
|
|||||||
|
|
||||||
const result = promiseTimeout<any>(connectTimeoutMs, (resolve, reject) => {
|
const result = promiseTimeout<any>(connectTimeoutMs, (resolve, reject) => {
|
||||||
onOpen = (data: any) => resolve(data)
|
onOpen = (data: any) => resolve(data)
|
||||||
onClose = reject
|
onClose = mapWebSocketError(reject)
|
||||||
ws.on('frame', onOpen)
|
ws.on('frame', onOpen)
|
||||||
ws.on('close', onClose)
|
ws.on('close', onClose)
|
||||||
ws.on('error', onClose)
|
ws.on('error', onClose)
|
||||||
@@ -335,7 +346,7 @@ export const makeSocket = ({
|
|||||||
let onClose: (err: Error) => void
|
let onClose: (err: Error) => void
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
onOpen = () => resolve(undefined)
|
onOpen = () => resolve(undefined)
|
||||||
onClose = reject
|
onClose = mapWebSocketError(reject)
|
||||||
ws.on('open', onOpen)
|
ws.on('open', onOpen)
|
||||||
ws.on('close', onClose)
|
ws.on('close', onClose)
|
||||||
ws.on('error', onClose)
|
ws.on('error', onClose)
|
||||||
@@ -433,12 +444,7 @@ export const makeSocket = ({
|
|||||||
end(err)
|
end(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
ws.on('error', error => end(
|
ws.on('error', mapWebSocketError(end))
|
||||||
new Boom(
|
|
||||||
`WebSocket Error (${error.message})`,
|
|
||||||
{ statusCode: getCodeFromWSError(error), data: error }
|
|
||||||
)
|
|
||||||
))
|
|
||||||
ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })))
|
ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })))
|
||||||
// the server terminated the connection
|
// the server terminated the connection
|
||||||
ws.on('CB:xmlstreamend', () => end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })))
|
ws.on('CB:xmlstreamend', () => end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })))
|
||||||
@@ -604,4 +610,19 @@ export const makeSocket = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* map the websocket error to the right type
|
||||||
|
* so it can be retried by the caller
|
||||||
|
* */
|
||||||
|
function mapWebSocketError(handler: (err: Error) => void) {
|
||||||
|
return (error: Error) => {
|
||||||
|
handler(
|
||||||
|
new Boom(
|
||||||
|
`WebSocket Error (${error.message})`,
|
||||||
|
{ statusCode: getCodeFromWSError(error), data: error }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type Socket = ReturnType<typeof makeSocket>
|
export type Socket = ReturnType<typeof makeSocket>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AxiosRequestConfig } from 'axios'
|
||||||
import type NodeCache from 'node-cache'
|
import type NodeCache from 'node-cache'
|
||||||
import type { Logger } from 'pino'
|
import type { Logger } from 'pino'
|
||||||
import type { Readable } from 'stream'
|
import type { Readable } from 'stream'
|
||||||
@@ -5,6 +6,7 @@ import type { URL } from 'url'
|
|||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { MEDIA_HKDF_KEY_MAPPING } from '../Defaults'
|
import { MEDIA_HKDF_KEY_MAPPING } from '../Defaults'
|
||||||
import type { GroupMetadata } from './GroupMetadata'
|
import type { GroupMetadata } from './GroupMetadata'
|
||||||
|
import { CacheStore } from './Socket'
|
||||||
|
|
||||||
// export the WAMessage Prototypes
|
// export the WAMessage Prototypes
|
||||||
export { proto as WAProto }
|
export { proto as WAProto }
|
||||||
@@ -210,9 +212,11 @@ export type MediaGenerationOptions = {
|
|||||||
mediaTypeOverride?: MediaType
|
mediaTypeOverride?: MediaType
|
||||||
upload: WAMediaUploadFunction
|
upload: WAMediaUploadFunction
|
||||||
/** cache media so it does not have to be uploaded again */
|
/** cache media so it does not have to be uploaded again */
|
||||||
mediaCache?: NodeCache
|
mediaCache?: CacheStore
|
||||||
|
|
||||||
mediaUploadTimeoutMs?: number
|
mediaUploadTimeoutMs?: number
|
||||||
|
|
||||||
|
options?: AxiosRequestConfig
|
||||||
}
|
}
|
||||||
export type MessageContentGenerationOptions = MediaGenerationOptions & {
|
export type MessageContentGenerationOptions = MediaGenerationOptions & {
|
||||||
getUrlInfo?: (text: string) => Promise<WAUrlInfo | undefined>
|
getUrlInfo?: (text: string) => Promise<WAUrlInfo | undefined>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import { AxiosRequestConfig } from 'axios'
|
import { AxiosRequestConfig } from 'axios'
|
||||||
import type { Agent } from 'https'
|
import type { Agent } from 'https'
|
||||||
import type NodeCache from 'node-cache'
|
|
||||||
import type { Logger } from 'pino'
|
import type { Logger } from 'pino'
|
||||||
import type { URL } from 'url'
|
import type { URL } from 'url'
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
@@ -11,33 +10,40 @@ import { MediaConnInfo } from './Message'
|
|||||||
export type WAVersion = [number, number, number]
|
export type WAVersion = [number, number, number]
|
||||||
export type WABrowserDescription = [string, string, string]
|
export type WABrowserDescription = [string, string, string]
|
||||||
|
|
||||||
export type MessageRetryMap = { [msgId: string]: number }
|
export type CacheStore = {
|
||||||
|
/** get a cached key and change the stats */
|
||||||
|
get<T>(key: string): T | undefined
|
||||||
|
/** set a key in the cache */
|
||||||
|
set<T>(key: string, value: T): void
|
||||||
|
/** delete a key from the cache */
|
||||||
|
del(key: string): void
|
||||||
|
/** flush all data */
|
||||||
|
flushAll(): void
|
||||||
|
}
|
||||||
|
|
||||||
export type SocketConfig = {
|
export type SocketConfig = {
|
||||||
/** the WS url to connect to WA */
|
/** the WS url to connect to WA */
|
||||||
waWebSocketUrl: string | URL
|
waWebSocketUrl: string | URL
|
||||||
/** Fails the connection if the socket times out in this interval */
|
/** Fails the connection if the socket times out in this interval */
|
||||||
connectTimeoutMs: number
|
connectTimeoutMs: number
|
||||||
/** Default timeout for queries, undefined for no timeout */
|
/** Default timeout for queries, undefined for no timeout */
|
||||||
defaultQueryTimeoutMs: number | undefined
|
defaultQueryTimeoutMs: number | undefined
|
||||||
/** ping-pong interval for WS connection */
|
/** ping-pong interval for WS connection */
|
||||||
keepAliveIntervalMs: number
|
keepAliveIntervalMs: number
|
||||||
/** proxy agent */
|
/** proxy agent */
|
||||||
agent?: Agent
|
agent?: Agent
|
||||||
/** pino logger */
|
/** pino logger */
|
||||||
logger: Logger
|
logger: Logger
|
||||||
/** version to connect with */
|
/** version to connect with */
|
||||||
version: WAVersion
|
version: WAVersion
|
||||||
/** override browser config */
|
/** override browser config */
|
||||||
browser: WABrowserDescription
|
browser: WABrowserDescription
|
||||||
/** agent used for fetch requests -- uploading/downloading media */
|
/** agent used for fetch requests -- uploading/downloading media */
|
||||||
fetchAgent?: Agent
|
fetchAgent?: Agent
|
||||||
/** should the QR be printed in the terminal */
|
/** should the QR be printed in the terminal */
|
||||||
printQRInTerminal: boolean
|
printQRInTerminal: boolean
|
||||||
/** should events be emitted for actions done by this socket connection */
|
/** should events be emitted for actions done by this socket connection */
|
||||||
emitOwnEvents: boolean
|
emitOwnEvents: boolean
|
||||||
/** provide a cache to store media, so does not have to be re-uploaded */
|
|
||||||
mediaCache?: NodeCache
|
|
||||||
/** custom upload hosts to upload media to */
|
/** custom upload hosts to upload media to */
|
||||||
customUploadHosts: MediaConnInfo['hosts']
|
customUploadHosts: MediaConnInfo['hosts']
|
||||||
/** time to wait between sending new retry requests */
|
/** time to wait between sending new retry requests */
|
||||||
@@ -50,14 +56,19 @@ export type SocketConfig = {
|
|||||||
shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => boolean
|
shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => boolean
|
||||||
/** transaction capability options for SignalKeyStore */
|
/** transaction capability options for SignalKeyStore */
|
||||||
transactionOpts: TransactionCapabilityOptions
|
transactionOpts: TransactionCapabilityOptions
|
||||||
/** provide a cache to store a user's device list */
|
|
||||||
userDevicesCache?: NodeCache
|
|
||||||
/** marks the client as online whenever the socket successfully connects */
|
/** marks the client as online whenever the socket successfully connects */
|
||||||
markOnlineOnConnect: boolean
|
markOnlineOnConnect: boolean
|
||||||
|
|
||||||
|
/** provide a cache to store media, so does not have to be re-uploaded */
|
||||||
|
mediaCache?: CacheStore
|
||||||
/**
|
/**
|
||||||
* map to store the retry counts for failed messages;
|
* map to store the retry counts for failed messages;
|
||||||
* used to determine whether to retry a message or not */
|
* used to determine whether to retry a message or not */
|
||||||
msgRetryCounterMap?: MessageRetryMap
|
msgRetryCounterCache?: CacheStore
|
||||||
|
/** provide a cache to store a user's device list */
|
||||||
|
userDevicesCache?: CacheStore
|
||||||
|
/** cache to store call offers */
|
||||||
|
callOfferCache?: CacheStore
|
||||||
/** width for link preview images */
|
/** width for link preview images */
|
||||||
linkPreviewImageThumbnailWidth: number
|
linkPreviewImageThumbnailWidth: number
|
||||||
/** Should Baileys ask the phone for full history, will be received async */
|
/** Should Baileys ask the phone for full history, will be received async */
|
||||||
@@ -92,7 +103,7 @@ export type SocketConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** options for axios */
|
/** options for axios */
|
||||||
options: AxiosRequestConfig<any>
|
options: AxiosRequestConfig<{}>
|
||||||
/**
|
/**
|
||||||
* fetch a message from your store
|
* fetch a message from your store
|
||||||
* implement this so that messages failed to send (solves the "this message can take a while" issue) can be retried
|
* implement this so that messages failed to send (solves the "this message can take a while" issue) can be retried
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
import NodeCache from 'node-cache'
|
import NodeCache from 'node-cache'
|
||||||
import type { Logger } from 'pino'
|
import type { Logger } from 'pino'
|
||||||
import type { AuthenticationCreds, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types'
|
import { DEFAULT_CACHE_TTLS } from '../Defaults'
|
||||||
|
import type { AuthenticationCreds, CacheStore, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types'
|
||||||
import { Curve, signedKeyPair } from './crypto'
|
import { Curve, signedKeyPair } from './crypto'
|
||||||
import { delay, generateRegistrationId } from './generics'
|
import { delay, generateRegistrationId } from './generics'
|
||||||
|
|
||||||
@@ -9,16 +10,17 @@ import { delay, generateRegistrationId } from './generics'
|
|||||||
* Adds caching capability to a SignalKeyStore
|
* Adds caching capability to a SignalKeyStore
|
||||||
* @param store the store to add caching to
|
* @param store the store to add caching to
|
||||||
* @param logger to log trace events
|
* @param logger to log trace events
|
||||||
* @param opts NodeCache options
|
* @param _cache cache store to use
|
||||||
*/
|
*/
|
||||||
export function makeCacheableSignalKeyStore(
|
export function makeCacheableSignalKeyStore(
|
||||||
store: SignalKeyStore,
|
store: SignalKeyStore,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
opts?: NodeCache.Options
|
_cache?: CacheStore
|
||||||
): SignalKeyStore {
|
): SignalKeyStore {
|
||||||
const cache = new NodeCache({
|
const cache = _cache || new NodeCache({
|
||||||
...opts || { },
|
stdTTL: DEFAULT_CACHE_TTLS.SIGNAL_STORE, // 5 minutes
|
||||||
useClones: false,
|
useClones: false,
|
||||||
|
deleteOnExpire: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
function getUniqueId(type: string, id: string) {
|
function getUniqueId(type: string, id: string) {
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ const NO_MESSAGE_FOUND_ERROR_TEXT = 'Message absent from node'
|
|||||||
|
|
||||||
type MessageType = 'chat' | 'peer_broadcast' | 'other_broadcast' | 'group' | 'direct_peer_status' | 'other_status'
|
type MessageType = 'chat' | 'peer_broadcast' | 'other_broadcast' | 'group' | 'direct_peer_status' | 'other_status'
|
||||||
|
|
||||||
export const decodeMessageStanza = (stanza: BinaryNode, auth: AuthenticationState) => {
|
/**
|
||||||
|
* Decode the received node as a message.
|
||||||
|
* @note this will only parse the message, not decrypt it
|
||||||
|
*/
|
||||||
|
export function decodeMessageNode(stanza: BinaryNode, meId: string) {
|
||||||
let msgType: MessageType
|
let msgType: MessageType
|
||||||
let chatId: string
|
let chatId: string
|
||||||
let author: string
|
let author: string
|
||||||
@@ -19,7 +23,7 @@ export const decodeMessageStanza = (stanza: BinaryNode, auth: AuthenticationStat
|
|||||||
const participant: string | undefined = stanza.attrs.participant
|
const participant: string | undefined = stanza.attrs.participant
|
||||||
const recipient: string | undefined = stanza.attrs.recipient
|
const recipient: string | undefined = stanza.attrs.recipient
|
||||||
|
|
||||||
const isMe = (jid: string) => areJidsSameUser(jid, auth.creds.me!.id)
|
const isMe = (jid: string) => areJidsSameUser(jid, meId)
|
||||||
|
|
||||||
if(isJidUser(from)) {
|
if(isJidUser(from)) {
|
||||||
if(recipient) {
|
if(recipient) {
|
||||||
@@ -60,8 +64,6 @@ export const decodeMessageStanza = (stanza: BinaryNode, auth: AuthenticationStat
|
|||||||
throw new Boom('Unknown message type', { data: stanza })
|
throw new Boom('Unknown message type', { data: stanza })
|
||||||
}
|
}
|
||||||
|
|
||||||
const sender = msgType === 'chat' ? author : chatId
|
|
||||||
|
|
||||||
const fromMe = isMe(stanza.attrs.participant || stanza.attrs.from)
|
const fromMe = isMe(stanza.attrs.participant || stanza.attrs.from)
|
||||||
const pushname = stanza.attrs.notify
|
const pushname = stanza.attrs.notify
|
||||||
|
|
||||||
@@ -75,13 +77,23 @@ export const decodeMessageStanza = (stanza: BinaryNode, auth: AuthenticationStat
|
|||||||
const fullMessage: proto.IWebMessageInfo = {
|
const fullMessage: proto.IWebMessageInfo = {
|
||||||
key,
|
key,
|
||||||
messageTimestamp: +stanza.attrs.t,
|
messageTimestamp: +stanza.attrs.t,
|
||||||
pushName: pushname
|
pushName: pushname,
|
||||||
|
broadcast: isJidBroadcast(from)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(key.fromMe) {
|
if(key.fromMe) {
|
||||||
fullMessage.status = proto.WebMessageInfo.Status.SERVER_ACK
|
fullMessage.status = proto.WebMessageInfo.Status.SERVER_ACK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fullMessage,
|
||||||
|
author,
|
||||||
|
sender: msgType === 'chat' ? author : chatId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decryptMessageNode = (stanza: BinaryNode, auth: AuthenticationState) => {
|
||||||
|
const { fullMessage, author, sender } = decodeMessageNode(stanza, auth.creds.me!.id)
|
||||||
return {
|
return {
|
||||||
fullMessage,
|
fullMessage,
|
||||||
category: stanza.attrs.category,
|
category: stanza.attrs.category,
|
||||||
|
|||||||
@@ -138,13 +138,13 @@ export const delayCancellable = (ms: number) => {
|
|||||||
|
|
||||||
export async function promiseTimeout<T>(ms: number | undefined, promise: (resolve: (v?: T) => void, reject: (error) => void) => void) {
|
export async function promiseTimeout<T>(ms: number | undefined, promise: (resolve: (v?: T) => void, reject: (error) => void) => void) {
|
||||||
if(!ms) {
|
if(!ms) {
|
||||||
return new Promise (promise)
|
return new Promise(promise)
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = new Error().stack
|
const stack = new Error().stack
|
||||||
// Create a promise that rejects in <ms> milliseconds
|
// Create a promise that rejects in <ms> milliseconds
|
||||||
const { delay, cancel } = delayCancellable (ms)
|
const { delay, cancel } = delayCancellable (ms)
|
||||||
const p = new Promise ((resolve, reject) => {
|
const p = new Promise((resolve, reject) => {
|
||||||
delay
|
delay
|
||||||
.then(() => reject(
|
.then(() => reject(
|
||||||
new Boom('Timed Out', {
|
new Boom('Timed Out', {
|
||||||
@@ -353,7 +353,10 @@ export const getCodeFromWSError = (error: Error) => {
|
|||||||
if(!Number.isNaN(code) && code >= 400) {
|
if(!Number.isNaN(code) && code >= 400) {
|
||||||
statusCode = code
|
statusCode = code
|
||||||
}
|
}
|
||||||
} else if((error as any).code?.startsWith('E')) { // handle ETIMEOUT, ENOTFOUND etc
|
} else if(
|
||||||
|
(error as any).code?.startsWith('E')
|
||||||
|
|| error?.message?.includes('timed out')
|
||||||
|
) { // handle ETIMEOUT, ENOTFOUND etc
|
||||||
statusCode = 408
|
statusCode = 408
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AxiosRequestConfig } from 'axios'
|
||||||
import { Logger } from 'pino'
|
import { Logger } from 'pino'
|
||||||
import { WAMediaUploadFunction, WAUrlInfo } from '../Types'
|
import { WAMediaUploadFunction, WAUrlInfo } from '../Types'
|
||||||
import { prepareWAMessageMedia } from './messages'
|
import { prepareWAMessageMedia } from './messages'
|
||||||
@@ -21,7 +22,7 @@ export type URLGenerationOptions = {
|
|||||||
/** Timeout in ms */
|
/** Timeout in ms */
|
||||||
timeout: number
|
timeout: number
|
||||||
proxyUrl?: string
|
proxyUrl?: string
|
||||||
headers?: { [key: string]: string }
|
headers?: AxiosRequestConfig<{}>['headers']
|
||||||
}
|
}
|
||||||
uploadImage?: WAMediaUploadFunction
|
uploadImage?: WAMediaUploadFunction
|
||||||
logger?: Logger
|
logger?: Logger
|
||||||
@@ -47,7 +48,10 @@ export const getUrlInfo = async(
|
|||||||
previewLink = 'https://' + previewLink
|
previewLink = 'https://' + previewLink
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = await getLinkPreview(previewLink, opts.fetchOpts)
|
const info = await getLinkPreview(previewLink, {
|
||||||
|
...opts.fetchOpts,
|
||||||
|
headers: opts.fetchOpts as {}
|
||||||
|
})
|
||||||
if(info && 'title' in info && info.title) {
|
if(info && 'title' in info && info.title) {
|
||||||
const [image] = info.images
|
const [image] = info.images
|
||||||
|
|
||||||
@@ -62,7 +66,11 @@ export const getUrlInfo = async(
|
|||||||
if(opts.uploadImage) {
|
if(opts.uploadImage) {
|
||||||
const { imageMessage } = await prepareWAMessageMedia(
|
const { imageMessage } = await prepareWAMessageMedia(
|
||||||
{ image: { url: image } },
|
{ image: { url: image } },
|
||||||
{ upload: opts.uploadImage, mediaTypeOverride: 'thumbnail-link' }
|
{
|
||||||
|
upload: opts.uploadImage,
|
||||||
|
mediaTypeOverride: 'thumbnail-link',
|
||||||
|
options: opts.fetchOpts
|
||||||
|
}
|
||||||
)
|
)
|
||||||
urlInfo.jpegThumbnail = imageMessage?.jpegThumbnail
|
urlInfo.jpegThumbnail = imageMessage?.jpegThumbnail
|
||||||
? Buffer.from(imageMessage.jpegThumbnail)
|
? Buffer.from(imageMessage.jpegThumbnail)
|
||||||
|
|||||||
@@ -192,8 +192,11 @@ export async function getAudioDuration(buffer: Buffer | string | Readable) {
|
|||||||
metadata = await musicMetadata.parseBuffer(buffer, undefined, { duration: true })
|
metadata = await musicMetadata.parseBuffer(buffer, undefined, { duration: true })
|
||||||
} else if(typeof buffer === 'string') {
|
} else if(typeof buffer === 'string') {
|
||||||
const rStream = createReadStream(buffer)
|
const rStream = createReadStream(buffer)
|
||||||
metadata = await musicMetadata.parseStream(rStream, undefined, { duration: true })
|
try {
|
||||||
rStream.close()
|
metadata = await musicMetadata.parseStream(rStream, undefined, { duration: true })
|
||||||
|
} finally {
|
||||||
|
rStream.destroy()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
metadata = await musicMetadata.parseStream(buffer, undefined, { duration: true })
|
metadata = await musicMetadata.parseStream(buffer, undefined, { duration: true })
|
||||||
}
|
}
|
||||||
@@ -209,29 +212,29 @@ export const toReadable = (buffer: Buffer) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const toBuffer = async(stream: Readable) => {
|
export const toBuffer = async(stream: Readable) => {
|
||||||
let buff = Buffer.alloc(0)
|
const chunks: Buffer[] = []
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
buff = Buffer.concat([ buff, chunk ])
|
chunks.push(chunk)
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.destroy()
|
stream.destroy()
|
||||||
return buff
|
return Buffer.concat(chunks)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStream = async(item: WAMediaUpload) => {
|
export const getStream = async(item: WAMediaUpload, opts?: AxiosRequestConfig) => {
|
||||||
if(Buffer.isBuffer(item)) {
|
if(Buffer.isBuffer(item)) {
|
||||||
return { stream: toReadable(item), type: 'buffer' }
|
return { stream: toReadable(item), type: 'buffer' } as const
|
||||||
}
|
}
|
||||||
|
|
||||||
if('stream' in item) {
|
if('stream' in item) {
|
||||||
return { stream: item.stream, type: 'readable' }
|
return { stream: item.stream, type: 'readable' } as const
|
||||||
}
|
}
|
||||||
|
|
||||||
if(item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {
|
if(item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {
|
||||||
return { stream: await getHttpStream(item.url), type: 'remote' }
|
return { stream: await getHttpStream(item.url, opts), type: 'remote' } as const
|
||||||
}
|
}
|
||||||
|
|
||||||
return { stream: createReadStream(item.url), type: 'file' }
|
return { stream: createReadStream(item.url), type: 'file' } as const
|
||||||
}
|
}
|
||||||
|
|
||||||
/** generates a thumbnail for a given media, if required */
|
/** generates a thumbnail for a given media, if required */
|
||||||
@@ -278,21 +281,23 @@ export const getHttpStream = async(url: string | URL, options: AxiosRequestConfi
|
|||||||
return fetched.data as Readable
|
return fetched.data as Readable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EncryptedStreamOptions = {
|
||||||
|
saveOriginalFileIfRequired?: boolean
|
||||||
|
logger?: Logger
|
||||||
|
opts?: AxiosRequestConfig
|
||||||
|
}
|
||||||
|
|
||||||
export const encryptedStream = async(
|
export const encryptedStream = async(
|
||||||
media: WAMediaUpload,
|
media: WAMediaUpload,
|
||||||
mediaType: MediaType,
|
mediaType: MediaType,
|
||||||
saveOriginalFileIfRequired = true,
|
{ logger, saveOriginalFileIfRequired, opts }: EncryptedStreamOptions = {}
|
||||||
logger?: Logger
|
|
||||||
) => {
|
) => {
|
||||||
const { stream, type } = await getStream(media)
|
const { stream, type } = await getStream(media, opts)
|
||||||
|
|
||||||
logger?.debug('fetched media stream')
|
logger?.debug('fetched media stream')
|
||||||
|
|
||||||
const mediaKey = Crypto.randomBytes(32)
|
const mediaKey = Crypto.randomBytes(32)
|
||||||
const { cipherKey, iv, macKey } = getMediaKeys(mediaKey, mediaType)
|
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 encWriteStream = new Readable({ read: () => {} })
|
||||||
|
|
||||||
let bodyPath: string | undefined
|
let bodyPath: string | undefined
|
||||||
@@ -312,15 +317,23 @@ export const encryptedStream = async(
|
|||||||
let sha256Plain = Crypto.createHash('sha256')
|
let sha256Plain = Crypto.createHash('sha256')
|
||||||
let sha256Enc = Crypto.createHash('sha256')
|
let sha256Enc = Crypto.createHash('sha256')
|
||||||
|
|
||||||
const onChunk = (buff: Buffer) => {
|
|
||||||
sha256Enc = sha256Enc.update(buff)
|
|
||||||
hmac = hmac.update(buff)
|
|
||||||
encWriteStream.push(buff)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const data of stream) {
|
for await (const data of stream) {
|
||||||
fileLength += data.length
|
fileLength += data.length
|
||||||
|
|
||||||
|
if(
|
||||||
|
type === 'remote'
|
||||||
|
&& opts?.maxContentLength
|
||||||
|
&& fileLength + data.length > opts.maxContentLength
|
||||||
|
) {
|
||||||
|
throw new Boom(
|
||||||
|
`content length exceeded when encrypting "${type}"`,
|
||||||
|
{
|
||||||
|
data: { media, type }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
sha256Plain = sha256Plain.update(data)
|
sha256Plain = sha256Plain.update(data)
|
||||||
if(writeStream) {
|
if(writeStream) {
|
||||||
if(!writeStream.write(data)) {
|
if(!writeStream.write(data)) {
|
||||||
@@ -342,7 +355,7 @@ export const encryptedStream = async(
|
|||||||
encWriteStream.push(mac)
|
encWriteStream.push(mac)
|
||||||
encWriteStream.push(null)
|
encWriteStream.push(null)
|
||||||
|
|
||||||
writeStream && writeStream.end()
|
writeStream?.end()
|
||||||
stream.destroy()
|
stream.destroy()
|
||||||
|
|
||||||
logger?.debug('encrypted data successfully')
|
logger?.debug('encrypted data successfully')
|
||||||
@@ -366,8 +379,22 @@ export const encryptedStream = async(
|
|||||||
sha256Enc.destroy(error)
|
sha256Enc.destroy(error)
|
||||||
stream.destroy(error)
|
stream.destroy(error)
|
||||||
|
|
||||||
|
if(didSaveToTmpPath) {
|
||||||
|
try {
|
||||||
|
await fs.unlink(bodyPath!)
|
||||||
|
} catch(err) {
|
||||||
|
logger?.error({ err }, 'failed to save to tmp path')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onChunk(buff: Buffer) {
|
||||||
|
sha256Enc = sha256Enc.update(buff)
|
||||||
|
hmac = hmac.update(buff)
|
||||||
|
encWriteStream.push(buff)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEF_HOST = 'mmg.whatsapp.net'
|
const DEF_HOST = 'mmg.whatsapp.net'
|
||||||
@@ -421,14 +448,14 @@ export const downloadEncryptedContent = async(
|
|||||||
|
|
||||||
const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined
|
const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined
|
||||||
|
|
||||||
const headers: { [_: string]: string } = {
|
const headers: AxiosRequestConfig['headers'] = {
|
||||||
...options?.headers || { },
|
...options?.headers || { },
|
||||||
Origin: DEFAULT_ORIGIN,
|
Origin: DEFAULT_ORIGIN,
|
||||||
}
|
}
|
||||||
if(startChunk || endChunk) {
|
if(startChunk || endChunk) {
|
||||||
headers.Range = `bytes=${startChunk}-`
|
headers!.Range = `bytes=${startChunk}-`
|
||||||
if(endChunk) {
|
if(endChunk) {
|
||||||
headers.Range += endChunk
|
headers!.Range += endChunk
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,7 +671,7 @@ export const encryptMediaRetryRequest = (
|
|||||||
tag: 'rmr',
|
tag: 'rmr',
|
||||||
attrs: {
|
attrs: {
|
||||||
jid: key.remoteJid!,
|
jid: key.remoteJid!,
|
||||||
from_me: (!!key.fromMe).toString(),
|
'from_me': (!!key.fromMe).toString(),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
participant: key.participant || undefined
|
participant: key.participant || undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,11 @@ export const prepareWAMessageMedia = async(
|
|||||||
} = await encryptedStream(
|
} = await encryptedStream(
|
||||||
uploadData.media,
|
uploadData.media,
|
||||||
options.mediaTypeOverride || mediaType,
|
options.mediaTypeOverride || mediaType,
|
||||||
requiresOriginalForSomeProcessing
|
{
|
||||||
|
logger,
|
||||||
|
saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
|
||||||
|
opts: options.options
|
||||||
|
}
|
||||||
)
|
)
|
||||||
// url safe Base64 encode the SHA256 hash of the body
|
// url safe Base64 encode the SHA256 hash of the body
|
||||||
const fileEncSha256B64 = fileEncSha256.toString('base64')
|
const fileEncSha256B64 = fileEncSha256.toString('base64')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Logger } from 'pino'
|
|||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { AuthenticationCreds, BaileysEventEmitter, Chat, GroupMetadata, ParticipantAction, SignalKeyStoreWithTransaction, WAMessageStubType } from '../Types'
|
import { AuthenticationCreds, BaileysEventEmitter, Chat, GroupMetadata, ParticipantAction, SignalKeyStoreWithTransaction, WAMessageStubType } from '../Types'
|
||||||
import { downloadAndProcessHistorySyncNotification, getContentType, normalizeMessageContent, toNumber } from '../Utils'
|
import { downloadAndProcessHistorySyncNotification, getContentType, normalizeMessageContent, toNumber } from '../Utils'
|
||||||
import { areJidsSameUser, jidNormalizedUser } from '../WABinary'
|
import { areJidsSameUser, isJidBroadcast, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary'
|
||||||
|
|
||||||
type ProcessMessageContext = {
|
type ProcessMessageContext = {
|
||||||
shouldProcessHistoryMsg: boolean
|
shouldProcessHistoryMsg: boolean
|
||||||
@@ -72,6 +72,22 @@ export const shouldIncrementChatUnread = (message: proto.IWebMessageInfo) => (
|
|||||||
!message.key.fromMe && !message.messageStubType
|
!message.key.fromMe && !message.messageStubType
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of the chat from the given key.
|
||||||
|
* Typically -- that'll be the remoteJid, but for broadcasts, it'll be the participant
|
||||||
|
*/
|
||||||
|
export const getChatId = ({ remoteJid, participant, fromMe }: proto.IMessageKey) => {
|
||||||
|
if(
|
||||||
|
isJidBroadcast(remoteJid!)
|
||||||
|
&& !isJidStatusBroadcast(remoteJid!)
|
||||||
|
&& !fromMe
|
||||||
|
) {
|
||||||
|
return participant!
|
||||||
|
}
|
||||||
|
|
||||||
|
return remoteJid!
|
||||||
|
}
|
||||||
|
|
||||||
const processMessage = async(
|
const processMessage = async(
|
||||||
message: proto.IWebMessageInfo,
|
message: proto.IWebMessageInfo,
|
||||||
{
|
{
|
||||||
@@ -86,7 +102,7 @@ const processMessage = async(
|
|||||||
const meId = creds.me!.id
|
const meId = creds.me!.id
|
||||||
const { accountSettings } = creds
|
const { accountSettings } = creds
|
||||||
|
|
||||||
const chat: Partial<Chat> = { id: jidNormalizedUser(message.key.remoteJid!) }
|
const chat: Partial<Chat> = { id: jidNormalizedUser(getChatId(message.key)) }
|
||||||
const isRealMsg = isRealMessage(message, meId)
|
const isRealMsg = isRealMessage(message, meId)
|
||||||
|
|
||||||
if(isRealMsg) {
|
if(isRealMsg) {
|
||||||
|
|||||||
Reference in New Issue
Block a user