Stream uploads + downloads + allow for remote url uploads

- Switch to using got
- Use encryption/decryption streams for speed & lesser memory consumption
- Allow for stream based download & simultaneous upload of media
This commit is contained in:
Adhiraj Singh
2021-01-13 22:48:28 +05:30
parent 500805236a
commit 0344d6336c
19 changed files with 501 additions and 146 deletions

View File

@@ -9,7 +9,7 @@ import {
ReconnectMode,
ProxyAgent,
waChatKey,
} from '../src/WAConnection/WAConnection'
} from '../src/WAConnection'
import * as fs from 'fs'
async function example() {

BIN
Lmao.mp4 Normal file

Binary file not shown.

7
auth_info copy.json Normal file
View File

@@ -0,0 +1,7 @@
{
"clientID": "wh1OmghfONDRWXt0u/yxfg==",
"serverToken": "1@nBrK6gYz8n+hj46rHcL18n6v08fUBKSDRCD8NvPq9qETNkF/2lyUHbCQQMlDjasHHwVAUp1BBkGQ==",
"clientToken": "LdThbdiLt7yFfyClGQ1fbh5GgN/oiEwvWCg+9gb04wYB7P4f2UzEeY4IDr2rV9Qg1W1Lm+uVUW4lVYbrzX0scQ==",
"encKey": "tHt3uFN7+IOgWccQKAnUo1JDbR9y+rWzEfuddNV1rTo=",
"macKey": "NJo0Bj1Ia7f0+s82gZKEDUgrhfmvc9lbSjgKlnc5Ci4="
}

View File

@@ -35,10 +35,10 @@
"@adiwajshing/keyed-db": "^0.2.2",
"curve25519-js": "^0.0.4",
"futoin-hkdf": "^1.3.2",
"got": "^11.8.1",
"https-proxy-agent": "^5.0.0",
"jimp": "^0.16.1",
"music-metadata": "^7.4.1",
"node-fetch": "^2.6.0",
"pino": "^6.7.0",
"pino-pretty": "^4.3.0",
"protobufjs": "^6.10.1",
@@ -50,9 +50,9 @@
"WAMessage/*"
],
"devDependencies": {
"@types/got": "^9.6.11",
"@types/mocha": "^7.0.2",
"@types/node": "^14.6.2",
"@types/node-fetch": "^2.5.7",
"@types/pino": "^6.3.2",
"@types/ws": "^7.2.6",
"assert": "^2.0.0",

View File

@@ -1,5 +1,5 @@
import fs from 'fs'
import { decryptWA } from './WAConnection/WAConnection'
import { decryptWA } from './WAConnection'
import Decoder from './Binary/Decoder'
interface BrowserMessagesInfo {

View File

@@ -1,4 +1,4 @@
import { WAConnection, MessageOptions, MessageType, unixTimestampSeconds, toNumber, GET_MESSAGE_ID, waMessageKey } from '../WAConnection/WAConnection'
import { WAConnection, MessageOptions, MessageType, unixTimestampSeconds, toNumber, GET_MESSAGE_ID, waMessageKey } from '../WAConnection'
import * as assert from 'assert'
import {promises as fs} from 'fs'

View File

@@ -1,5 +1,5 @@
import * as assert from 'assert'
import {WAConnection} from '../WAConnection/WAConnection'
import {WAConnection} from '../WAConnection'
import { AuthenticationCredentialsBase64, BaileysError, ReconnectMode, DisconnectReason, WAChat, WAContact } from '../WAConnection/Constants'
import { delay } from '../WAConnection/Utils'
import { assertChatDBIntegrity, makeConnection, testJid } from './Common'

View File

@@ -1,4 +1,4 @@
import { MessageType, GroupSettingChange, delay, ChatModification, whatsappID } from '../WAConnection/WAConnection'
import { MessageType, GroupSettingChange, delay, ChatModification, whatsappID } from '../WAConnection'
import * as assert from 'assert'
import { WAConnectionTest, testJid, sendAndRetreiveMessage } from './Common'

43
src/Tests/Tests.Media.ts Normal file
View File

@@ -0,0 +1,43 @@
import { deepStrictEqual, strictEqual } from 'assert'
import { createWriteStream } from 'fs'
import { readFile } from 'fs/promises'
import { proto } from '../../WAMessage/WAMessage'
import { MessageType } from '../WAConnection'
import { aesEncrypWithIV, decryptMediaMessageBuffer, encryptedStream, getMediaKeys, getStream, hmacSign, sha256 } from '../WAConnection/Utils'
import { WAConnectionTest } from './Common'
describe('Media Download Tests', () => {
it('should encrypt media streams correctly', async function() {
const url = './Media/meme.jpeg'
const streamValues = await encryptedStream({ url }, MessageType.image)
const buffer = await readFile(url)
const mediaKeys = getMediaKeys(streamValues.mediaKey, MessageType.image)
const enc = aesEncrypWithIV(buffer, mediaKeys.cipherKey, mediaKeys.iv)
const mac = hmacSign(Buffer.concat([mediaKeys.iv, enc]), mediaKeys.macKey).slice(0, 10)
const body = Buffer.concat([enc, mac]) // body is enc + mac
const fileSha256 = sha256(buffer)
const fileEncSha256 = sha256(body)
deepStrictEqual(streamValues.fileSha256, fileSha256)
strictEqual(streamValues.fileLength, buffer.length)
deepStrictEqual(streamValues.mac, mac)
deepStrictEqual(await readFile(streamValues.encBodyPath), body)
deepStrictEqual(streamValues.fileEncSha256, fileEncSha256)
})
})
/*
WAConnectionTest('Media Upload', conn => {
it('should upload the same file', async () => {
const FILES = [
{ url: './Media/meme.jpeg', type: MessageType.image },
{ url: './Media/ma_gif.mp4', type: MessageType.video },
{ url: './Media/sonata.mp3', type: MessageType.audio },
]
})
})*/

View File

@@ -1,7 +1,8 @@
import { MessageType, Mimetype, delay, promiseTimeout, WA_MESSAGE_STATUS_TYPE, WAMessageStatusUpdate, generateMessageID, WAMessage } from '../WAConnection/WAConnection'
import {promises as fs} from 'fs'
import { MessageType, Mimetype, delay, promiseTimeout, WA_MESSAGE_STATUS_TYPE, generateMessageID, WAMessage } from '../WAConnection'
import { promises as fs } from 'fs'
import * as assert from 'assert'
import { WAConnectionTest, testJid, sendAndRetreiveMessage, assertChatDBIntegrity } from './Common'
import { WAConnectionTest, testJid, sendAndRetreiveMessage } from './Common'
import { resolve } from 'path'
WAConnectionTest('Messages', conn => {
@@ -61,8 +62,7 @@ WAConnectionTest('Messages', conn => {
}
})
it('should send a gif', async () => {
const content = await fs.readFile('./Media/ma_gif.mp4')
const message = await sendAndRetreiveMessage(conn, content, MessageType.video, { mimetype: Mimetype.gif })
const message = await sendAndRetreiveMessage(conn, { url: './Media/ma_gif.mp4' }, MessageType.video, { mimetype: Mimetype.gif })
await conn.downloadAndSaveMediaMessage(message,'./Media/received_vid')
})
@@ -81,9 +81,18 @@ WAConnectionTest('Messages', conn => {
assert.strictEqual (message.message?.audioMessage?.ptt, true)
await conn.downloadAndSaveMediaMessage(message,'./Media/received_aud')
})
it('should send an image', async () => {
const content = await fs.readFile('./Media/meme.jpeg')
const message = await sendAndRetreiveMessage(conn, content, MessageType.image)
it('should send a jpeg image', async () => {
const message = await sendAndRetreiveMessage(conn, { url: './Media/meme.jpeg' }, MessageType.image)
assert.ok (message.message?.imageMessage?.jpegThumbnail)
const msg = await conn.downloadMediaMessage(message)
assert.deepStrictEqual(msg, await fs.readFile('./Media/meme.jpeg'))
})
it('should send a remote jpeg image', async () => {
const message = await sendAndRetreiveMessage(
conn,
{ url: 'https://www.memestemplates.com/wp-content/uploads/2020/05/tom-with-phone.jpg' },
MessageType.image
)
assert.ok (message.message?.imageMessage?.jpegThumbnail)
await conn.downloadMediaMessage(message)
})

View File

@@ -1,7 +1,7 @@
import { Presence, ChatModification, delay, newMessagesDB, WA_DEFAULT_EPHEMERAL, MessageType, WAMessage } from '../WAConnection/WAConnection'
import { Presence, ChatModification, delay, newMessagesDB, WA_DEFAULT_EPHEMERAL, MessageType, WAMessage } from '../WAConnection'
import { promises as fs } from 'fs'
import * as assert from 'assert'
import fetch from 'node-fetch'
import got from 'got'
import { WAConnectionTest, testJid, sendAndRetreiveMessage } from './Common'
WAConnectionTest('Misc', conn => {
@@ -79,15 +79,14 @@ WAConnectionTest('Misc', conn => {
await delay (5000)
const ppUrl = await conn.getProfilePicture(conn.user.jid)
const fetched = await fetch(ppUrl)
const buff = await fetched.buffer ()
const {rawBody: oldPP} = await got(ppUrl)
const newPP = await fs.readFile ('./Media/cat.jpeg')
const response = await conn.updateProfilePicture (conn.user.jid, newPP)
const newPP = await fs.readFile('./Media/cat.jpeg')
await conn.updateProfilePicture(conn.user.jid, newPP)
await delay (10000)
await conn.updateProfilePicture (conn.user.jid, buff) // revert back
await conn.updateProfilePicture (conn.user.jid, oldPP) // revert back
})
it('should return the profile picture', async () => {
const response = await conn.getProfilePicture(testJid)

View File

@@ -1,9 +1,9 @@
import * as fs from 'fs'
import WS from 'ws'
import * as fs from 'fs'
import * as Utils from './Utils'
import Encoder from '../Binary/Encoder'
import Decoder from '../Binary/Decoder'
import fetch, { RequestRedirect } from 'node-fetch'
import got, { Method } from 'got'
import {
AuthenticationCredentials,
WAUser,
@@ -24,7 +24,8 @@ import {
} from './Constants'
import { EventEmitter } from 'events'
import KeyedDB from '@adiwajshing/keyed-db'
import { STATUS_CODES, Agent } from 'http'
import { STATUS_CODES } from 'http'
import { Agent } from 'https'
import pino from 'pino'
const logger = pino({ prettyPrint: { levelFirst: true, ignore: 'hostname', translateTime: true }, prettifier: require('pino-pretty') })
@@ -458,13 +459,20 @@ export class WAConnection extends EventEmitter {
/**
* Does a fetch request with the configuration of the connection
*/
protected fetchRequest = (endpoint: string, method: string = 'GET', body?: any, agent?: Agent, headers?: {[k: string]: string}, redirect: RequestRedirect = 'follow') => (
fetch(endpoint, {
protected fetchRequest = (
endpoint: string,
method: Method = 'GET',
body?: any,
agent?: Agent,
headers?: {[k: string]: string},
followRedirect = true
) => (
got(endpoint, {
method,
body,
redirect,
followRedirect,
headers: { Origin: DEFAULT_ORIGIN, ...(headers || {}) },
agent: agent || this.connectOptions.fetchAgent
agent: { https: agent || this.connectOptions.fetchAgent }
})
)
generateMessageTag (longTag: boolean = false) {

View File

@@ -33,10 +33,10 @@ export class WAConnection extends Base {
isOnWhatsAppNoConn = async (str: string) => {
let phone = str.split('@')[0]
const url = `https://wa.me/${phone}`
const response = await this.fetchRequest(url, 'GET', undefined, undefined, undefined, 'manual')
const loc = response.headers.get('Location')
const response = await this.fetchRequest(url, 'GET', undefined, undefined, undefined, false)
const loc = response.headers['Location'] as string
if (!loc) {
this.logger.warn({ url, status: response.status }, 'did not get location from request')
this.logger.warn({ url, status: response.statusCode }, 'did not get location from request')
return
}
const locUrl = new URL('', loc)

View File

@@ -1,5 +1,5 @@
import {WAConnection as Base} from './5.User'
import {promises as fs} from 'fs'
import {createReadStream, promises as fs} from 'fs'
import {
MessageOptions,
MessageType,
@@ -9,10 +9,11 @@ import {
WALocationMessage,
WAContactMessage,
WATextMessage,
WAMessageContent, WAMetric, WAFlag, WAMessage, BaileysError, WA_MESSAGE_STATUS_TYPE, WAMessageProto, MediaConnInfo, MessageTypeProto, URL_REGEX, WAUrlInfo, WA_DEFAULT_EPHEMERAL
WAMessageContent, WAMetric, WAFlag, WAMessage, BaileysError, WA_MESSAGE_STATUS_TYPE, WAMessageProto, MediaConnInfo, MessageTypeProto, URL_REGEX, WAUrlInfo, WA_DEFAULT_EPHEMERAL, WAMediaUpload
} from './Constants'
import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes, generateThumbnail, getMediaKeys, decodeMediaMessageBuffer, extensionForMediaMessage, whatsappID, unixTimestampSeconds, getAudioDuration, newMessagesDB } from './Utils'
import { generateMessageID, extensionForMediaMessage, whatsappID, unixTimestampSeconds, getAudioDuration, newMessagesDB, encryptedStream, decryptMediaMessageBuffer, generateThumbnail } from './Utils'
import { Mutex } from './Mutex'
import { Readable } from 'stream'
export class WAConnection extends Base {
/**
@@ -116,9 +117,7 @@ export class WAConnection extends Base {
return WAMessageProto.Message.fromObject(content)
}
/** Prepare a media message for sending */
async prepareMessageMedia(buffer: Buffer, mediaType: MessageType, options: MessageOptions = {}) {
await this.waitForConnection ()
async prepareMessageMedia(media: WAMediaUpload, mediaType: MessageType, options: MessageOptions = {}) {
if (mediaType === MessageType.document && !options.mimetype) {
throw new Error('mimetype required to send a document')
}
@@ -133,33 +132,31 @@ export class WAConnection extends Base {
isGIF = true
options.mimetype = MimetypeMap[MessageType.video]
}
// generate a media key
const mediaKey = randomBytes(32)
const mediaKeys = getMediaKeys(mediaKey, mediaType)
const enc = aesEncrypWithIV(buffer, mediaKeys.cipherKey, mediaKeys.iv)
const mac = hmacSign(Buffer.concat([mediaKeys.iv, enc]), mediaKeys.macKey).slice(0, 10)
const body = Buffer.concat([enc, mac]) // body is enc + mac
const fileSha256 = sha256(buffer)
const fileEncSha256 = sha256(body)
const {
mediaKey,
encBodyPath,
bodyPath,
fileEncSha256,
fileSha256,
fileLength
} = await encryptedStream(media, mediaType)
// url safe Base64 encode the SHA256 hash of the body
const fileEncSha256B64 = encodeURIComponent(
fileEncSha256
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/\=+$/, '')
)
await generateThumbnail(buffer, mediaType, options)
const fileEncSha256B64 = encodeURIComponent(
fileEncSha256.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/\=+$/, '')
)
await generateThumbnail(bodyPath, mediaType, options)
if (mediaType === MessageType.audio && !options.duration) {
try {
options.duration = await getAudioDuration (buffer)
options.duration = await getAudioDuration(bodyPath)
} catch (error) {
this.logger.debug ({ error }, 'failed to obtain audio duration: ' + error.message)
}
}
// send a query JSON to obtain the url & auth token to upload our media
let json = await this.refreshMediaConn (options.forceNewMediaOptions)
let json = await this.refreshMediaConn(options.forceNewMediaOptions)
let mediaUrl: string
for (let host of json.hosts) {
@@ -167,8 +164,14 @@ export class WAConnection extends Base {
const url = `https://${host.hostname}${MediaPathMap[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
try {
const urlFetch = await this.fetchRequest(url, 'POST', body, options.uploadAgent, { 'Content-Type': 'application/octet-stream' })
const result = await urlFetch.json()
const {body: responseText} = await this.fetchRequest(
url,
'POST',
createReadStream(encBodyPath),
options.uploadAgent,
{ 'Content-Type': 'application/octet-stream' }
)
const result = JSON.parse(responseText)
mediaUrl = result?.url
if (mediaUrl) break
@@ -178,7 +181,7 @@ export class WAConnection extends Base {
}
} catch (error) {
const isLast = host.hostname === json.hosts[json.hosts.length-1].hostname
this.logger.error (`Error in uploading to ${host.hostname}${isLast ? '' : ', retrying...'}`)
this.logger.error (`Error in uploading to ${host.hostname} (${error}) ${isLast ? '' : ', retrying...'}`)
}
}
if (!mediaUrl) throw new Error('Media upload failed on all hosts')
@@ -191,7 +194,7 @@ export class WAConnection extends Base {
mimetype: options.mimetype,
fileEncSha256: fileEncSha256,
fileSha256: fileSha256,
fileLength: buffer.length,
fileLength: fileLength,
seconds: options.duration,
fileName: options.filename || 'file',
gifPlayback: isGIF || undefined,
@@ -317,24 +320,39 @@ export class WAConnection extends Base {
})
Object.keys (response[1]).forEach (key => content[key] = response[1][key]) // update message
}
async downloadMediaMessage (message: WAMessage): Promise<Buffer>
async downloadMediaMessage (message: WAMessage, type: 'buffer'): Promise<Buffer>
async downloadMediaMessage (message: WAMessage, type: 'stream'): Promise<Readable>
/**
* Securely downloads the media from the message.
* Renews the download url automatically, if necessary.
*/
@Mutex (message => message?.key?.id)
async downloadMediaMessage (message: WAMessage) {
async downloadMediaMessage (message: WAMessage, type: 'buffer' | 'stream' = 'buffer') {
let mContent = message.message?.ephemeralMessage?.message || message.message
if (!mContent) throw new BaileysError('No message present', { status: 400 })
const downloadMediaMessage = async () => {
const stream = await decryptMediaMessageBuffer(mContent)
if(type === 'buffer') {
let buffer = Buffer.from([])
for await(const chunk of stream) {
buffer = Buffer.concat([buffer, chunk])
}
return buffer
}
return stream
}
try {
const buff = await decodeMediaMessageBuffer (mContent, this.fetchRequest)
const buff = await downloadMediaMessage()
return buff
} catch (error) {
if (error instanceof BaileysError && error.status === 404) { // media needs to be updated
this.logger.info (`updating media of message: ${message.key.id}`)
await this.updateMediaMessage (message)
mContent = message.message?.ephemeralMessage?.message || message.message
const buff = await decodeMediaMessageBuffer (mContent, this.fetchRequest)
const buff = await downloadMediaMessage()
return buff
}
throw error
@@ -348,10 +366,11 @@ export class WAConnection extends Base {
* @param attachExtension should the parsed extension be applied automatically to the file
*/
async downloadAndSaveMediaMessage (message: WAMessage, filename: string, attachExtension: boolean=true) {
const buffer = await this.downloadMediaMessage (message)
const extension = extensionForMediaMessage (message.message)
const trueFileName = attachExtension ? (filename + '.' + extension) : filename
await fs.writeFile (trueFileName, buffer)
const buffer = await this.downloadMediaMessage(message)
await fs.writeFile(trueFileName, buffer)
return trueFileName
}
/** Query a string to check if it has a url, if it does, return required extended text message */

View File

@@ -2,6 +2,7 @@ import { WA } from '../Binary/Constants'
import { proto } from '../../WAMessage/WAMessage'
import { Agent } from 'https'
import KeyedDB from '@adiwajshing/keyed-db'
import { URL } from 'url'
export const WS_URL = 'wss://web.whatsapp.com/ws'
export const DEFAULT_ORIGIN = 'https://web.whatsapp.com'
@@ -70,6 +71,9 @@ export interface WAQuery {
startDebouncedTimeout?: boolean
maxRetries?: number
}
export type WAMediaUpload = Buffer | { url: URL | string }
export enum ReconnectMode {
/** does not reconnect */
off = 0,

View File

@@ -1,16 +1,20 @@
import * as Crypto from 'crypto'
import { Readable, Transform } from 'stream'
import HKDF from 'futoin-hkdf'
import Jimp from 'jimp'
import {promises as fs} from 'fs'
import {createReadStream, createWriteStream, promises as fs, WriteStream} from 'fs'
import { exec } from 'child_process'
import {platform, release} from 'os'
import {platform, release, tmpdir} from 'os'
import HttpsProxyAgent from 'https-proxy-agent'
import { URL } from 'url'
import { Agent } from 'https'
import Decoder from '../Binary/Decoder'
import { MessageType, HKDFInfoKeys, MessageOptions, WAChat, WAMessageContent, BaileysError, WAMessageProto, TimedOutError, CancelledError, WAGenericMediaMessage, WAMessage, WAMessageKey } from './Constants'
import { MessageType, HKDFInfoKeys, MessageOptions, WAChat, WAMessageContent, BaileysError, WAMessageProto, TimedOutError, CancelledError, WAGenericMediaMessage, WAMessage, WAMessageKey, DEFAULT_ORIGIN, WAMediaUpload } from './Constants'
import KeyedDB from '@adiwajshing/keyed-db'
import { Response } from 'node-fetch'
import got, { Options, Response } from 'got'
import { join } from 'path'
import { IAudioMetadata } from 'music-metadata'
const platformMap = {
'aix': 'AIX',
@@ -222,9 +226,10 @@ const extractVideoThumb = async (
})
}) as Promise<void>
export const compressImage = async (buffer: Buffer) => {
const jimp = await Jimp.read (buffer)
return jimp.resize(48, 48).getBufferAsync (Jimp.MIME_JPEG)
export const compressImage = async (bufferOrFilePath: Buffer | string) => {
const jimp = await Jimp.read(bufferOrFilePath as any)
const result = await jimp.resize(48, 48).getBufferAsync(Jimp.MIME_JPEG)
return result
}
export const generateProfilePicture = async (buffer: Buffer) => {
const jimp = await Jimp.read (buffer)
@@ -241,42 +246,131 @@ export const mediaMessageSHA256B64 = (message: WAMessageContent) => {
const media = Object.values(message)[0] as WAGenericMediaMessage
return media?.fileSha256 && Buffer.from(media.fileSha256).toString ('base64')
}
export async function getAudioDuration (buffer: Buffer) {
export async function getAudioDuration (buffer: Buffer | string) {
const musicMetadata = await import ('music-metadata')
const metadata = await musicMetadata.parseBuffer (buffer, null, {duration: true});
let metadata: IAudioMetadata
if(Buffer.isBuffer(buffer)) {
metadata = await musicMetadata.parseBuffer(buffer, null, { duration: true })
} else {
const rStream = createReadStream(buffer)
metadata = await musicMetadata.parseStream(rStream, null, { duration: true })
rStream.close()
}
return metadata.format.duration;
}
export const toReadable = (buffer: Buffer) => {
const readable = new Readable({ read: () => {} })
readable.push(buffer)
readable.push(null)
return readable
}
export const getStream = async (item: WAMediaUpload) => {
if(Buffer.isBuffer(item)) return { stream: toReadable(item), type: 'buffer' }
if(item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {
return { stream: await getGotStream(item.url), type: 'remote' }
}
return { stream: createReadStream(item.url), type: 'file' }
}
/** generates a thumbnail for a given media, if required */
export async function generateThumbnail(buffer: Buffer, mediaType: MessageType, info: MessageOptions) {
export async function generateThumbnail(file: string, mediaType: MessageType, info: MessageOptions) {
if ('thumbnail' in info) {
// don't do anything if the thumbnail is already provided, or is null
if (mediaType === MessageType.audio) {
throw new Error('audio messages cannot have thumbnails')
}
} else if (mediaType === MessageType.image) {
const buff = await compressImage (buffer)
const buff = await compressImage(file)
info.thumbnail = buff.toString('base64')
} else if (mediaType === MessageType.video) {
const filename = './' + randomBytes(5).toString('hex') + '.mp4'
const imgFilename = filename + '.jpg'
await fs.writeFile(filename, buffer)
const imgFilename = join(tmpdir(), generateMessageID() + '.jpg')
try {
await extractVideoThumb(filename, imgFilename, '00:00:00', { width: 48, height: 48 })
await extractVideoThumb(file, imgFilename, '00:00:00', { width: 48, height: 48 })
const buff = await fs.readFile(imgFilename)
info.thumbnail = buff.toString('base64')
await fs.unlink(imgFilename)
} catch (err) {
console.log('could not generate video thumb: ' + err)
}
await fs.unlink(filename)
}
}
export const getGotStream = async(url: string | URL, options: Options & { isStream?: true } = {}) => {
const fetched = got.stream(url, options)
await new Promise((resolve, reject) => {
fetched.once('response', ({statusCode: status}: Response) => {
if (status >= 400) {
reject(new BaileysError (
'Invalid code (' + status + ') returned',
{ status }
))
} else {
resolve(undefined)
}
})
})
return fetched
}
export const encryptedStream = async(media: WAMediaUpload, mediaType: MessageType) => {
const { stream, type } = await getStream(media)
const mediaKey = randomBytes(32)
const {cipherKey, iv, macKey} = getMediaKeys(mediaKey, mediaType)
// random name
const encBodyPath = join(tmpdir(), mediaType + generateMessageID() + '.enc')
const encWriteStream = createWriteStream(encBodyPath)
let bodyPath: string
let writeStream: WriteStream
if(type === 'file') {
bodyPath = (media as any).url
} else {
bodyPath = join(tmpdir(), mediaType + generateMessageID())
writeStream = createWriteStream(bodyPath)
}
let fileLength = 0
const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv)
let hmac = Crypto.createHmac('sha256', macKey).update(iv)
let sha256Plain = Crypto.createHash('sha256')
let sha256Enc = Crypto.createHash('sha256')
const onChunk = (buff: Buffer) => {
sha256Enc = sha256Enc.update(buff)
hmac = hmac.update(buff)
encWriteStream.write(buff)
}
for await(const data of stream) {
fileLength += data.length
sha256Plain = sha256Plain.update(data)
writeStream && writeStream.write(data)
onChunk(aes.update(data))
}
onChunk(aes.final())
const mac = hmac.digest().slice(0, 10)
sha256Enc = sha256Enc.update(mac)
const fileSha256 = sha256Plain.digest()
const fileEncSha256 = sha256Enc.digest()
encWriteStream.write(mac)
encWriteStream.close()
writeStream && writeStream.close()
return {
mediaKey,
encBodyPath,
bodyPath,
mac,
fileEncSha256,
fileSha256,
fileLength
}
}
/**
* Decode a media message (video, image, document, audio) & return decrypted buffer
* @param message the media message you want to decode
*/
export async function decodeMediaMessageBuffer(message: WAMessageContent, fetchRequest: (host: string, method: string) => Promise<Response>) {
export async function decryptMediaMessageBuffer(message: WAMessageContent): Promise<Readable> {
/*
One can infer media type from the key in the message
it is usually written as [mediaType]Message. Eg. imageMessage, audioMessage etc.
@@ -289,7 +383,11 @@ export async function decodeMediaMessageBuffer(message: WAMessageContent, fetchR
throw new BaileysError('cannot decode text message', message)
}
if (type === MessageType.location || type === MessageType.liveLocation) {
return Buffer.from(message[type].jpegThumbnail)
const buffer = Buffer.from(message[type].jpegThumbnail)
const readable = new Readable({ read: () => {} })
readable.push(buffer)
readable.push(null)
return readable
}
let messageContent: WAGenericMediaMessage
if (message.productMessage) {
@@ -299,41 +397,39 @@ export async function decodeMediaMessageBuffer(message: WAMessageContent, fetchR
} else {
messageContent = message[type]
}
// download the message
const fetched = await fetchRequest(messageContent.url, 'GET')
if (fetched.status >= 400) {
throw new BaileysError ('Invalid code (' + fetched.status + ') returned. File has possibly been deleted from WA servers. Run `client.updateMediaMessage()` to refresh the url', {status: fetched.status})
}
const buffer = await fetched.buffer()
const fetched = await getGotStream(messageContent.url, {
headers: { Origin: DEFAULT_ORIGIN }
})
let remainingBytes = Buffer.from([])
const { cipherKey, iv } = getMediaKeys(messageContent.mediaKey, type)
const aes = Crypto.createDecipheriv("aes-256-cbc", cipherKey, iv)
const decryptedMedia = (type: MessageType) => {
// get the keys to decrypt the message
const mediaKeys = getMediaKeys(messageContent.mediaKey, type) //getMediaKeys(Buffer.from(messageContent.mediaKey, 'base64'), type)
// first part is actual file
const file = buffer.slice(0, buffer.length - 10)
// last 10 bytes is HMAC sign of file
const mac = buffer.slice(buffer.length - 10, buffer.length)
// sign IV+file & check for match with mac
const testBuff = Buffer.concat([mediaKeys.iv, file])
const sign = hmacSign(testBuff, mediaKeys.macKey).slice(0, 10)
// our sign should equal the mac
if (!sign.equals(mac)) throw new Error()
return aesDecryptWithIV(file, mediaKeys.cipherKey, mediaKeys.iv) // decrypt media
}
const allTypes = [type, ...Object.keys(HKDFInfoKeys)]
for (let i = 0; i < allTypes.length;i++) {
try {
const decrypted = decryptedMedia (allTypes[i] as MessageType)
if (i > 0) { console.log (`decryption of ${type} media with HKDF key of ${allTypes[i]}`) }
return decrypted
} catch {
if (i === 0) { console.log (`decryption of ${type} media with original HKDF key failed`) }
}
}
throw new BaileysError('Decryption failed, HMAC sign does not match', {status: 400})
const output = new Transform({
transform(chunk, _, callback) {
let data = Buffer.concat([remainingBytes, chunk])
const decryptLength =
Math.floor(data.length / 16) * 16
remainingBytes = data.slice(decryptLength)
data = data.slice(0, decryptLength)
try {
this.push(aes.update(data))
callback()
} catch(error) {
callback(error)
}
},
final(callback) {
try {
this.push(aes.final())
callback()
} catch(error) {
callback(error)
}
},
})
return fetched.pipe(output, { end: true })
}
export function extensionForMediaMessage(message: WAMessageContent) {
const getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1]

View File

@@ -2,4 +2,4 @@ export * from '../WAMessage/WAMessage'
export * from './Binary/Constants'
export * from './Binary/Decoder'
export * from './Binary/Encoder'
export * from './WAConnection/WAConnection'
export * from './WAConnection'

224
yarn.lock
View File

@@ -362,16 +362,59 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
"@sindresorhus/is@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.0.tgz#2ff674e9611b45b528896d820d3d7a812de2f0e4"
integrity sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ==
"@szmarczak/http-timer@^4.0.5":
version "4.0.5"
resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152"
integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==
dependencies:
defer-to-connect "^2.0.0"
"@tokenizer/token@^0.1.0", "@tokenizer/token@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.1.1.tgz#f0d92c12f87079ddfd1b29f614758b9696bc29e3"
integrity sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w==
"@types/cacheable-request@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976"
integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==
dependencies:
"@types/http-cache-semantics" "*"
"@types/keyv" "*"
"@types/node" "*"
"@types/responselike" "*"
"@types/debug@^4.1.5":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd"
integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==
"@types/got@^9.6.11":
version "9.6.11"
resolved "https://registry.yarnpkg.com/@types/got/-/got-9.6.11.tgz#482b402cc5ee459481aeeadb08142ebb1a9afb26"
integrity sha512-dr3IiDNg5TDesGyuwTrN77E1Cd7DCdmCFtEfSGqr83jMMtcwhf/SGPbN2goY4JUWQfvxwY56+e5tjfi+oXeSdA==
dependencies:
"@types/node" "*"
"@types/tough-cookie" "*"
form-data "^2.5.0"
"@types/http-cache-semantics@*":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a"
integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==
"@types/keyv@*":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7"
integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==
dependencies:
"@types/node" "*"
"@types/long@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
@@ -382,14 +425,6 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-7.0.2.tgz#b17f16cf933597e10d6d78eae3251e692ce8b0ce"
integrity sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==
"@types/node-fetch@^2.5.7":
version "2.5.7"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c"
integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*", "@types/node@^14.6.2":
version "14.14.17"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.17.tgz#29fab92f3986c0e379968ad3c2043683d8020dbb"
@@ -424,6 +459,13 @@
"@types/node" "*"
safe-buffer "*"
"@types/responselike@*", "@types/responselike@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
dependencies:
"@types/node" "*"
"@types/sonic-boom@*":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@types/sonic-boom/-/sonic-boom-0.7.0.tgz#38337036293992a1df65dd3161abddf8fb9b7176"
@@ -441,6 +483,11 @@
resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1"
integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==
"@types/tough-cookie@*":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d"
integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==
"@types/ws@^7.2.6":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.0.tgz#499690ea08736e05a8186113dac37769ab251a0e"
@@ -624,6 +671,24 @@ buffer@^5.2.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"
cacheable-lookup@^5.0.3:
version "5.0.4"
resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005"
integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==
cacheable-request@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58"
integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==
dependencies:
clone-response "^1.0.2"
get-stream "^5.1.0"
http-cache-semantics "^4.0.0"
keyv "^4.0.0"
lowercase-keys "^2.0.0"
normalize-url "^4.1.0"
responselike "^2.0.0"
call-bind@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce"
@@ -701,6 +766,13 @@ cliui@^5.0.0:
strip-ansi "^5.2.0"
wrap-ansi "^5.1.0"
clone-response@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
dependencies:
mimic-response "^1.0.0"
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -730,7 +802,7 @@ colors@^1.4.0:
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
combined-stream@^1.0.8:
combined-stream@^1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
@@ -801,6 +873,18 @@ decamelize@^4.0.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
dependencies:
mimic-response "^3.1.0"
defer-to-connect@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1"
integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==
define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@@ -976,13 +1060,13 @@ foreach@^2.0.5:
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
form-data@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
form-data@^2.5.0:
version "2.5.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
combined-stream "^1.0.6"
mime-types "^2.1.12"
fs-extra@^9.0.1:
@@ -1034,6 +1118,13 @@ get-stdin@^4.0.1:
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
get-stream@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
dependencies:
pump "^3.0.0"
gifwrap@^0.9.2:
version "0.9.2"
resolved "https://registry.yarnpkg.com/gifwrap/-/gifwrap-0.9.2.tgz#348e286e67d7cf57942172e1e6f05a71cee78489"
@@ -1069,6 +1160,23 @@ global@~4.4.0:
min-document "^2.19.0"
process "^0.11.10"
got@^11.8.1:
version "11.8.1"
resolved "https://registry.yarnpkg.com/got/-/got-11.8.1.tgz#df04adfaf2e782babb3daabc79139feec2f7e85d"
integrity sha512-9aYdZL+6nHmvJwHALLwKSUZ0hMwGaJGYv3hoPLPgnT8BoBXm1SjnZeky+91tfwJaDzun2s4RsBRy48IEYv2q2Q==
dependencies:
"@sindresorhus/is" "^4.0.0"
"@szmarczak/http-timer" "^4.0.5"
"@types/cacheable-request" "^6.0.1"
"@types/responselike" "^1.0.0"
cacheable-lookup "^5.0.3"
cacheable-request "^7.0.1"
decompress-response "^6.0.0"
http2-wrapper "^1.0.0-beta.5.2"
lowercase-keys "^2.0.0"
p-cancelable "^2.0.0"
responselike "^2.0.0"
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.4"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
@@ -1123,6 +1231,19 @@ hosted-git-info@^2.1.4:
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
http-cache-semantics@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
http2-wrapper@^1.0.0-beta.5.2:
version "1.0.0-beta.5.2"
resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz#8b923deb90144aea65cf834b016a340fc98556f3"
integrity sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==
dependencies:
quick-lru "^5.1.1"
resolve-alpn "^1.0.0"
https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
@@ -1331,6 +1452,11 @@ js-yaml@3.14.0:
argparse "^1.0.7"
esprima "^4.0.0"
json-buffer@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
json5@^2.1.0:
version "2.1.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
@@ -1347,6 +1473,13 @@ jsonfile@^6.0.1:
optionalDependencies:
graceful-fs "^4.1.6"
keyv@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254"
integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==
dependencies:
json-buffer "3.0.1"
leven@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
@@ -1417,6 +1550,11 @@ loud-rejection@^1.0.0:
currently-unhandled "^0.4.1"
signal-exit "^3.0.0"
lowercase-keys@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -1465,23 +1603,33 @@ meow@^3.3.0:
redent "^1.0.0"
trim-newlines "^1.0.0"
mime-db@1.44.0:
version "1.44.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
mime-db@1.45.0:
version "1.45.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
mime-types@^2.1.12:
version "2.1.27"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
version "2.1.28"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd"
integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==
dependencies:
mime-db "1.44.0"
mime-db "1.45.0"
mime@^1.3.4:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mimic-response@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
min-document@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
@@ -1576,11 +1724,6 @@ neo-async@^2.6.0:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
node-fetch@^2.6.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -1596,6 +1739,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
normalize-url@^4.1.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129"
integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
object-assign@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -1648,6 +1796,11 @@ onigasm@^2.2.5:
dependencies:
lru-cache "^5.1.1"
p-cancelable@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e"
integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==
p-limit@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
@@ -1877,6 +2030,11 @@ quick-format-unescaped@^4.0.1:
resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.1.tgz#437a5ea1a0b61deb7605f8ab6a8fd3858dbeb701"
integrity sha512-RyYpQ6Q5/drsJyOhrWHYMWTedvjTIat+FTwv0K4yoUxzvekw2aRHMQJLlnvt8UantkZg2++bEzD9EdxXqkWf4A==
quick-lru@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -1962,6 +2120,11 @@ require-main-filename@^2.0.0:
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
resolve-alpn@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.0.0.tgz#745ad60b3d6aff4b4a48e01b8c0bdc70959e0e8c"
integrity sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==
resolve@^1.0.0, resolve@^1.1.6, resolve@^1.10.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
@@ -1970,6 +2133,13 @@ resolve@^1.0.0, resolve@^1.1.6, resolve@^1.10.0:
is-core-module "^2.1.0"
path-parse "^1.0.6"
responselike@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723"
integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==
dependencies:
lowercase-keys "^2.0.0"
rimraf@^2.6.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"