diff --git a/Example/example.ts b/Example/example.ts index 56efc2c..6f2c036 100644 --- a/Example/example.ts +++ b/Example/example.ts @@ -9,7 +9,7 @@ import { ReconnectMode, ProxyAgent, waChatKey, -} from '../src/WAConnection/WAConnection' +} from '../src/WAConnection' import * as fs from 'fs' async function example() { diff --git a/Lmao.mp4 b/Lmao.mp4 new file mode 100644 index 0000000..0e4c62a Binary files /dev/null and b/Lmao.mp4 differ diff --git a/auth_info copy.json b/auth_info copy.json new file mode 100644 index 0000000..7668ae6 --- /dev/null +++ b/auth_info copy.json @@ -0,0 +1,7 @@ +{ + "clientID": "wh1OmghfONDRWXt0u/yxfg==", + "serverToken": "1@nBrK6gYz8n+hj46rHcL18n6v08fUBKSDRCD8NvPq9qETNkF/2lyUHbCQQMlDjasHHwVAUp1BBkGQ==", + "clientToken": "LdThbdiLt7yFfyClGQ1fbh5GgN/oiEwvWCg+9gb04wYB7P4f2UzEeY4IDr2rV9Qg1W1Lm+uVUW4lVYbrzX0scQ==", + "encKey": "tHt3uFN7+IOgWccQKAnUo1JDbR9y+rWzEfuddNV1rTo=", + "macKey": "NJo0Bj1Ia7f0+s82gZKEDUgrhfmvc9lbSjgKlnc5Ci4=" +} \ No newline at end of file diff --git a/package.json b/package.json index 60627a6..f41ce4b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/BrowserMessageDecoding.ts b/src/BrowserMessageDecoding.ts index 455679f..addcbec 100644 --- a/src/BrowserMessageDecoding.ts +++ b/src/BrowserMessageDecoding.ts @@ -1,5 +1,5 @@ import fs from 'fs' -import { decryptWA } from './WAConnection/WAConnection' +import { decryptWA } from './WAConnection' import Decoder from './Binary/Decoder' interface BrowserMessagesInfo { diff --git a/src/Tests/Common.ts b/src/Tests/Common.ts index bc7aebe..a35486b 100644 --- a/src/Tests/Common.ts +++ b/src/Tests/Common.ts @@ -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' diff --git a/src/Tests/Tests.Connect.ts b/src/Tests/Tests.Connect.ts index 9848c1c..2608ffb 100644 --- a/src/Tests/Tests.Connect.ts +++ b/src/Tests/Tests.Connect.ts @@ -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' diff --git a/src/Tests/Tests.Groups.ts b/src/Tests/Tests.Groups.ts index 5287f1c..680ba3f 100644 --- a/src/Tests/Tests.Groups.ts +++ b/src/Tests/Tests.Groups.ts @@ -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' diff --git a/src/Tests/Tests.Media.ts b/src/Tests/Tests.Media.ts new file mode 100644 index 0000000..34426ad --- /dev/null +++ b/src/Tests/Tests.Media.ts @@ -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 }, + ] + }) + +})*/ \ No newline at end of file diff --git a/src/Tests/Tests.Messages.ts b/src/Tests/Tests.Messages.ts index 1444a99..4df90b2 100644 --- a/src/Tests/Tests.Messages.ts +++ b/src/Tests/Tests.Messages.ts @@ -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) }) diff --git a/src/Tests/Tests.Misc.ts b/src/Tests/Tests.Misc.ts index c2e6f35..9329791 100644 --- a/src/Tests/Tests.Misc.ts +++ b/src/Tests/Tests.Misc.ts @@ -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) diff --git a/src/WAConnection/0.Base.ts b/src/WAConnection/0.Base.ts index 96e5d59..f953f83 100644 --- a/src/WAConnection/0.Base.ts +++ b/src/WAConnection/0.Base.ts @@ -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) { diff --git a/src/WAConnection/5.User.ts b/src/WAConnection/5.User.ts index 3eb8ba3..5f8ca0e 100644 --- a/src/WAConnection/5.User.ts +++ b/src/WAConnection/5.User.ts @@ -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) diff --git a/src/WAConnection/6.MessagesSend.ts b/src/WAConnection/6.MessagesSend.ts index 05f0c2d..c0e44f8 100644 --- a/src/WAConnection/6.MessagesSend.ts +++ b/src/WAConnection/6.MessagesSend.ts @@ -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 + async downloadMediaMessage (message: WAMessage, type: 'buffer'): Promise + async downloadMediaMessage (message: WAMessage, type: 'stream'): Promise /** * 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 */ diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index 4c3e95d..98339d3 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -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, diff --git a/src/WAConnection/Utils.ts b/src/WAConnection/Utils.ts index e458059..0d0d459 100644 --- a/src/WAConnection/Utils.ts +++ b/src/WAConnection/Utils.ts @@ -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 -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) { +export async function decryptMediaMessageBuffer(message: WAMessageContent): Promise { /* One can infer media type from the key in the message it is usually written as [mediaType]Message. Eg. imageMessage, audioMessage etc. @@ -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] diff --git a/src/WAConnection/WAConnection.ts b/src/WAConnection/index.ts similarity index 100% rename from src/WAConnection/WAConnection.ts rename to src/WAConnection/index.ts diff --git a/src/index.ts b/src/index.ts index 7b32d8e..34a105f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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' diff --git a/yarn.lock b/yarn.lock index 204b306..6c5acbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"