From 1ab536e5a2fe07f8dd73966d3bbdef88a4ea4502 Mon Sep 17 00:00:00 2001 From: Joao Pedro Date: Thu, 20 Feb 2025 15:42:10 -0300 Subject: [PATCH] Add downloadMediaMessage route --- src/api/controllers/chat.controller.ts | 5 ++++ src/api/dto/chat.dto.ts | 9 +++++++ .../whatsapp/whatsapp.baileys.service.ts | 25 +++++++++++++++++++ src/api/routes/chat.router.ts | 12 +++++++++ src/utils/readStreamWithTimeout.ts | 22 ++++++++++++++++ src/validate/chat.schema.ts | 20 +++++++++++++++ 6 files changed, 93 insertions(+) create mode 100644 src/utils/readStreamWithTimeout.ts diff --git a/src/api/controllers/chat.controller.ts b/src/api/controllers/chat.controller.ts index 207d8ba..8ae9818 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -2,6 +2,7 @@ import { ArchiveChatDto, BlockUserDto, DeleteMessage, + DownloadMediaMessageDto, getBase64FromMediaMessageDto, MarkChatUnreadDto, NumberDto, @@ -82,6 +83,10 @@ export class ChatController { return await this.waMonitor.waInstances[instanceName].updatePrivacySettings(data); } + public async downloadMediaMessage({ instanceName }: InstanceDto, data: DownloadMediaMessageDto) { + return await this.waMonitor.waInstances[instanceName].downloadMediaMessage(data); + } + public async fetchBusinessProfile({ instanceName }: InstanceDto, data: ProfilePictureDto) { return await this.waMonitor.waInstances[instanceName].fetchBusinessProfile(data.number); } diff --git a/src/api/dto/chat.dto.ts b/src/api/dto/chat.dto.ts index 00da7fd..ad4132b 100644 --- a/src/api/dto/chat.dto.ts +++ b/src/api/dto/chat.dto.ts @@ -1,4 +1,6 @@ import { + DownloadableMessage, + MediaType, proto, WAPresence, WAPrivacyGroupAddValue, @@ -94,6 +96,13 @@ export class PrivacySettingDto { groupadd: WAPrivacyGroupAddValue; } +export class DownloadMediaMessageDto { + downloadableMessage: DownloadableMessage; + type: MediaType; + timeout?: number; + returnType?: 'base64' | 'buffer' = 'buffer'; +} + export class DeleteMessage { id: string; fromMe: boolean; diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index ddd9360..fea7867 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3,6 +3,7 @@ import { ArchiveChatDto, BlockUserDto, DeleteMessage, + DownloadMediaMessageDto, getBase64FromMediaMessageDto, LastMessage, MarkChatUnreadDto, @@ -77,6 +78,7 @@ import { Instance } from '@prisma/client'; import { createJid } from '@utils/createJid'; import { makeProxyAgent } from '@utils/makeProxyAgent'; import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; +import { readStreamWithTimeout } from '@utils/readStreamWithTimeout'; import { status } from '@utils/renderStatus'; import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma'; import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; @@ -92,6 +94,7 @@ import makeWASocket, { Contact, delay, DisconnectReason, + downloadContentFromMessage, downloadMediaMessage, fetchLatestBaileysVersion, generateWAMessageFromContent, @@ -3133,6 +3136,28 @@ export class BaileysStartupService extends ChannelStartupService { } } + public async downloadMediaMessage(downloadMedia: DownloadMediaMessageDto) { + try { + const media = await downloadContentFromMessage( + { + ...downloadMedia.downloadableMessage, + }, + downloadMedia.type, + ); + + const buffer = await readStreamWithTimeout(media, downloadMedia.timeout); + + if (downloadMedia.returnType === 'base64') { + const base64Data = buffer.toString('base64'); + return base64Data; + } + + return buffer; + } catch (error) { + throw new InternalServerErrorException('Error downloading media message', error.toString()); + } + } + public async fetchBusinessProfile(number: string): Promise { try { const jid = number ? createJid(number) : this.instance.wuid; diff --git a/src/api/routes/chat.router.ts b/src/api/routes/chat.router.ts index 20126c1..e4439f3 100644 --- a/src/api/routes/chat.router.ts +++ b/src/api/routes/chat.router.ts @@ -3,6 +3,7 @@ import { ArchiveChatDto, BlockUserDto, DeleteMessage, + DownloadMediaMessageDto, getBase64FromMediaMessageDto, MarkChatUnreadDto, NumberDto, @@ -24,6 +25,7 @@ import { blockUserSchema, contactValidateSchema, deleteMessageSchema, + downloadMediaMessageSchema, markChatUnreadSchema, messageUpSchema, messageValidateSchema, @@ -267,6 +269,16 @@ export class ChatRouter extends RouterBroker { }); return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('downloadMediaMessage'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: downloadMediaMessageSchema, + ClassRef: DownloadMediaMessageDto, + execute: (instance, data) => chatController.downloadMediaMessage(instance, data), + }); + + return res.status(HttpStatus.OK).json(response); }); } diff --git a/src/utils/readStreamWithTimeout.ts b/src/utils/readStreamWithTimeout.ts new file mode 100644 index 0000000..9a4ba2d --- /dev/null +++ b/src/utils/readStreamWithTimeout.ts @@ -0,0 +1,22 @@ +import { promisify } from 'util'; + +export async function readStreamWithTimeout(stream: any, timeout: number = 30_000) { + const setTimeoutPromise = promisify(setTimeout); + + let buffer = Buffer.from([]); + const chunks = []; + const timer: any = setTimeoutPromise(timeout).then(() => { + stream.destroy(new Error('Stream timeout')); + }); + + try { + for await (const chunk of stream) { + chunks.push(chunk); + } + buffer = Buffer.concat(chunks); + } finally { + clearTimeout(timer); + } + + return buffer; +} diff --git a/src/validate/chat.schema.ts b/src/validate/chat.schema.ts index dba2799..f9c2181 100644 --- a/src/validate/chat.schema.ts +++ b/src/validate/chat.schema.ts @@ -287,6 +287,26 @@ export const privacySettingsSchema: JSONSchema7 = { ...isNotEmpty('readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'), }; +export const downloadMediaMessageSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + downloadableMessage: { + type: 'object', + properties: { + url: { type: 'string' }, + mediaKey: { type: 'string' }, + directPath: { type: 'string' }, + }, + ...isNotEmpty('url', 'mediaKey', 'directPath'), + }, + type: { type: 'string', enum: ['image', 'video', 'audio', 'document', 'sticker', 'ptt'] }, + timeout: { type: 'number', default: 30_000 }, + returnType: { type: 'string', enum: ['base64', 'buffer'], default: 'buffer' }, + }, + ...isNotEmpty('downloadableMessage', 'type'), +}; + export const profileNameSchema: JSONSchema7 = { $id: v4(), type: 'object',