diff --git a/src/config.service.ts b/src/config.service.ts index df86d5ca..a1948ca9 100644 --- a/src/config.service.ts +++ b/src/config.service.ts @@ -1,6 +1,7 @@ import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { GlobalWebhookConfigConfig } from '@waha/core/config/GlobalWebhookConfig'; +import { IGNORE_ALL_MEDIA_MIMETYPE } from '@waha/core/media/MediaManager'; import { IgnoreJidConfig } from '@waha/core/utils/jids'; import { parseBool } from './helpers'; @@ -65,7 +66,7 @@ export class WhatsappConfigService implements OnApplicationBootstrap { get mimetypes(): string[] { if (!this.shouldDownloadMedia) { - return ['mimetype/ignore-all-media']; + return [IGNORE_ALL_MEDIA_MIMETYPE]; } const types = this.configService.get('WHATSAPP_FILES_MIMETYPES', ''); return types ? types.split(',') : []; diff --git a/src/core/engines/gows/session.gows.core.ts b/src/core/engines/gows/session.gows.core.ts index 033381f2..99fb73f0 100644 --- a/src/core/engines/gows/session.gows.core.ts +++ b/src/core/engines/gows/session.gows.core.ts @@ -2110,7 +2110,13 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { }, Message: channelMessage.Message, }; - const message = await this.processIncomingMessage(msg, downloadMedia); + // An explicit downloadMedia=true forces the download even when media is + // globally disabled. + const message = await this.processIncomingMessage( + msg, + downloadMedia, + downloadMedia, + ); const reactions: any = sortObjectByValues(channelMessage.ReactionCounts) || {}; return { @@ -2492,7 +2498,11 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { const msgs = parseJsonList(response); const promises = []; for (const msg of msgs) { - promises.push(this.processIncomingMessage(msg, downloadMedia)); + // An explicit downloadMedia=true forces the download even when media is + // globally disabled. + promises.push( + this.processIncomingMessage(msg, downloadMedia, downloadMedia), + ); } let result = await Promise.all(promises); result = result.filter(Boolean); @@ -2519,7 +2529,13 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { }); const response = await promisify(this.client.GetMessageById)(request); const msg = parseJson(response); - return this.processIncomingMessage(msg, query.downloadMedia); + // An explicit downloadMedia=true forces the download even when media is + // globally disabled. + return this.processIncomingMessage( + msg, + query.downloadMedia, + query.downloadMedia, + ); } /** @@ -2683,7 +2699,14 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { return true; } - protected async processIncomingMessage(message, downloadMedia = true) { + // `force` comes from an explicit downloadMedia=true on the request and + // downloads the media even when media download is globally disabled + // (WHATSAPP_DOWNLOAD_MEDIA=false). + protected async processIncomingMessage( + message, + downloadMedia = true, + force = false, + ) { // Filter if (!this.shouldProcessIncomingMessage(message)) { return null; @@ -2692,7 +2715,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { const wamessage = this.toWAMessage(message); // Media if (downloadMedia) { - const media = await this.downloadMediaSafe(message); + const media = await this.downloadMediaSafe(message, force); wamessage.media = media; } if (downloadMedia && wamessage.replyTo?.hasMedia) { @@ -2703,14 +2726,14 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { ID: wamessage.replyTo.id || '', }, }; - wamessage.replyTo.media = await this.downloadMediaSafe(msg); + wamessage.replyTo.media = await this.downloadMediaSafe(msg, force); } return wamessage; } - protected async downloadMediaSafe(message) { + protected async downloadMediaSafe(message, force = false) { try { - return await this.downloadMedia(message); + return await this.downloadMedia(message, force); } catch (e) { this.logger.error('Failed when tried to download media for a message'); this.logger.error(e, e.stack); @@ -2718,7 +2741,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { } } - protected async downloadMedia(message) { + protected async downloadMedia(message, force = false) { let processor: IMediaEngineProcessor = new GOWSEngineMediaProcessor( this, ); @@ -2727,6 +2750,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { processor, message, this.name, + force, ); return media; } diff --git a/src/core/engines/noweb/session.noweb.core.ts b/src/core/engines/noweb/session.noweb.core.ts index 9ee500ad..d443e5e4 100644 --- a/src/core/engines/noweb/session.noweb.core.ts +++ b/src/core/engines/noweb/session.noweb.core.ts @@ -1468,7 +1468,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { const promises = []; for (const msg of messages) { - promises.push(this.processIncomingMessage(msg, downloadMedia)); + // An explicit downloadMedia=true forces the download even when media is + // globally disabled. + promises.push( + this.processIncomingMessage(msg, downloadMedia, downloadMedia), + ); } let result = await Promise.all(promises); result = result.filter(Boolean); @@ -1496,7 +1500,13 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { merge, ); if (!message) return null; - return await this.processIncomingMessage(message, query.downloadMedia); + // An explicit downloadMedia=true forces the download even when media is + // globally disabled. + return await this.processIncomingMessage( + message, + query.downloadMedia, + query.downloadMedia, + ); } @Activity() @@ -3071,9 +3081,13 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { return ''; } + // `force` comes from an explicit downloadMedia=true on the request and + // downloads the media even when media download is globally disabled + // (WHATSAPP_DOWNLOAD_MEDIA=false). protected async processIncomingMessage( message, downloadMedia: boolean, + force = false, ): Promise { // Filter if (!this.shouldProcessIncomingMessage(message)) { @@ -3086,7 +3100,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { } // Media if (downloadMedia && wamessage.hasMedia) { - wamessage.media = await this.downloadMediaSafe(message); + wamessage.media = await this.downloadMediaSafe(message, force); } if (downloadMedia && wamessage.replyTo?.hasMedia) { @@ -3102,7 +3116,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { remoteJid: message.key.remoteJid, }, }; - wamessage.replyTo.media = await this.downloadMediaSafe(m); + wamessage.replyTo.media = await this.downloadMediaSafe(m, force); } return wamessage; } @@ -3357,9 +3371,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { return { id: chatId, presences: presences }; } - protected async downloadMediaSafe(message): Promise { + protected async downloadMediaSafe( + message, + force = false, + ): Promise { try { - return await this.downloadMedia(message); + return await this.downloadMedia(message, force); } catch (e) { this.logger.error('Failed when tried to download media for a message'); this.logger.error(e, e.stack); @@ -3367,13 +3384,16 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { return null; } - protected async downloadMedia(message): Promise { + protected async downloadMedia( + message, + force = false, + ): Promise { let processor: IMediaEngineProcessor = new NOWEBEngineMediaProcessor( this, this.loggerBuilder, ); processor = new LottieMediaProcessorWrapper(processor, this.logger); - return this.mediaManager.processMedia(processor, message, this.name); + return this.mediaManager.processMedia(processor, message, this.name, force); } protected async getMessageOptions(request: { diff --git a/src/core/engines/webjs/session.webjs.core.ts b/src/core/engines/webjs/session.webjs.core.ts index 990f9d88..8776453d 100644 --- a/src/core/engines/webjs/session.webjs.core.ts +++ b/src/core/engines/webjs/session.webjs.core.ts @@ -1202,7 +1202,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { ); const promises = []; for (const msg of messages) { - promises.push(this.processIncomingMessage(msg, downloadMedia)); + // An explicit downloadMedia=true forces the download even when media is + // globally disabled. + promises.push( + this.processIncomingMessage(msg, downloadMedia, downloadMedia), + ); } let result = await Promise.all(promises); result = result.filter(Boolean); @@ -1284,7 +1288,13 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { return null; }); } - return await this.processIncomingMessage(message, query.downloadMedia); + // An explicit downloadMedia=true forces the download even when media is + // globally disabled. + return await this.processIncomingMessage( + message, + query.downloadMedia, + query.downloadMedia, + ); } @Activity() @@ -1735,9 +1745,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { channelMessage: WebjsChannelMessage, downloadMedia: boolean, ): Promise { + // An explicit downloadMedia=true forces the download even when media is + // globally disabled. const message = await this.processIncomingMessage( channelMessage.message, downloadMedia, + downloadMedia, ); const reactions = {}; for (const reaction of channelMessage.reactions.sort((x) => -x.count)) { @@ -2292,21 +2305,28 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { .switch(this.callRejected$.asObservable()); } + // `force` comes from an explicit downloadMedia=true on the request and + // downloads the media even when media download is globally disabled + // (WHATSAPP_DOWNLOAD_MEDIA=false). protected async processIncomingMessage( message: Message, downloadMedia = true, + force = false, ) { // Convert const wamessage = this.toWAMessage(message); // Media if (downloadMedia) { - const media = await this.downloadMediaSafe(message); + const media = await this.downloadMediaSafe(message, force); wamessage.media = media; } if (downloadMedia && wamessage.replyTo?.hasMedia) { const quotedMessage = await message.getQuotedMessage().catch(() => null); if (quotedMessage) { - wamessage.replyTo.media = await this.downloadMediaSafe(quotedMessage); + wamessage.replyTo.media = await this.downloadMediaSafe( + quotedMessage, + force, + ); } } return wamessage; @@ -2524,9 +2544,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { return contact; } - protected async downloadMediaSafe(message): Promise { + protected async downloadMediaSafe( + message, + force = false, + ): Promise { try { - return await this.downloadMedia(message); + return await this.downloadMedia(message, force); } catch (e) { this.logger.error('Failed when tried to download media for a message'); this.logger.error(e, e.stack); @@ -2534,13 +2557,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { return null; } - protected async downloadMedia(message: Message) { + protected async downloadMedia(message: Message, force = false) { let processor = new WEBJSEngineMediaProcessor(); processor = new LottieMediaProcessorWrapper(processor, this.logger); const media = await this.mediaManager.processMedia( processor, message, this.name, + force, ); return media; } diff --git a/src/core/engines/wpp/session.wpp.core.ts b/src/core/engines/wpp/session.wpp.core.ts index c966fec9..357aedd3 100644 --- a/src/core/engines/wpp/session.wpp.core.ts +++ b/src/core/engines/wpp/session.wpp.core.ts @@ -1148,7 +1148,13 @@ export class WhatsappSessionWPPCore extends WhatsappSession { if (!message) { return null; } - return this.processIncomingMessage(message, query.downloadMedia); + // An explicit downloadMedia=true forces the download even when media is + // globally disabled. + return this.processIncomingMessage( + message, + query.downloadMedia, + query.downloadMedia, + ); } @Activity() @@ -2346,25 +2352,38 @@ export class WhatsappSessionWPPCore extends WhatsappSession { }; } - protected async processIncomingMessage(message: any, downloadMedia = true) { + // `force` comes from an explicit downloadMedia=true on the request and + // downloads the media even when media download is globally disabled + // (WHATSAPP_DOWNLOAD_MEDIA=false). + protected async processIncomingMessage( + message: any, + downloadMedia = true, + force = false, + ) { const wamessage = this.toWAMessage(message); if (downloadMedia) { - const media = await this.downloadMediaSafe(message); + const media = await this.downloadMediaSafe(message, force); wamessage.media = media; } if (downloadMedia && wamessage.replyTo?.hasMedia) { const quotedMessage = message?.quotedMsg || message?._data?.quotedMsg; if (quotedMessage) { - wamessage.replyTo.media = await this.downloadMediaSafe(quotedMessage); + wamessage.replyTo.media = await this.downloadMediaSafe( + quotedMessage, + force, + ); } } return wamessage; } - protected async downloadMedia(message: any): Promise { + protected async downloadMedia( + message: any, + force = false, + ): Promise { let processor = new WPPEngineMediaProcessor(this.wpp); processor = new LottieMediaProcessorWrapper(processor, this.logger); - return this.mediaManager.processMedia(processor, message, this.name); + return this.mediaManager.processMedia(processor, message, this.name, force); } protected checkStatusRequest(request: { contacts?: any[] }) { @@ -2375,9 +2394,12 @@ export class WhatsappSessionWPPCore extends WhatsappSession { } } - protected async downloadMediaSafe(message): Promise { + protected async downloadMediaSafe( + message, + force = false, + ): Promise { try { - return await this.downloadMedia(message); + return await this.downloadMedia(message, force); } catch (error) { this.logger.error('Failed when tried to download media for a message'); this.logger.error(error, error.stack); diff --git a/src/core/media/IMediaManager.ts b/src/core/media/IMediaManager.ts index 944e4f9f..4c1727d0 100644 --- a/src/core/media/IMediaManager.ts +++ b/src/core/media/IMediaManager.ts @@ -10,6 +10,7 @@ interface IMediaManager { processor: IMediaEngineProcessor, message: Message, session: string, + force?: boolean, ): Promise; close(): void; } diff --git a/src/core/media/MediaManager.ts b/src/core/media/MediaManager.ts index c3e2b3ab..d74dceb7 100644 --- a/src/core/media/MediaManager.ts +++ b/src/core/media/MediaManager.ts @@ -13,6 +13,11 @@ const mime = require('mime-types'); // eslint-disable-next-line @typescript-eslint/no-var-requires const promiseRetry = require('promise-retry'); +// Sentinel used as the only allowed mimetype when media download is globally +// disabled (WHATSAPP_DOWNLOAD_MEDIA=false). It matches no real mimetype, so +// nothing is downloaded - unless a request explicitly forces it. +export const IGNORE_ALL_MEDIA_MIMETYPE = 'mimetype/ignore-all-media'; + export class MediaManager implements IMediaManager { // https://github.com/IndigoUnited/node-promise-retry RETRY_OPTIONS = { @@ -35,13 +40,24 @@ export class MediaManager implements IMediaManager { } /** - * Check that we need to download files with the mimetype + * Check that we need to download files with the mimetype. + * `force` comes from an explicit downloadMedia=true on the request. It + * overrides only the global "download disabled" sentinel, never a real + * mimetype allow list. */ - private shouldProcessMimetype(mimetype: string) { + private shouldProcessMimetype(mimetype: string, force = false) { // No specific mimetypes provided - always download if (!this.mimetypes || this.mimetypes.length === 0) { return true; } + // Media download is globally disabled. An explicit downloadMedia=true on a + // single request can still ask for this one file. + if ( + this.mimetypes.length === 1 && + this.mimetypes[0] === IGNORE_ALL_MEDIA_MIMETYPE + ) { + return force; + } // Found "right" mimetype in the list of allowed mimetypes - download it return this.mimetypes.some((type) => mimetype.startsWith(type)); } @@ -50,12 +66,13 @@ export class MediaManager implements IMediaManager { processor: IMediaEngineProcessor, message: Message, session: string, + force = false, ): Promise { const messageId = processor.getMessageId(message); const chatId = processor.getChatId(message); const mimetype = processor.getMimetype(message); const filename = processor.getFilename(message); - if (!this.shouldProcessMimetype(mimetype)) { + if (!this.shouldProcessMimetype(mimetype, force)) { this.log.info( `The message '${messageId}' has '${mimetype}' mimetype media, skip it.`, ); @@ -105,6 +122,7 @@ export class MediaManager implements IMediaManager { processor: IMediaEngineProcessor, message: Message, session: string, + force = false, ): Promise { let messageId: string; try { @@ -128,7 +146,12 @@ export class MediaManager implements IMediaManager { try { media.filename = processor.getFilename(message); media.mimetype = processor.getMimetype(message); - const data = await this.processMediaInternal(processor, message, session); + const data = await this.processMediaInternal( + processor, + message, + session, + force, + ); media = { ...media, ...data }; } catch (err) { this.log.error(err, `Error processing media for message '${messageId}'`);