From 5869d8617741fe3ce43355568354e70f135cfb8c Mon Sep 17 00:00:00 2001 From: Anton Voylenko Date: Mon, 31 Mar 2025 10:55:15 +0300 Subject: [PATCH 1/2] feat: new media download way --- src/controllers/messageController.js | 53 ++++++++++++- src/routes.js | 1 + src/utils.js | 11 ++- swagger.json | 110 +++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 2 deletions(-) diff --git a/src/controllers/messageController.js b/src/controllers/messageController.js index 43eaec4..f422d61 100644 --- a/src/controllers/messageController.js +++ b/src/controllers/messageController.js @@ -1,6 +1,7 @@ const { MessageMedia, Location, Poll } = require('whatsapp-web.js') +const { Readable } = require('stream') const { sessions } = require('../sessions') -const { sendErrorResponse } = require('../utils') +const { sendErrorResponse, decodeBase64 } = require('../utils') /** * Get message by its ID from a given chat using the provided client. @@ -116,6 +117,7 @@ const downloadMedia = async (req, res) => { const client = sessions.get(req.params.sessionId) const message = await _getMessageById(client, messageId, chatId) if (!message) { throw new Error('Message not found') } + if (!message.hasMedia) { throw new Error('Message media not found') } const messageMedia = await message.downloadMedia() res.json({ success: true, messageMedia }) } catch (error) { @@ -123,6 +125,54 @@ const downloadMedia = async (req, res) => { } } +/** + * Downloads media from a message and sends it as binary data. + * @async + * @function + * @param {Object} req - The request object. + * @param {Object} res - The response object. + * @param {string} req.params.sessionId - The session ID. + * @param {string} req.body.messageId - The message ID. + * @param {string} req.body.chatId - The chat ID. + * @returns {Promise} - A Promise that resolves with no value when the function completes. + */ +const downloadMediaAsData = async (req, res) => { + /* + #swagger.summary = 'Download attached message media as binary data' + */ + try { + const { messageId, chatId } = req.body + const client = sessions.get(req.params.sessionId) + const message = await _getMessageById(client, messageId, chatId) + if (!message) { throw new Error('Message not found') } + if (!message.hasMedia) { throw new Error('Message media not found') } + const { data, mimetype, filename, filesize } = await message.downloadMedia() + /* #swagger.responses[200] = { + description: 'Binary data.' + } + */ + res.writeHead(200, { + ...(mimetype && { 'Content-Type': mimetype }), + ...(filesize && { 'Content-Length': filesize }), + ...(filename && { 'Content-Disposition': `attachment; filename=${encodeURIComponent(filename)}` }) + }) + const readableStream = new Readable({ + read () { + for (const chunk of decodeBase64(data)) { + this.push(chunk) + } + this.push(null) + } + }) + readableStream.on('end', () => { + res.end() + }) + readableStream.pipe(res) + } catch (error) { + sendErrorResponse(res, 500, error.message) + } +} + /** * Forwards a message to a destination chat. * @async @@ -684,6 +734,7 @@ module.exports = { getClassInfo, deleteMessage, downloadMedia, + downloadMediaAsData, forward, getInfo, getMentions, diff --git a/src/routes.js b/src/routes.js index ab58957..496dbda 100644 --- a/src/routes.js +++ b/src/routes.js @@ -170,6 +170,7 @@ routes.use('/message', messageRouter) messageRouter.post('/getClassInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getClassInfo) messageRouter.post('/delete/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.deleteMessage) messageRouter.post('/downloadMedia/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.downloadMedia) +messageRouter.post('/downloadMediaAsData/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.downloadMediaAsData) messageRouter.post('/forward/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.forward) messageRouter.post('/getInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getInfo) messageRouter.post('/getMentions/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getMentions) diff --git a/src/utils.js b/src/utils.js index 48e3b11..85b0504 100644 --- a/src/utils.js +++ b/src/utils.js @@ -51,10 +51,19 @@ const sendMessageSeenStatus = async (message) => { } } +const decodeBase64 = function * (base64String) { + const chunkSize = 1024 + for (let i = 0; i < base64String.length; i += chunkSize) { + const chunk = base64String.slice(i, i + chunkSize) + yield Buffer.from(chunk, 'base64') + } +} + module.exports = { triggerWebhook, sendErrorResponse, waitForNestedObject, checkIfEventisEnabled, - sendMessageSeenStatus + sendMessageSeenStatus, + decodeBase64 } diff --git a/swagger.json b/swagger.json index 8545632..9c17c28 100644 --- a/swagger.json +++ b/swagger.json @@ -8664,6 +8664,116 @@ } } }, + "/message/downloadMediaAsData/{sessionId}": { + "post": { + "tags": [ + "Message" + ], + "summary": "Download attached message media as binary data", + "description": "", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the session (alphanumeric and - allowed)", + "example": "f8377d8d-a589-4242-9ba6-9486a04ef80c" + } + ], + "responses": { + "200": { + "description": "Binary data." + }, + "403": { + "description": "Forbidden.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenResponse" + } + } + } + }, + "404": { + "description": "Not Found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Entity.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server failure.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "chatId": { + "type": "string", + "description": "The chat id which contains the message", + "example": "6281288888888@c.us" + }, + "messageId": { + "type": "string", + "description": "Unique WhatsApp ID for the message", + "example": "ABCDEF999999999" + } + } + } + }, + "application/xml": { + "schema": { + "type": "object", + "properties": { + "chatId": { + "type": "string", + "description": "The chat id which contains the message", + "example": "6281288888888@c.us" + }, + "messageId": { + "type": "string", + "description": "Unique WhatsApp ID for the message", + "example": "ABCDEF999999999" + } + } + } + } + } + } + } + }, "/message/forward/{sessionId}": { "post": { "tags": [ From 462db11df5594e4ea9d7e34f20a46379faa46ca9 Mon Sep 17 00:00:00 2001 From: Anton Voylenko Date: Mon, 31 Mar 2025 10:55:27 +0300 Subject: [PATCH 2/2] chore: bump deps --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 24b3676..1de6633 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "axios": "^1.8.3", + "axios": "^1.8.4", "dotenv": "^16.4.5", "express": "^4.21.2", "express-rate-limit": "^7.4.1", @@ -17,7 +17,7 @@ "qr-image": "^3.2.0", "qrcode-terminal": "^0.12.0", "swagger-ui-express": "^5.0.1", - "whatsapp-web.js": "^1.26.1-alpha.3", + "whatsapp-web.js": "^1.27.0", "ws": "^8.18.1" }, "devDependencies": { @@ -1725,9 +1725,9 @@ } }, "node_modules/axios": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", - "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -8108,9 +8108,9 @@ "license": "BSD-2-Clause" }, "node_modules/whatsapp-web.js": { - "version": "1.26.1-alpha.3", - "resolved": "https://registry.npmjs.org/whatsapp-web.js/-/whatsapp-web.js-1.26.1-alpha.3.tgz", - "integrity": "sha512-W8RoTf6rZfHJx+JZGWUlsX0mbtZpYnoXzbSmtOtSn+pgTPO70rmdef4J9kuTWi4CRgnLLTQnCzpbDkEMXCUDBw==", + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/whatsapp-web.js/-/whatsapp-web.js-1.27.0.tgz", + "integrity": "sha512-hUhHpNXFTiQwTPfw/5WwV9MZLX+9sQXT1MoI14QYOuNxOxoJU12P2ODoqYnIWiqwIhyn++wQLpPsBC4LzcdzAQ==", "license": "Apache-2.0", "dependencies": { "@pedroslopez/moduleraid": "^5.0.2", diff --git a/package.json b/package.json index 1fc9b2b..38e28e9 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "swagger": "node swagger.js" }, "dependencies": { - "axios": "^1.8.3", + "axios": "^1.8.4", "dotenv": "^16.4.5", "express": "^4.21.2", "express-rate-limit": "^7.4.1", @@ -17,7 +17,7 @@ "qr-image": "^3.2.0", "qrcode-terminal": "^0.12.0", "swagger-ui-express": "^5.0.1", - "whatsapp-web.js": "^1.26.1-alpha.3", + "whatsapp-web.js": "^1.27.0", "ws": "^8.18.1" }, "devDependencies": {