import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor'; import { IMediaManager } from '@waha/core/media/IMediaManager'; import { IMediaStorage, MediaData, MediaStorageData, } from '@waha/core/media/IMediaStorage'; import { WAMedia } from '@waha/structures/media.dto'; import { Logger } from 'pino'; // eslint-disable-next-line @typescript-eslint/no-var-requires const mime = require('mime-types'); // eslint-disable-next-line @typescript-eslint/no-var-requires const promiseRetry = require('promise-retry'); export class MediaManager implements IMediaManager { // https://github.com/IndigoUnited/node-promise-retry RETRY_OPTIONS = { retries: 5, minTimeout: 100, maxTimeout: 500, }; constructor( private storage: IMediaStorage, private mimetypes: string[], protected log: Logger, ) { // Log mimetypes if (this.mimetypes && this.mimetypes.length > 0) { const mimetypes = this.mimetypes.join(','); const msg = `Only '${mimetypes}' mimetypes will be downloaded for the session`; this.log.info(msg); } } /** * Check that we need to download files with the mimetype */ private shouldProcessMimetype(mimetype: string) { // No specific mimetypes provided - always download if (!this.mimetypes || this.mimetypes.length === 0) { return true; } // Found "right" mimetype in the list of allowed mimetypes - download it return this.mimetypes.some((type) => mimetype.startsWith(type)); } private async processMediaInternal( processor: IMediaEngineProcessor, message: Message, session: string, ): 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)) { this.log.info( `The message '${messageId}' has '${mimetype}' mimetype media, skip it.`, ); return null; } const extension = mime.extension(mimetype); const mediaData: MediaData = { session: session, message: { id: messageId, chatId: chatId, }, file: { extension: extension, filename: filename, }, }; const exists = await this.withRetry('Checking media', () => this.exists(mediaData), ); if (!exists) { this.log.info(`The message ${messageId} has media, downloading it...`); // Fetching media const buffer = await this.withRetry('Fetching media', () => this.fetchMedia(message, processor), ); // Saving media await this.withRetry('Saving media', () => this.saveMedia(buffer, mediaData), ); this.log.info(`The media from '${messageId}' has been saved.`); } const data = await this.withRetry('Getting media URL', () => this.getStorageData(mediaData), ); return data; } async processMedia( processor: IMediaEngineProcessor, message: Message, session: string, ): Promise { let messageId: string; try { messageId = processor.getMessageId(message); if (!processor.hasMedia(message)) { return message; } } catch (error) { this.log.error( error, `Error checking if message has media for message '${messageId}'`, ); return message; } let media: WAMedia = { url: null, filename: null, mimetype: null, }; try { media.filename = processor.getFilename(message); media.mimetype = processor.getMimetype(message); media.filename = processor.getFilename(message); const data = await this.processMediaInternal(processor, message, session); media = { ...media, ...data }; } catch (err) { this.log.error(err, `Error processing media for message '${messageId}'`); media.error = err; // @ts-ignore media.error.details = `${err.stack}`; } return media; } private async fetchMedia( message: any, processor: IMediaEngineProcessor, ): Promise { const messageId = processor.getMessageId(message); this.log.debug(`Fetching media from WhatsApp message '${messageId}'...`); const buffer = await processor.getMediaBuffer(message); if (!buffer) { throw new Error( `Message '${messageId}' has no media, but it has media flag in the engine`, ); } return buffer; } private async saveMedia( buffer: Buffer, mediaData: MediaData, ): Promise { this.log.debug( `Saving media from WhatsApp the message '${mediaData.message.id}'...`, ); const result = await this.storage.save(buffer, mediaData); this.log.debug(`The media from '${mediaData.message.id}' has been saved.`); return result; } private async getStorageData( mediaData: MediaData, ): Promise { return await this.storage.getStorageData(mediaData); } private async exists(mediaData: MediaData): Promise { this.log.trace( `Checking if media exists for message '${mediaData.message.id}'...`, ); const result = await this.storage.exists(mediaData); this.log.trace( `Media for message '${mediaData.message.id}' exists: ${result}`, ); return result; } private async withRetry(name: string, fn: CallableFunction) { const retryOptions = this.RETRY_OPTIONS; try { return await promiseRetry((retry: CallableFunction, number: number) => { return fn().catch(retry); }, retryOptions); } catch (error) { this.log.error( error, `Failed to execute '${name}', tried '${retryOptions.retries}' times`, ); throw error; } } close() { this.storage.close().catch((err) => { this.log.error(`Failed to close media storage: ${err}`); }); } }