Skip to content

Commit c5c4e0d

Browse files
committed
⚡️ improved media sending process #2675
1 parent 7779e77 commit c5c4e0d

File tree

2 files changed

+147
-23
lines changed

2 files changed

+147
-23
lines changed

src/api/Client.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { Listener } from 'eventemitter2';
3434
import PriorityQueue from 'p-queue/dist/priority-queue';
3535
import { MessagePreprocessors } from '../structures/preProcessors';
3636
import { NextFunction, Request, Response } from 'express';
37-
import { base64MimeType, ensureDUrl, generateGHIssueLink, getDUrl, isBase64, isDataURL, now } from '../utils/tools';
37+
import { assertFile, base64MimeType, ensureDUrl, FileOutputTypes, generateGHIssueLink, getDUrl, isBase64, isDataURL, now, rmFileAsync } from '../utils/tools';
3838
import { Call } from './model/call';
3939
import { AdvancedButton, Button, LocationButtonBody, Section } from './model/button';
4040
import { JsonObject } from 'type-fest';
@@ -102,6 +102,7 @@ declare module WAPI {
102102
const getMessageById: (mesasgeId: string) => Message;
103103
const getMessageInfo: (mesasgeId: string) => Promise<any>;
104104
const getOrder: (id: string) => Order;
105+
const createTemporaryFileInput: () => any;
105106
const getMyLastMessage: (chatId: string) => Promise<Message>;
106107
const getStarredMessages: (chatId: string) => Promise<Message[]>;
107108
const starMessage: (messageId: string) => Promise<boolean>;
@@ -1743,32 +1744,47 @@ public async testCallback(callbackToTest: SimpleListener, testData: any) : Prom
17431744
ptt?:boolean,
17441745
withoutPreview?:boolean,
17451746
hideTags ?: boolean,
1746-
viewOnce ?: boolean
1747+
viewOnce ?: boolean,
1748+
requestConfig ?: any
17471749
) : Promise<MessageId | boolean> {
1748-
//check if the 'base64' file exists
1749-
if(!isDataURL(file) && !isBase64(file) && !file.includes("data:")) {
1750-
//must be a file then
1751-
const relativePath = path.join(path.resolve(process.cwd(),file|| ''));
1752-
if(fs.existsSync(file) || fs.existsSync(relativePath)) {
1753-
file = await datauri(fs.existsSync(file) ? file : relativePath);
1754-
} else if(isUrl(file)){
1755-
return await this.sendFileFromUrl(to,file,filename,caption,quotedMsgId,{},waitForId,ptt,withoutPreview, hideTags, viewOnce);
1756-
} else throw new CustomError(ERROR_NAME.FILE_NOT_FOUND,`Cannot find file. Make sure the file reference is relative, a valid URL or a valid DataURL: ${file.slice(0,25)}`)
1757-
} else if(file.includes("data:") && file.includes("undefined") || file.includes("application/octet-stream") && filename && mime.lookup(filename)) {
1758-
file = `data:${mime.lookup(filename)};base64,${file.split(',')[1]}`
1759-
}
1750+
const err = [
1751+
'Not able to send message to broadcast',
1752+
'Not a contact',
1753+
'Error: Number not linked to WhatsApp Account',
1754+
'ERROR: Please make sure you have at least one chat'
1755+
];
1756+
1757+
/**
1758+
* TODO: File upload improvements
1759+
* 1. *Create an arbitrary file input element
1760+
* 2. *Take the file parameter and create a tempfile in temp dir
1761+
* 3. Forward the tempfile path to the file input, upload the file to the browser context.
1762+
* 4. Instruct the WAPI.sendImage function to consume the file from the element in step 1.
1763+
* 5. *Destroy the input element from the page (happens in wapi.sendimage)
1764+
* 6. *Unlink/rm the tempfile
1765+
* 7. Return the ID of the WAPI.sendImage function.
1766+
*/
1767+
const [[inputElementId, inputElement], fileAsLocalTemp] = await Promise.all([
1768+
(async ()=>{
1769+
const inputElementId = await this._page.evaluate(()=>WAPI.createTemporaryFileInput());
1770+
const inputElement = await this._page.$(`#${inputElementId}`);
1771+
return [inputElementId, inputElement];
1772+
})(),
1773+
assertFile(file, filename, FileOutputTypes.TEMP_FILE_PATH as any,requestConfig || {})
1774+
])
1775+
await inputElement.uploadFile(fileAsLocalTemp as string);
1776+
file = inputElementId;
17601777

1761-
const err = [
1762-
'Not able to send message to broadcast',
1763-
'Not a contact',
1764-
'Error: Number not linked to WhatsApp Account',
1765-
'ERROR: Please make sure you have at least one chat'
1766-
];
1778+
/**
1779+
* Old method of asserting that the file be a data url - cons = time wasted serializing/deserializing large file to and from b64.
1780+
*/
1781+
// file = await assertFile(file, filename, FileOutputTypes.DATA_URL as any,requestConfig || {}) as string
17671782

17681783
const res = await this.pup(
17691784
({ to, file, filename, caption, quotedMsgId, waitForId, ptt, withoutPreview, hideTags, viewOnce}) => WAPI.sendImage(file, to, filename, caption, quotedMsgId, waitForId, ptt, withoutPreview, hideTags, viewOnce),
17701785
{ to, file, filename, caption, quotedMsgId, waitForId, ptt, withoutPreview, hideTags, viewOnce}
17711786
)
1787+
if(fileAsLocalTemp) await rmFileAsync(fileAsLocalTemp as string)
17721788
if(err.includes(res)) console.error(res);
17731789
return (err.includes(res) ? false : res) as MessageId | boolean;
17741790
}

src/utils/tools.ts

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1+
import Crypto from "crypto";
12
import * as fs from 'fs'
23
import * as path from 'path';
34
import datauri from 'datauri'
45
import isUrl from 'is-url-superb'
56
import { JsonObject } from 'type-fest';
6-
import { ConfigObject, CustomError, DataURL, ERROR_NAME } from '../api/model';
7+
import { AdvancedFile, ConfigObject, CustomError, DataURL, ERROR_NAME } from '../api/model';
78
import { default as axios, AxiosRequestConfig } from 'axios';
89
import { SessionInfo } from '../api/model/sessionInfo';
910
import { execSync } from 'child_process';
1011
import {
1112
performance
1213
} from 'perf_hooks';
14+
import mime from 'mime';
15+
import { tmpdir } from 'os';
16+
import { Readable } from "stream";
17+
import { log } from "../logging/logging";
1318

1419
//@ts-ignore
1520
process.send = process.send || function () {};
@@ -248,9 +253,13 @@ export const generateGHIssueLink = (config : ConfigObject, sessionInfo: SessionI
248253
* download it and convert it to a DataURL. If Base64, returns it.
249254
* @param {string} file - The file to be converted to a DataURL.
250255
* @param {AxiosRequestConfig} requestConfig - AxiosRequestConfig = {}
256+
* @param {string} filename - Filename with an extension so a datauri mimetype can be inferred.
251257
* @returns A DataURL
252258
*/
253-
export const ensureDUrl = async (file : string, requestConfig: AxiosRequestConfig = {}) => {
259+
export const ensureDUrl = async (file : string | Buffer, requestConfig: AxiosRequestConfig = {}, filename?: string) => {
260+
if(Buffer.isBuffer(file)) {
261+
return `data:${mime.lookup(filename)};base64,${file.toString('base64').split(',')[1]}`
262+
} else
254263
if(!isDataURL(file) && !isBase64(file)) {
255264
//must be a file then
256265
const relativePath = path.join(path.resolve(process.cwd(),file|| ''));
@@ -260,5 +269,104 @@ export const ensureDUrl = async (file : string, requestConfig: AxiosRequestConfi
260269
file = await getDUrl(file, requestConfig);
261270
} else throw new CustomError(ERROR_NAME.FILE_NOT_FOUND,'Cannot find file. Make sure the file reference is relative, a valid URL or a valid DataURL')
262271
}
272+
if(file.includes("data:") && file.includes("undefined") || file.includes("application/octet-stream") && filename && mime.lookup(filename)) {
273+
file = `data:${mime.lookup(filename)};base64,${file.split(',')[1]}`
274+
}
263275
return file;
264-
}
276+
}
277+
278+
export const FileInputTypes = {
279+
"VALIDATED_FILE_PATH": "VALIDATED_FILE_PATH",
280+
"URL": "URL",
281+
"DATA_URL": "DATA_URL",
282+
"BASE_64": "BASE_64",
283+
"BUFFER": "BUFFER",
284+
"READ_STREAM": "READ_STREAM",
285+
}
286+
287+
export const FileOutputTypes = {
288+
...FileInputTypes,
289+
"TEMP_FILE_PATH": "TEMP_FILE_PATH",
290+
}
291+
292+
/**
293+
* Remove file asynchronously
294+
* @param file Filepath
295+
* @returns
296+
*/
297+
export function rmFileAsync(file: string) {
298+
return new Promise((resolve, reject) => {
299+
fs.unlink(file, (err) => {
300+
if (err) {
301+
reject(err);
302+
} else {
303+
resolve(true);
304+
}
305+
})
306+
})
307+
}
308+
309+
/**
310+
* Takes a file parameter and consistently returns the desired type of file.
311+
* @param file The file path, URL, base64 or DataURL string of the file
312+
* @param outfileName The ouput filename of the file
313+
* @param desiredOutputType The type of file output required from this function
314+
* @param requestConfig optional axios config if file parameter is a url
315+
*/
316+
export const assertFile : (file: AdvancedFile | Buffer, outfileName: string, desiredOutputType: keyof typeof FileOutputTypes, requestConfig ?: any ) => Promise<string | Buffer | Readable > = async (file, outfileName, desiredOutputType, requestConfig) => {
317+
let inputType;
318+
if(typeof file == 'string') {
319+
if(isDataURL(file)) inputType = FileInputTypes.DATA_URL
320+
else if(isBase64(file)) inputType = FileInputTypes.BASE_64
321+
/**
322+
* Check if it is a path
323+
*/
324+
else {
325+
const relativePath = path.join(path.resolve(process.cwd(),file|| ''));
326+
if(fs.existsSync(file) || fs.existsSync(relativePath)) {
327+
// file = await datauri(fs.existsSync(file) ? file : relativePath);
328+
inputType = FileInputTypes.VALIDATED_FILE_PATH;
329+
} else if(isUrl(file)) inputType = FileInputTypes.URL;
330+
/**
331+
* If not file type is determined by now then it is some sort of unidentifiable string. Throw an error.
332+
*/
333+
if(!inputType) throw new CustomError(ERROR_NAME.FILE_NOT_FOUND,`Cannot find file. Make sure the file reference is relative, a valid URL or a valid DataURL: ${file.slice(0,25)}`)
334+
}
335+
} else {
336+
if(Buffer.isBuffer(file)) inputType = FileInputTypes.BUFFER
337+
/**
338+
* Leave space to determine if incoming file parameter is any other type of object (maybe one day people will submit a path object as a param?)
339+
*/
340+
}
341+
if(inputType === desiredOutputType) return file;
342+
switch(desiredOutputType) {
343+
case FileOutputTypes.DATA_URL:
344+
case FileOutputTypes.BASE_64:
345+
return await ensureDUrl(file as string, requestConfig, outfileName);
346+
break;
347+
case FileOutputTypes.TEMP_FILE_PATH: {
348+
/**
349+
* Create a temp file in tempdir, return the tempfile.
350+
*/
351+
const tempFilePath = path.join(tmpdir(),`${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.${outfileName}`);
352+
log.info(`Saved temporary file to ${tempFilePath}`)
353+
if(inputType != FileInputTypes.BUFFER){
354+
file = await ensureDUrl(file as string, requestConfig, outfileName);
355+
file = Buffer.from(file.split(',')[1], 'base64')
356+
}
357+
await fs.writeFileSync(tempFilePath, file);
358+
return tempFilePath
359+
break;
360+
}
361+
case FileOutputTypes.BUFFER:
362+
return Buffer.from((await ensureDUrl(file as string, requestConfig, outfileName)).split(',')[1], 'base64');
363+
break;
364+
case FileOutputTypes.READ_STREAM: {
365+
if(inputType === FileInputTypes.VALIDATED_FILE_PATH) return fs.createReadStream(file)
366+
else if(inputType != FileInputTypes.BUFFER) file = Buffer.from((await ensureDUrl(file as string, requestConfig, outfileName)).split(',')[1], 'base64')
367+
return Readable.from(file)
368+
break;
369+
}
370+
}
371+
return file;
372+
}

0 commit comments

Comments
 (0)