@vreden/meta
Version:
Baileys is a lightweight JavaScript library for interacting with the WhatsApp Web API using WebSocket.
869 lines (799 loc) • 30.3 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k
var desc = Object.getOwnPropertyDescriptor(m, k)
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k] } }
}
Object.defineProperty(o, k2, desc)
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k
o[k2] = m[k]
}))
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v })
}) : function(o, v) {
o["default"] = v
})
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod
var result = {}
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k)
__setModuleDefault(result, mod)
return result
}
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }
}
Object.defineProperty(exports, "__esModule", { value: true })
const boom_1 = require("@hapi/boom")
const axios_1 = __importDefault(require("axios"))
const child_process_1 = require("child_process")
const Crypto = __importStar(require("crypto"))
const events_1 = require("events")
const fs_1 = require("fs")
const os_1 = require("os")
const path_1 = require("path")
const stream_1 = require("stream")
const WAProto_1 = require("../../WAProto")
const Defaults_1 = require("../Defaults")
const WABinary_1 = require("../WABinary")
const crypto_1 = require("./crypto")
const generics_1 = require("./generics")
const getImageProcessingLibrary = async () => {
const [_jimp, sharp] = await Promise.all([
(async () => {
const jimp = await (Promise.resolve().then(() => __importStar(require('jimp'))).catch(() => { }))
return jimp
})(),
(async () => {
const sharp = await (Promise.resolve().then(() => __importStar(require('sharp'))).catch(() => { }))
return sharp
})()
])
if (sharp) {
return { sharp }
}
const jimp = _jimp?.default || _jimp
if (jimp) {
return { jimp }
}
throw new boom_1.Boom('No image processing library available')
}
const hkdfInfoKey = (type) => {
const hkdfInfo = Defaults_1.MEDIA_HKDF_KEY_MAPPING[type]
return `WhatsApp ${hkdfInfo} Keys`
}
const getRawMediaUploadData = async (media, mediaType, logger) => {
const { stream } = await getStream(media)
logger?.debug('got stream for raw upload')
const hasher = Crypto.createHash('sha256')
const filePath = path_1.join(os_1.tmpdir(), mediaType + generics_1.generateMessageID())
const fileWriteStream = fs_1.createWriteStream(filePath)
let fileLength = 0
try {
for await (const data of stream) {
fileLength += data.length
hasher.update(data)
if (!fileWriteStream.write(data)) {
await events_1.once(fileWriteStream, 'drain')
}
}
fileWriteStream.end()
await events_1.once(fileWriteStream, 'finish')
stream.destroy()
const fileSha256 = hasher.digest()
logger?.debug('hashed data for raw upload')
return {
filePath: filePath,
fileSha256,
fileLength
}
}
catch (error) {
fileWriteStream.destroy()
stream.destroy()
try {
await fs_1.promises.unlink(filePath)
}
catch {
//
}
throw error
}
}
/** generates all the keys required to encrypt/decrypt & sign a media message */
async function getMediaKeys(buffer, mediaType) {
if (!buffer) {
throw new boom_1.Boom('Cannot derive from empty media key')
}
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 = await crypto_1.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 = (videoPath, time = '00:00:00', size = { width: 320 }) => {
return new Promise((resolve, reject) => {
const args = [
'-ss', time,
'-i', videoPath,
'-y',
'-vf', `scale=${size.width}:-1`,
'-vframes', '1',
'-f', 'image2',
'-vcodec', 'mjpeg',
'pipe:1'
]
const ffmpeg = child_process_1.spawn('ffmpeg', args)
const chunks = []
let errorOutput = ''
ffmpeg.stdout.on('data', chunk => chunks.push(chunk))
ffmpeg.stderr.on('data', data => {
errorOutput += data.toString()
})
ffmpeg.on('error', reject)
ffmpeg.on('close', code => {
if (code === 0) return resolve(Buffer.concat(chunks))
reject(new Error(`ffmpeg exited with code ${code}\n${errorOutput}`))
})
})
}
const extractImageThumb = async (bufferOrFilePath, width = 32, quality = 50) => {
if (typeof bufferOrFilePath === "string" && bufferOrFilePath.startsWith("http")) {
const response = await axios_1.default.get(bufferOrFilePath, { responseType: "arraybuffer" })
bufferOrFilePath = Buffer.from(response.data)
}
if (bufferOrFilePath instanceof stream_1.Readable) {
bufferOrFilePath = await toBuffer(bufferOrFilePath)
}
const lib = await getImageProcessingLibrary()
if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
const img = lib.sharp.default(bufferOrFilePath)
const dimensions = await img.metadata()
const buffer = await img
.resize({
width,
height: width,
fit: 'contain',
background: { r: 255, g: 255, b: 255, alpha: 0 }
})
.jpeg({ quality })
.toBuffer()
return {
buffer,
original: {
width: dimensions.width,
height: dimensions.height,
},
}
}
else if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
const { read, MIME_JPEG, RESIZE_BEZIER, AUTO } = lib.jimp
const jimp = await read(bufferOrFilePath)
const dimensions = {
width: jimp.getWidth(),
height: jimp.getHeight()
}
const buffer = await jimp
.quality(quality)
.resize(width, AUTO, RESIZE_BEZIER)
.getBufferAsync(MIME_JPEG)
return {
buffer,
original: dimensions
}
}
else {
throw new boom_1.Boom('No image processing library available')
}
}
const encodeBase64EncodedStringForUpload = (b64) => (encodeURIComponent(b64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/\=+$/, '')))
const generateProfilePicture = async (mediaUpload) => {
let bufferOrFilePath
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
if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
img = await lib.sharp.default(bufferOrFilePath)
.resize(720, 720, {
fit: 'inside',
})
.jpeg({ quality: 50 })
.toBuffer()
}
else if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
const { read, MIME_JPEG } = lib.jimp
const image = await read(bufferOrFilePath)
const min = image.getWidth()
const max = image.getHeight()
const cropped = image.crop(0, 0, min, max)
img = await cropped.scaleToFit(720, 720).getBufferAsync(MIME_JPEG)
}
else {
throw new boom_1.Boom('No image processing library available')
}
return {
img: await img,
}
}
/** gets the SHA256 of the given media message */
const mediaMessageSHA256B64 = (message) => {
const media = Object.values(message)[0]
return (media === null || media === void 0 ? void 0 : media.fileSha256) && Buffer.from(media.fileSha256).toString('base64')
}
async function getAudioDuration(buffer) {
const musicMetadata = await Promise.resolve().then(() => __importStar(require('music-metadata')))
const options = {
duration: true
}
let metadata
if (Buffer.isBuffer(buffer)) {
metadata = await musicMetadata.parseBuffer(buffer, undefined, options)
}
else if (typeof buffer === 'string') {
metadata = await musicMetadata.parseFile(buffer, options)
}
else {
metadata = await musicMetadata.parseStream(buffer, undefined, options)
}
return metadata.format.duration
}
/**
referenced from and modifying https://github.com/wppconnect-team/wa-js/blob/main/src/chat/functions/prepareAudioWaveform.ts
*/
async function getAudioWaveform(buffer, logger) {
try {
const { default: decoder } = await eval('import(\'audio-decode\')')
let audioData
if (Buffer.isBuffer(buffer)) {
audioData = buffer
}
else if (typeof buffer === 'string') {
const rStream = fs_1.createReadStream(buffer)
audioData = await toBuffer(rStream)
}
else {
audioData = await toBuffer(buffer)
}
const audioBuffer = await decoder(audioData)
const rawData = audioBuffer.getChannelData(0) // We only need to work with one channel of data
const samples = 64 // Number of samples we want to have in our final data set
const blockSize = Math.floor(rawData.length / samples) // the number of samples in each subdivision
const filteredData = []
for (let i = 0; i < samples; i++) {
const blockStart = blockSize * i // the location of the first sample in the block
let sum = 0
for (let j = 0; j < blockSize; j++) {
sum = sum + Math.abs(rawData[blockStart + j]) // find the sum of all the samples in the block
}
filteredData.push(sum / blockSize) // divide the sum by the block size to get the average
}
// This guarantees that the largest data point will be set to 1, and the rest of the data will scale proportionally.
const multiplier = Math.pow(Math.max(...filteredData), -1)
const normalizedData = filteredData.map((n) => n * multiplier)
// Generate waveform like WhatsApp
const waveform = new Uint8Array(normalizedData.map((n) => Math.floor(100 * n)))
return waveform
}
catch (e) {
logger?.debug('Failed to generate waveform: ' + e)
}
}
const toReadable = (buffer) => {
const readable = new stream_1.Readable({ read: () => { } })
readable.push(buffer)
readable.push(null)
return readable
}
const toBuffer = async (stream) => {
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
stream.destroy()
return Buffer.concat(chunks)
}
const getStream = async (item, opts) => {
if (Buffer.isBuffer(item)) {
return { stream: toReadable(item), type: 'buffer' }
}
if ('stream' in item) {
return { stream: item.stream, type: 'readable' }
}
const urlStr = item.url.toString()
if (urlStr.startsWith('data:')) {
const buffer = Buffer.from(urlStr.split(',')[1], 'base64')
return { stream: await toReadable(buffer), type: 'buffer' }
}
if (urlStr.startsWith('http://') || urlStr.startsWith('https://')) {
return { stream: await getHttpStream(item.url, opts), type: 'remote' }
}
return { stream: fs_1.createReadStream(item.url), type: 'file' }
}
/** generates a thumbnail for a given media, if required */
async function generateThumbnail(file, mediaType, options) {
let thumbnail
let originalImageDimensions
if (mediaType === 'image') {
const { buffer, original } = await extractImageThumb(file, 256, 95)
thumbnail = buffer.toString('base64')
if (original.width && original.height) {
originalImageDimensions = {
width: original.width,
height: original.height,
}
}
}
else if (mediaType === 'video') {
try {
const buff = await extractVideoThumb(file, '00:00:00', { width: 32, height: 32 })
thumbnail = buff.toString('base64')
}
catch (err) {
options?.logger?.debug('could not generate video thumb: ' + err)
}
}
return {
thumbnail,
originalImageDimensions
}
}
const getHttpStream = async (url, options = {}) => {
const fetched = await axios_1.default.get(url.toString(), { ...options, responseType: 'stream' })
return fetched.data
}
const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
const { stream, type } = await getStream(media, opts)
logger?.debug('fetched media stream')
const encFilePath = path_1.join(os_1.tmpdir(), mediaType + generics_1.generateMessageID() + '-plain')
const encFileWriteStream = fs_1.createWriteStream(encFilePath)
let originalFilePath
let originalFileStream
if (type === 'file') {
originalFilePath = media.url.toString()
} else if (saveOriginalFileIfRequired) {
originalFilePath = path_1.join(os_1.tmpdir(), mediaType + generics_1.generateMessageID() + '-original')
originalFileStream = fs_1.createWriteStream(originalFilePath)
}
let fileLength = 0
const sha256 = Crypto.createHash('sha256')
try {
for await (const data of stream) {
fileLength += data.length
if (type === 'remote'
&& opts?.maxContentLength
&& fileLength + data.length > opts.maxContentLength) {
throw new boom_1.Boom(`content length exceeded when preparing "${type}"`, {
data: { media, type }
})
}
sha256.update(data)
encFileWriteStream.write(data)
if (originalFileStream && !originalFileStream.write(data)) {
await events_1.once(originalFileStream, 'drain')
}
}
const fileSha256 = sha256.digest()
encFileWriteStream.end()
originalFileStream?.end?.call(originalFileStream)
stream.destroy()
logger?.debug('prepared plain stream successfully')
return {
mediaKey: undefined,
originalFilePath,
encFilePath,
mac: undefined,
fileEncSha256: undefined,
fileSha256,
fileLength
}
}
catch (error) {
encFileWriteStream.destroy()
originalFileStream?.destroy?.call(originalFileStream)
sha256.destroy()
stream.destroy()
try {
await fs_1.promises.unlink(encFilePath)
if (originalFilePath && didSaveToTmpPath) {
await fs_1.promises.unlink(originalFilePath)
}
} catch (err) {
logger?.error({ err }, 'failed deleting tmp files')
}
throw error
}
}
const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
const { stream, type } = await getStream(media, opts)
logger?.debug('fetched media stream')
const mediaKey = Crypto.randomBytes(32)
const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType)
const encFilePath = path_1.join(os_1.tmpdir(), mediaType + generics_1.generateMessageID() + '-enc')
const encFileWriteStream = fs_1.createWriteStream(encFilePath)
let originalFileStream
let originalFilePath
if (saveOriginalFileIfRequired) {
originalFilePath = path_1.join(os_1.tmpdir(), mediaType + generics_1.generateMessageID() + '-original')
originalFileStream = fs_1.createWriteStream(originalFilePath)
}
let fileLength = 0
const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv)
const hmac = Crypto.createHmac('sha256', macKey).update(iv)
const sha256Plain = Crypto.createHash('sha256')
const sha256Enc = Crypto.createHash('sha256')
const onChunk = (buff) => {
sha256Enc.update(buff)
hmac.update(buff)
encFileWriteStream.write(buff)
}
try {
for await (const data of stream) {
fileLength += data.length
if (type === 'remote'
&& opts?.maxContentLength
&& fileLength + data.length > opts.maxContentLength) {
throw new boom_1.Boom(`content length exceeded when encrypting "${type}"`, {
data: { media, type }
})
}
if (originalFileStream) {
if (!originalFileStream.write(data)) {
await events_1.once(originalFileStream, 'drain')
}
}
sha256Plain.update(data)
onChunk(aes.update(data))
}
onChunk(aes.final())
const mac = hmac.digest().slice(0, 10)
sha256Enc.update(mac)
const fileSha256 = sha256Plain.digest()
const fileEncSha256 = sha256Enc.digest()
encFileWriteStream.write(mac)
encFileWriteStream.end()
originalFileStream?.end?.call(originalFileStream)
stream.destroy()
logger?.debug('encrypted data successfully')
return {
mediaKey,
originalFilePath,
encFilePath,
mac,
fileEncSha256,
fileSha256,
fileLength
}
}
catch (error) {
// destroy all streams with error
encFileWriteStream.destroy()
originalFileStream?.destroy?.call(originalFileStream)
aes.destroy()
hmac.destroy()
sha256Plain.destroy()
sha256Enc.destroy()
stream.destroy()
try {
await fs_1.promises.unlink(encFilePath)
if (originalFilePath) {
await fs_1.promises.unlink(originalFilePath)
}
}
catch (err) {
logger?.error({ err }, 'failed deleting tmp files')
}
throw error
}
}
const DEF_HOST = 'mmg.whatsapp.net'
const AES_CHUNK_SIZE = 16
const toSmallestChunkSize = (num) => {
return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE
}
const getUrlFromDirectPath = (directPath) => `https://${DEF_HOST}${directPath}`
const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
const isValidMediaUrl = url?.startsWith('https://mmg.whatsapp.net/')
const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath)
if (!downloadUrl) {
throw new boom_1.Boom('No valid media URL or directPath present in message', { statusCode: 400 })
}
const keys = await getMediaKeys(mediaKey, type)
return downloadEncryptedContent(downloadUrl, keys, opts)
}
/**
* Decrypts and downloads an AES256-CBC encrypted file given the keys.
* Assumes the SHA256 of the plaintext is appended to the end of the ciphertext
* */
const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
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
const headers = {
...(options?.headers) || {},
Origin: Defaults_1.DEFAULT_ORIGIN,
}
if (startChunk || endChunk) {
headers.Range = `bytes=${startChunk}-`
if (endChunk) {
headers.Range += endChunk
}
}
// download the message
const fetched = await getHttpStream(downloadUrl, {
...options || {},
headers,
maxBodyLength: Infinity,
maxContentLength: Infinity,
})
let remainingBytes = Buffer.from([])
let aes
const pushBytes = (bytes, push) => {
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))
bytesFetched += bytes.length
}
else {
push(bytes)
}
}
const output = new stream_1.Transform({
transform(chunk, _, callback) {
let data = Buffer.concat([remainingBytes, chunk])
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)
}
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 })
}
function extensionForMediaMessage(message) {
const getExtension = (mimetype) => mimetype.split('')[0].split('/')[1]
const type = Object.keys(message)[0]
let extension
if (type === 'locationMessage' ||
type === 'liveLocationMessage' ||
type === 'productMessage') {
extension = '.jpeg'
}
else {
const messageContent = message[type]
extension = getExtension(messageContent.mimetype)
}
return extension
}
const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger, options }, refreshMediaConn) => {
return async (filePath, { mediaType, fileEncSha256B64, newsletter, timeoutMs }) => {
// send a query JSON to obtain the url & auth token to upload our media
let uploadInfo = await refreshMediaConn(false)
let urls
let media = Defaults_1.MEDIA_PATH_MAP[mediaType]
const hosts = [...customUploadHosts, ...uploadInfo.hosts]
fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64)
if (newsletter) {
media = media?.replace('/mms/', '/newsletter/newsletter-')
}
for (const { hostname } of hosts) {
logger.debug(`uploading to "${hostname}"`)
const auth = encodeURIComponent(uploadInfo.auth) // the auth token
const url = `https://${hostname}${media}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let result
try {
const body = await axios_1.default.post(url, fs_1.createReadStream(filePath), {
...options,
maxRedirects: 0,
headers: {
...options.headers || {},
'Content-Type': 'application/octet-stream',
'Origin': Defaults_1.DEFAULT_ORIGIN
},
httpsAgent: fetchAgent,
timeout: timeoutMs,
responseType: 'json',
maxBodyLength: Infinity,
maxContentLength: Infinity,
})
result = body.data
if (result?.url || result?.directPath) {
urls = {
mediaUrl: result.url,
directPath: result.direct_path,
meta_hmac: result.meta_hmac,
fbid: result.fbid,
ts: result.ts
}
break
}
else {
uploadInfo = await refreshMediaConn(true)
throw new Error(`upload failed, reason: ${JSON.stringify(result)}`)
}
}
catch (error) {
if (axios_1.default.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...'}`)
}
}
if (!urls) {
throw new boom_1.Boom('Media upload failed on all hosts', { statusCode: 500 })
}
return urls
}
}
const getMediaRetryKey = (mediaKey) => {
return crypto_1.hkdf(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' })
}
/**
* Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
*/
const encryptMediaRetryRequest = async (key, mediaKey, meId) => {
const recp = { stanzaId: key.id }
const recpBuffer = WAProto_1.proto.ServerErrorReceipt.encode(recp).finish()
const iv = Crypto.randomBytes(12)
const retryKey = await getMediaRetryKey(mediaKey)
const ciphertext = crypto_1.aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id))
const req = {
tag: 'receipt',
attrs: {
id: key.id,
to: WABinary_1.jidNormalizedUser(meId),
type: 'server-error'
},
content: [
// this encrypt node is actually pretty useless
// the media is returned even without this node
// keeping it here to maintain parity with WA Web
{
tag: 'encrypt',
attrs: {},
content: [
{ tag: 'enc_p', attrs: {}, content: ciphertext },
{ tag: 'enc_iv', attrs: {}, content: iv }
]
},
{
tag: 'rmr',
attrs: {
jid: key.remoteJid,
'from_me': (!!key.fromMe).toString(),
// @ts-ignore
participant: key.participant || undefined
}
}
]
}
return req
}
const decodeMediaRetryNode = (node) => {
const rmrNode = WABinary_1.getBinaryNodeChild(node, 'rmr')
const event = {
key: {
id: node.attrs.id,
remoteJid: rmrNode.attrs.jid,
fromMe: rmrNode.attrs.from_me === 'true',
participant: rmrNode.attrs.participant
}
}
const errorNode = WABinary_1.getBinaryNodeChild(node, 'error')
if (errorNode) {
const errorCode = +errorNode.attrs.code
event.error = new boom_1.Boom(`Failed to re-upload media (${errorCode})`, { data: errorNode.attrs, statusCode: getStatusCodeForMediaRetry(errorCode) })
}
else {
const encryptedInfoNode = WABinary_1.getBinaryNodeChild(node, 'encrypt')
const ciphertext = WABinary_1.getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_p')
const iv = WABinary_1.getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_iv')
if (ciphertext && iv) {
event.media = { ciphertext, iv }
}
else {
event.error = new boom_1.Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 })
}
}
return event
}
const decryptMediaRetryData = async ({ ciphertext, iv }, mediaKey, msgId) => {
const retryKey = await getMediaRetryKey(mediaKey)
const plaintext = crypto_1.aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId))
return WAProto_1.proto.MediaRetryNotification.decode(plaintext)
}
const getStatusCodeForMediaRetry = (code) => MEDIA_RETRY_STATUS_MAP[code]
const MEDIA_RETRY_STATUS_MAP = {
[WAProto_1.proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
[WAProto_1.proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
[WAProto_1.proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
[WAProto_1.proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418,
}
module.exports = {
hkdfInfoKey,
getMediaKeys,
extractVideoThumb,
extractImageThumb,
encodeBase64EncodedStringForUpload,
generateProfilePicture,
mediaMessageSHA256B64,
getAudioDuration,
getAudioWaveform,
toReadable,
toBuffer,
getStream,
generateThumbnail,
getHttpStream,
prepareStream,
encryptedStream,
getUrlFromDirectPath,
downloadContentFromMessage,
downloadEncryptedContent,
extensionForMediaMessage,
getRawMediaUploadData,
getWAUploadToServer,
getMediaRetryKey,
encryptMediaRetryRequest,
decodeMediaRetryNode,
decryptMediaRetryData,
getStatusCodeForMediaRetry,
MEDIA_RETRY_STATUS_MAP
}