UNPKG

@warriorteam/zalo-personal

Version:

Unofficial Zalo Personal API for JavaScript - A powerful library for interacting with Zalo personal accounts with URL attachment support

243 lines (242 loc) 11.9 kB
import FormData from "form-data"; import fs from "node:fs"; import { ZaloApiError } from "../Errors/ZaloApiError.js"; import { ThreadType } from "../models/index.js"; import { apiFactory, getFileExtension, getFileName, getFileSize, getImageMetaData, getMd5LargeFileObject, resolveResponse, isValidUrl, downloadFileFromUrl, } from "../utils.js"; const urlType = { image: "photo_original/upload", video: "asyncfile/upload", others: "asyncfile/upload", }; export const uploadAttachmentFactory = apiFactory()((api, ctx, utils) => { const serviceURL = `${api.zpwServiceMap.file[0]}/api`; const { sharefile } = ctx.settings.features; function isExceedMaxFile(totalFile) { return totalFile > sharefile.max_file; } function isExceedMaxFileSize(fileSize) { return fileSize > sharefile.max_size_share_file_v3 * 1024 * 1024; } function isExtensionValid(ext) { return sharefile.restricted_ext_file.indexOf(ext) == -1; } /** * Upload an attachment to a thread * * @param sources path to files or attachment sources * @param threadId Group or User ID * @param type Message type (User or Group) * * @throws ZaloApiError */ return async function uploadAttachment(sources, threadId, type = ThreadType.User) { if (!sources) throw new ZaloApiError("Missing sources"); if (!Array.isArray(sources)) sources = [sources]; if (sources.length == 0) throw new ZaloApiError("Missing sources"); if (isExceedMaxFile(sources.length)) throw new ZaloApiError("Exceed maximum file of " + sharefile.max_file); if (!threadId) throw new ZaloApiError("Missing threadId"); const chunkSize = ctx.settings.features.sharefile.chunk_size_file; const isGroupMessage = type == ThreadType.Group; let attachmentsData = []; let url = `${serviceURL}/${isGroupMessage ? "group" : "message"}/`; const typeParam = isGroupMessage ? "11" : "2"; let clientId = Date.now(); for (const source of sources) { const isFilePath = typeof source == "string"; const isUrl = typeof source == "object" && "url" in source; const isBuffer = typeof source == "object" && "data" in source && source.data instanceof Buffer; if (!isFilePath && !isUrl && !isBuffer) throw new ZaloApiError("Invalid source type"); let processedSource; if (isFilePath) { if (!fs.existsSync(source)) throw new ZaloApiError("File not found"); const fileName = getFileName(source); const extFile = getFileExtension(source); if (isExtensionValid(extFile) == false) throw new ZaloApiError(`File extension "${extFile}" is not allowed`); // Read file and get metadata const fileBuffer = await fs.promises.readFile(source); let metadata; switch (extFile) { case "jpg": case "jpeg": case "png": case "webp": const imageData = await getImageMetaData(source); metadata = { totalSize: imageData.totalSize || 0, width: imageData.width, height: imageData.height, }; break; default: const fileSize = await getFileSize(source); metadata = { totalSize: fileSize }; break; } processedSource = { data: fileBuffer, filename: fileName, metadata, }; } else if (isUrl) { if (!isValidUrl(source.url)) throw new ZaloApiError("Invalid URL"); // Download file from URL const downloadResult = await downloadFileFromUrl(source.url, source.headers); const fileName = source.filename || downloadResult.filename; const extFile = getFileExtension(fileName); if (isExtensionValid(extFile) == false) throw new ZaloApiError(`File extension "${extFile}" is not allowed`); processedSource = { data: downloadResult.data, filename: fileName, metadata: downloadResult.metadata, }; } else if (isBuffer) { if (!source.filename) throw new ZaloApiError("Missing filename"); const extFile = getFileExtension(source.filename); if (isExtensionValid(extFile) == false) throw new ZaloApiError(`File extension "${extFile}" is not allowed`); processedSource = { data: source.data, filename: source.filename, metadata: source.metadata, }; } else { throw new ZaloApiError("Invalid source type"); } const extFile = getFileExtension(processedSource.filename); const fileName = processedSource.filename; // Remove duplicate validation since it's already done above const data = { filePath: processedSource.filename, chunkContent: [], params: {}, source: processedSource, }; if (isGroupMessage) data.params.grid = threadId; else data.params.toid = threadId; switch (extFile) { case "jpg": case "jpeg": case "png": case "webp": if (isExceedMaxFileSize(processedSource.metadata.totalSize)) throw new ZaloApiError(`File ${fileName} size exceed maximum size of ${sharefile.max_size_share_file_v3}MB`); data.fileData = { fileName, totalSize: processedSource.metadata.totalSize, width: processedSource.metadata.width, height: processedSource.metadata.height, }; data.fileType = "image"; data.params.totalChunk = Math.ceil(processedSource.metadata.totalSize / chunkSize); data.params.fileName = fileName; data.params.clientId = clientId++; data.params.totalSize = processedSource.metadata.totalSize; data.params.imei = ctx.imei; data.params.isE2EE = 0; data.params.jxl = 0; data.params.chunkId = 1; break; case "mp4": if (isExceedMaxFileSize(processedSource.metadata.totalSize)) throw new ZaloApiError(`File ${fileName} size exceed maximum size of ${sharefile.max_size_share_file_v3}MB`); data.fileType = "video"; data.fileData = { fileName, totalSize: processedSource.metadata.totalSize, }; data.params.totalChunk = Math.ceil(processedSource.metadata.totalSize / chunkSize); data.params.fileName = fileName; data.params.clientId = clientId++; data.params.totalSize = processedSource.metadata.totalSize; data.params.imei = ctx.imei; data.params.isE2EE = 0; data.params.jxl = 0; data.params.chunkId = 1; break; default: if (isExceedMaxFileSize(processedSource.metadata.totalSize)) throw new ZaloApiError(`File ${fileName} size exceed maximum size of ${sharefile.max_size_share_file_v3}MB`); data.fileType = "others"; data.fileData = { fileName, totalSize: processedSource.metadata.totalSize, }; data.params.totalChunk = Math.ceil(processedSource.metadata.totalSize / chunkSize); data.params.fileName = fileName; data.params.clientId = clientId++; data.params.totalSize = processedSource.metadata.totalSize; data.params.imei = ctx.imei; data.params.isE2EE = 0; data.params.jxl = 0; data.params.chunkId = 1; break; } const fileBuffer = processedSource.data; for (let i = 0; i < data.params.totalChunk; i++) { const formData = new FormData(); const slicedBuffer = fileBuffer.subarray(i * chunkSize, (i + 1) * chunkSize); formData.append("chunkContent", slicedBuffer, { filename: fileName, contentType: "application/octet-stream", }); data.chunkContent[i] = formData; } attachmentsData.push(data); } const requests = [], results = []; for (const data of attachmentsData) { for (let i = 0; i < data.params.totalChunk; i++) { const encryptedParams = utils.encodeAES(JSON.stringify(data.params)); if (!encryptedParams) throw new ZaloApiError("Failed to encrypt message"); requests.push(utils .request(utils.makeURL(url + urlType[data.fileType], { type: typeParam, params: encryptedParams }), { method: "POST", headers: data.chunkContent[i].getHeaders(), body: data.chunkContent[i].getBuffer(), }) .then(async (response) => { /** * @TODO: better type rather than any */ const resData = await resolveResponse(ctx, response); if (resData && resData.fileId != -1 && resData.photoId != -1) await new Promise((resolve) => { if (data.fileType == "video" || data.fileType == "others") { const uploadCallback = async (wsData) => { let result = Object.assign(Object.assign(Object.assign({ fileType: data.fileType }, resData), wsData), { totalSize: data.fileData.totalSize, fileName: data.fileData.fileName, checksum: (await getMd5LargeFileObject(data.source, data.fileData.totalSize)).data }); results.push(result); resolve(); }; ctx.uploadCallbacks.set(resData.fileId, uploadCallback); } if (data.fileType == "image") { let result = Object.assign({ fileType: "image", width: data.fileData.width, height: data.fileData.height, totalSize: data.fileData.totalSize, hdSize: data.fileData.totalSize }, resData); results.push(result); resolve(); } }); })); data.params.chunkId++; } } await Promise.all(requests); return results; }; });