diff --git a/docs/site/content/en/docs/how-to/config/index.md b/docs/site/content/en/docs/how-to/config/index.md index 3c5c493d..b16b3a7b 100644 --- a/docs/site/content/en/docs/how-to/config/index.md +++ b/docs/site/content/en/docs/how-to/config/index.md @@ -72,14 +72,34 @@ Keep in mind that session's proxy configuration takes precedence over proxy conf The following environment variables can be used to configure the file storage options for the WAHA: -- `WHATSAPP_FILES_MIMETYPES`: This variable can be used to download only specific mimetypes from messages. By default, - all files are downloaded. The mimetypes must be separated by a comma, without spaces. For - example: `audio,image/png,image/gif`. To choose a specific type, use a prefix (like `audio,image`). +- `WHATSAPP_FILES_MIMETYPES`: This variable can be used to download only specific mimetypes from messages. + By default, all files are downloaded. The mimetypes must be separated by a comma, without spaces. + For example: `audio,image/png,image/gif`. To choose a specific type, use a prefix (like `audio,image`). See usage below. +- `WHATSAPP_DOWNLOAD_MEDIA=true` - this variable can be used to **completely** disable downloading media files. By default, all files are downloaded. + Set this variable to `WHATSAPP_DOWNLOAD_MEDIA=false` to disable downloading media files. + - Under the hood, it sets `WHATSAPP_FILES_MIMETYPES=mimetype/ignore-all-media` to ignore all media files. - `WHATSAPP_FILES_LIFETIME`: This variable can be used to set the time (in seconds) after which files will be removed to free up space. The default value is `180`. - `WHATSAPP_FILES_FOLDER`: This variable can be used to set the folder where files from chats (images, voice messages) will be stored. The default value is `/tmp/whatsapp-files`. +💡 Even if WAHA doesn't process the message media because of `WHATSAPP_FILES_MIMETYPES` or `WHATSAPP_DOWNLOAD_MEDIA` +you'll get a webhook event with `hasMedia: True` field, but with no `media.url`. +```json +{ + "event": "message", + "session": "default", + "payload": { + "hasMedia": true, + "media": { + "url": null, + "mimetype": "video/mp4", + "filename": null + } + } +} +``` + ## Examples #### Debug Mode @@ -138,6 +158,13 @@ comma-separated list of mimetypes: WHATSAPP_FILES_MIMETYPES=audio,image/png,image/gif ``` +#### Disable Downloading Media Files +To disable downloading media files, set the `WHATSAPP_DOWNLOAD_MEDIA` environment variable to `false`: + +``` +WHATSAPP_DOWNLOAD_MEDIA=false +``` + #### Setting the File Lifetime To set the time (in seconds) after which files will be removed to free up space, set the `WHATSAPP_FILES_LIFETIME` diff --git a/docs/site/content/en/docs/how-to/receive-messages/index.md b/docs/site/content/en/docs/how-to/receive-messages/index.md index c439c1d3..a2f63160 100644 --- a/docs/site/content/en/docs/how-to/receive-messages/index.md +++ b/docs/site/content/en/docs/how-to/receive-messages/index.md @@ -155,9 +155,9 @@ Receive events when server or recipient gets the message, read or played it. ## Files ![](/images/versions/plus.png) When people send you media - images, voice messages, and documents - WAHA saves it in the file storage. -In your application you must download it and use it as you want to. You can find the URL in `mediaUrl` field. +In your application you must download it and use it as you want to. You can find the URL in `media.url` field. -For example, you can get the webhook like this with `mediaUrl` value (we've skipped other fields): +For example, you can get the webhook like this with `media` value (we've skipped other fields): ```json { @@ -167,7 +167,12 @@ For example, you can get the webhook like this with `mediaUrl` value (we've skip "id": "true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "timestamp": 1667561485, "from": "11111111111@c.us", - "mediaUrl": "http://localhost:3000/api/files/true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.jpg" + "hasMedia": true, + "media": { + "url": "http://localhost:3000/api/files/true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.jpg", + "mimetype": "image/jpeg", + "filename": null + } } } ``` @@ -182,8 +187,11 @@ For documents (file attachments) there's also `filename` field with the original "id": "true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "timestamp": 1667561485, "from": "11111111111@c.us", - "mediaUrl": "http://localhost:3000/api/files/true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.pdf", - "filename": "some-file.pdf" + "media": { + "url": "http://localhost:3000/api/files/true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.pdf", + "filename": "some-file.pdf", + "mimetype": "application/pdf" + } } } ``` diff --git a/docs/site/content/en/docs/overview/changelog.md b/docs/site/content/en/docs/overview/changelog.md index 52a15518..0a1902bc 100644 --- a/docs/site/content/en/docs/overview/changelog.md +++ b/docs/site/content/en/docs/overview/changelog.md @@ -21,6 +21,25 @@ You even can **subscribe to get new updates** there! ## 2023.11 November 2023 +- Add dedicated `media` field in webhook payload - now you can know which media is attached to the message. +```json +{ + "event": "message", + "session": "default", + "payload": { + "hasMedia": true, + "media": { + "url": "http://localhost:3000/api/files/true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.pdf", + "mimetype": "application/pdf", + "filename": "file.pdf" + } + } +} + +``` +- Add `WHATSAPP_DOWNLOAD_MEDIA` environment variable to control media download. Available only in Plus version. + - `WHATSAPP_DOWNLOAD_MEDIA=True` - download media + - `WHATSAPP_DOWNLOAD_MEDIA=False` - do not download media at all. - Add `filename` field for document (file attachments) - the original filename of the document. - Add `/api/sendVideo` endpoint to **NOWEB** and **WEBJS** engines. Available only in Plus version. - Add security settings for edit group info `GET /api/{session}/groups/{groupId}/settings/security/info-admin-only` - updates the group settings to only allow admins to edit group info (title, description, photo). diff --git a/src/api/sessions.controller.ts b/src/api/sessions.controller.ts index 5994f51c..5b420eac 100644 --- a/src/api/sessions.controller.ts +++ b/src/api/sessions.controller.ts @@ -1,5 +1,4 @@ import { Body, Controller, Get, Post, Query } from '@nestjs/common'; -import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception'; import { ApiExtraModels, ApiOperation, @@ -8,6 +7,7 @@ import { } from '@nestjs/swagger'; import { SessionManager } from '../core/abc/manager.abc'; +import { WhatsappSession } from '../core/abc/session.abc'; import { parseBool } from '../helpers'; import { ListSessionsQuery, @@ -19,9 +19,6 @@ import { SessionStopRequest, } from '../structures/sessions.dto'; import { SessionApiParam, SessionParam } from './helpers'; -import { WhatsappSession } from '../core/abc/session.abc'; -import { WAHAChatPresences } from '../structures/presence.dto'; -import { WAHAWebhook } from '../structures/webhooks.dto'; @ApiSecurity('api_key') @Controller('api/sessions') @@ -34,7 +31,7 @@ class SessionsController { const result = await this.manager.start(request); await this.manager.sessionStorage.configRepository.save( request.name, - request.config, + request.config || null, ); return result; } @@ -71,7 +68,7 @@ class SessionController { @Get('me') @SessionApiParam @ApiOperation({ summary: 'Get information about the authenticated account' }) - getMe(@SessionParam session: WhatsappSession): Promise { + getMe(@SessionParam session: WhatsappSession): Promise { return session.getSessionMeInfo(); } } diff --git a/src/config.service.ts b/src/config.service.ts index 266a0fd0..3b73f322 100644 --- a/src/config.service.ts +++ b/src/config.service.ts @@ -36,9 +36,17 @@ export class WhatsappConfigService { return this.configService.get('WHATSAPP_FILES_LIFETIME', 180); } - get mimetypes(): string[] | null { + get mimetypes(): string[] { + if (!this.shouldDownloadMedia) { + return ['mimetype/ignore-all-media']; + } const types = this.configService.get('WHATSAPP_FILES_MIMETYPES', ''); - return types ? types.split(',') : null; + return types ? types.split(',') : []; + } + + get shouldDownloadMedia(): boolean { + const value = this.configService.get('WHATSAPP_DOWNLOAD_MEDIA', 'true'); + return parseBool(value); } get startSessions(): string[] { @@ -111,7 +119,7 @@ export class WhatsappConfigService { return WAHAEngine.WEBJS; } - get(name: string, defaultValue = undefined): any { + get(name: string, defaultValue: any = undefined): any { return this.configService.get(name, defaultValue); } diff --git a/src/core/abc/manager.abc.ts b/src/core/abc/manager.abc.ts index 9460f8f0..239ec0f7 100644 --- a/src/core/abc/manager.abc.ts +++ b/src/core/abc/manager.abc.ts @@ -9,7 +9,7 @@ import { SessionStopRequest, } from '../../structures/sessions.dto'; import { WhatsappSession } from './session.abc'; -import { LocalSessionStorage, MediaStorage } from './storage.abc'; +import { LocalSessionStorage } from './storage.abc'; import { WebhookConductor } from './webhooks.abc'; export abstract class SessionManager implements OnApplicationShutdown { @@ -21,8 +21,6 @@ export abstract class SessionManager implements OnApplicationShutdown { protected abstract get WebhookConductorClass(): typeof WebhookConductor; - protected abstract get MediaStorageClass(): typeof MediaStorage; - abstract onApplicationShutdown(signal?: string); // @@ -34,7 +32,7 @@ export abstract class SessionManager implements OnApplicationShutdown { abstract logout(request: SessionLogoutRequest): Promise; - abstract getSession(name: string, error?: boolean): WhatsappSession; + abstract getSession(name: string): WhatsappSession; abstract getSessions(all: boolean): Promise; } diff --git a/src/core/abc/media.abc.ts b/src/core/abc/media.abc.ts new file mode 100644 index 00000000..c5d737b5 --- /dev/null +++ b/src/core/abc/media.abc.ts @@ -0,0 +1,28 @@ +abstract class MediaStorage { + abstract save( + messageId: string, + mimetype: string, + buffer: Buffer, + ): Promise; +} + +interface IEngineMediaProcessor { + hasMedia(message: Message): boolean; + + getFilename(message: Message): string | null; + + getMimetype(message: Message): string; + + getMessageId(message: Message): string; + + getMediaBuffer(message: Message): Promise; +} + +interface MediaManager { + processMedia( + processor: IEngineMediaProcessor, + message: Message, + ): Promise; +} + +export { IEngineMediaProcessor, MediaManager, MediaStorage }; diff --git a/src/core/abc/session.abc.ts b/src/core/abc/session.abc.ts index e70c928a..6f2b515f 100644 --- a/src/core/abc/session.abc.ts +++ b/src/core/abc/session.abc.ts @@ -3,6 +3,7 @@ import { EventEmitter } from 'events'; import * as fs from 'fs'; import { MessageId } from 'whatsapp-web.js'; +import { OTPRequest, RequestCodeRequest } from '../../structures/auth.dto'; import { ChatRequest, CheckNumberStatusQuery, @@ -37,9 +38,6 @@ import { ProxyConfig, SessionConfig, } from '../../structures/sessions.dto'; -import { NotImplementedByEngineError } from '../exceptions'; -import { LocalSessionStorage, MediaStorage } from './storage.abc'; -import { OTPRequest, RequestCodeRequest } from '../../structures/auth.dto'; import { ImageStatus, TextStatus, @@ -47,6 +45,9 @@ import { VoiceStatus, } from '../../structures/status.dto'; import { WASessionStatusBody } from '../../structures/webhooks.dto'; +import { NotImplementedByEngineError } from '../exceptions'; +import { MediaManager } from './media.abc'; +import { LocalSessionStorage } from './storage.abc'; const CHROME_PATH = '/usr/bin/google-chrome-stable'; const CHROMIUM_PATH = '/usr/bin/chromium'; @@ -73,11 +74,11 @@ export enum WAHAInternalEvent { export interface SessionParams { name: string; - storage: MediaStorage; + mediaManager: MediaManager; log: ConsoleLogger; sessionStorage: LocalSessionStorage; proxyConfig?: ProxyConfig; - sessionConfig: SessionConfig; + sessionConfig?: SessionConfig; } export abstract class WhatsappSession { @@ -85,11 +86,11 @@ export abstract class WhatsappSession { public engine: WAHAEngine; public name: string; - protected storage: MediaStorage; + protected mediaManager: MediaManager; protected log: ConsoleLogger; protected sessionStorage: LocalSessionStorage; protected proxyConfig?: ProxyConfig; - public sessionConfig: SessionConfig; + public sessionConfig?: SessionConfig; private _status: WAHASessionStatus; @@ -98,7 +99,7 @@ export abstract class WhatsappSession { log, sessionStorage, proxyConfig, - storage, + mediaManager, sessionConfig, }: SessionParams) { this.events = new EventEmitter(); @@ -106,7 +107,7 @@ export abstract class WhatsappSession { this.proxyConfig = proxyConfig; this.log = log; this.sessionStorage = sessionStorage; - this.storage = storage; + this.mediaManager = mediaManager; this.sessionConfig = sessionConfig; } diff --git a/src/core/abc/storage.abc.ts b/src/core/abc/storage.abc.ts index 0ed7fe74..5c4c005a 100644 --- a/src/core/abc/storage.abc.ts +++ b/src/core/abc/storage.abc.ts @@ -1,17 +1,12 @@ import { SessionConfig } from '../../structures/sessions.dto'; -abstract class MediaStorage { - abstract save( - messageId: string, - mimetype: string, - buffer: Buffer, - ): Promise; -} - abstract class SessionConfigRepository { - abstract save(sessionName: string, config: SessionConfig): Promise; + abstract save( + sessionName: string, + config: SessionConfig | null, + ): Promise; - abstract get(sessionName: string): Promise; + abstract get(sessionName: string): Promise; } abstract class SessionStorage { @@ -37,4 +32,4 @@ abstract class LocalSessionStorage extends SessionStorage { abstract getFolderPath(sessionName: string): string; } -export { LocalSessionStorage, MediaStorage, SessionConfigRepository }; +export { LocalSessionStorage, SessionConfigRepository }; diff --git a/src/core/helpers.proxy.ts b/src/core/helpers.proxy.ts index 639fbbee..f9223981 100644 --- a/src/core/helpers.proxy.ts +++ b/src/core/helpers.proxy.ts @@ -9,7 +9,7 @@ import { WhatsappSession } from './abc/session.abc'; export function getProxyConfig( config: WhatsappConfigService, sessions: Record, - sessionName?: string, + sessionName: string, ): ProxyConfig | undefined { // Single proxy server configuration if (typeof config.proxyServer === 'string') { @@ -23,7 +23,7 @@ export function getProxyConfig( // Multiple proxy server configuration if (Array.isArray(config.proxyServer)) { const prefix = config.proxyServerIndexPrefix; - let indexToUse: number = undefined; + let indexToUse: number | undefined = undefined; if (prefix && sessionName.includes(prefix)) { // match numbers at the end of the string const matches = sessionName.match(/\d+$/); @@ -66,8 +66,8 @@ function getAuthenticatedUrl( export function createAgentProxy(proxyConfig: ProxyConfig): Agent | undefined { const url = getAuthenticatedUrl( proxyConfig.server, - proxyConfig.username, - proxyConfig.password, + proxyConfig.username || '', + proxyConfig.password || '', ); return new HttpsProxyAgent(url); } diff --git a/src/core/manager.core.ts b/src/core/manager.core.ts index 0bd5bf61..ea37bf86 100644 --- a/src/core/manager.core.ts +++ b/src/core/manager.core.ts @@ -18,17 +18,14 @@ import { } from '../structures/sessions.dto'; import { WebhookConfig } from '../structures/webhooks.config.dto'; import { SessionManager } from './abc/manager.abc'; -import { - SessionParams, - WAHAInternalEvent, - WhatsappSession, -} from './abc/session.abc'; +import { SessionParams, WhatsappSession } from './abc/session.abc'; import { DOCS_URL } from './exceptions'; import { getProxyConfig } from './helpers.proxy'; +import { CoreMediaManager, MediaStorageCore } from './media.core'; import { WhatsappSessionNoWebCore } from './session.noweb.core'; import { WhatsappSessionVenomCore } from './session.venom.core'; import { WhatsappSessionWebJSCore } from './session.webjs.core'; -import { MediaStorageCore, SessionStorageCore } from './storage.core'; +import { SessionStorageCore } from './storage.core'; import { WebhookConductorCore } from './webhooks.core'; export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException { @@ -45,11 +42,9 @@ export function buildLogger(name) { @Injectable() export class SessionManagerCore extends SessionManager { - private session: WhatsappSession; + private session?: WhatsappSession; DEFAULT = 'default'; - // @ts-ignore - protected MediaStorageClass = MediaStorageCore; // @ts-ignore protected WebhookConductorClass = WebhookConductorCore; protected readonly EngineClass: typeof WhatsappSession; @@ -110,13 +105,16 @@ export class SessionManagerCore extends SessionManager { const name = request.name; this.log.log(`'${name}' - starting session...`); const log = buildLogger(`WhatsappSession - ${name}`); - const storage = new this.MediaStorageClass(); + const mediaManager = new CoreMediaManager( + new MediaStorageCore(), + this.config.mimetypes, + ); const webhookLog = buildLogger(`Webhook - ${name}`); const webhook = new this.WebhookConductorClass(webhookLog); const proxyConfig = this.getProxyConfig(request); const sessionConfig: SessionParams = { name, - storage, + mediaManager, log, sessionStorage: this.sessionStorage, proxyConfig: proxyConfig, @@ -149,7 +147,9 @@ export class SessionManagerCore extends SessionManager { webhooks = webhooks.concat(request.config.webhooks); } const globalWebhookConfig = this.config.getWebhookConfig(); - webhooks.push(globalWebhookConfig); + if (globalWebhookConfig) { + webhooks.push(globalWebhookConfig); + } return webhooks; } @@ -162,6 +162,9 @@ export class SessionManagerCore extends SessionManager { if (request.config?.proxy) { return request.config.proxy; } + if (!this.session) { + return undefined; + } const sessions = { [request.name]: this.session }; return getProxyConfig(this.config, sessions, request.name); } @@ -181,16 +184,13 @@ export class SessionManagerCore extends SessionManager { await this.sessionStorage.clean(request.name); } - getSession(name: string, error = true): WhatsappSession { + getSession(name: string): WhatsappSession { this.onlyDefault(name); const session = this.session; if (!session) { - if (error) { - throw new NotFoundException( - `We didn't find a session with name '${name}'. Please start it first by using POST /sessions/start request`, - ); - } - return; + throw new NotFoundException( + `We didn't find a session with name '${name}'. Please start it first by using POST /sessions/start request`, + ); } return session; } diff --git a/src/core/media.core.ts b/src/core/media.core.ts new file mode 100644 index 00000000..6c8c91bb --- /dev/null +++ b/src/core/media.core.ts @@ -0,0 +1,45 @@ +import { + IEngineMediaProcessor, + MediaManager, + MediaStorage, +} from './abc/media.abc'; +import { DOCS_URL } from './exceptions'; +import { WAMedia } from '../structures/responses.dto'; +import * as process from 'process'; + +export class MediaStorageCore implements MediaStorage { + async save( + messageId: string, + mimetype: string, + buffer: Buffer, + ): Promise { + return Promise.resolve( + `Media attachment's available only in WAHA Plus version. ${DOCS_URL}`, + ); + } +} + +export class CoreMediaManager implements MediaManager { + constructor(private storage: MediaStorage, private mimetypes: string[]) {} + + async processMedia( + processor: IEngineMediaProcessor, + message: Message, + ): Promise { + if (!processor.hasMedia(message)) { + return message; + } + const mimetype = ''; + const filename = processor.getFilename(message); + const url = await this.storage.save('', mimetype, Buffer.from('')); + const media: WAMedia = { + mimetype: mimetype, + filename: filename, + url: url, + }; + + // @ts-ignore + message.media = media; + return message; + } +} diff --git a/src/core/session.noweb.core.ts b/src/core/session.noweb.core.ts index 757aa0c0..9f3b50d1 100644 --- a/src/core/session.noweb.core.ts +++ b/src/core/session.noweb.core.ts @@ -17,7 +17,6 @@ import * as fs from 'fs/promises'; import { Agent } from 'https'; import * as lodash from 'lodash'; import { PairingCodeResponse } from 'src/structures/auth.dto'; -import { Message } from 'whatsapp-web.js'; import { flipObject, splitAt } from '../helpers'; import { @@ -55,12 +54,14 @@ import { WAHAPresenceData, } from '../structures/presence.dto'; import { WAMessage } from '../structures/responses.dto'; +import { MeInfo } from '../structures/sessions.dto'; import { BROADCAST_ID, TextStatus } from '../structures/status.dto'; import { PollVote, PollVotePayload, WAMessageAckBody, } from '../structures/webhooks.dto'; +import { IEngineMediaProcessor } from './abc/media.abc'; import { ensureSuffix, WAHAInternalEvent, @@ -72,7 +73,7 @@ import { } from './exceptions'; import { createAgentProxy } from './helpers.proxy'; import { QR } from './QR'; -import { MeInfo } from '../structures/sessions.dto'; +import * as Buffer from 'buffer'; // eslint-disable-next-line @typescript-eslint/no-var-requires const QRCode = require('qrcode'); @@ -659,11 +660,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { body: body, to: toCusFormat(fromToParticipant.to), participant: toCusFormat(fromToParticipant.participant), - // @ts-ignore - hasMedia: Boolean(message.mediaUrl), - // @ts-ignore - mediaUrl: message.mediaUrl, - filename: message.message?.documentMessage?.fileName, + // Media + hasMedia: Boolean(message.media), + media: message.media, + mediaUrl: message.media?.url, // @ts-ignore ack: message.ack, // @ts-ignore @@ -812,14 +812,39 @@ export class WhatsappSessionNoWebCore extends WhatsappSession { return { id: chatId, presences: presences }; } - protected async downloadMedia(message: Message) { - if (!message.hasMedia) { - return message; - } + protected downloadMedia(message) { + const processor = new EngineMediaProcessor(this); + return this.mediaManager.processMedia(processor, message); + } +} - // @ts-ignore - message.mediaUrl = await this.storage.save(message.key.id, '', undefined); - return message; +export class EngineMediaProcessor implements IEngineMediaProcessor { + constructor(public session: WhatsappSessionNoWebCore) {} + + hasMedia(message: any): boolean { + const messageType = Object.keys(message.message)[0]; + const hasMedia = + messageType === 'imageMessage' || + messageType == 'audioMessage' || + messageType == 'documentMessage' || + messageType == 'videoMessage'; + return hasMedia; + } + + getMessageId(message: any): string { + return ''; + } + + getMimetype(message: any): string { + return ''; + } + + getMediaBuffer(message: any): Promise { + return Promise.resolve(undefined); + } + + getFilename(message: any): string | null { + return message.message?.documentMessage?.fileName || null; } } diff --git a/src/core/session.venom.core.ts b/src/core/session.venom.core.ts index a29f0fb0..9f6c2615 100644 --- a/src/core/session.venom.core.ts +++ b/src/core/session.venom.core.ts @@ -21,9 +21,11 @@ import { WAHASessionStatus, } from '../structures/enums.dto'; import { WAMessage } from '../structures/responses.dto'; +import { IEngineMediaProcessor } from './abc/media.abc'; import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc'; import { NotImplementedByEngineError } from './exceptions'; import { QR } from './QR'; +import * as Buffer from 'buffer'; export class WhatsappSessionVenomCore extends WhatsappSession { engine = WAHAEngine.VENOM; @@ -246,17 +248,13 @@ export class WhatsappSessionVenomCore extends WhatsappSession { * END - Methods for API */ - protected async downloadAndDecryptMedia(message: Message) { - if (!message.isMMS || !message.isMedia) { - return message; - } - // @ts-ignore - message.mediaUrl = await this.storage.save(message.id, '', undefined); - return message; + protected downloadMedia(message: Message) { + const processor = new EngineMediaProcessor(this); + return this.mediaManager.processMedia(processor, message); } private processIncomingMessage(message: Message) { - return this.downloadAndDecryptMedia(message).then(this.toWAMessage); + return this.downloadMedia(message).then(this.toWAMessage); } protected toWAMessage(message: Message): Promise { @@ -268,10 +266,13 @@ export class WhatsappSessionVenomCore extends WhatsappSession { fromMe: message.fromMe, to: message.to, body: message.body, + // Media // @ts-ignore - hasMedia: Boolean(message.mediaUrl), + hasMedia: Boolean(message.media), // @ts-ignore - mediaUrl: message.mediaUrl, + media: message.media, + // @ts-ignore + mediaUrl: message.media?.url, // @ts-ignore ack: message.ack, location: undefined, @@ -280,3 +281,29 @@ export class WhatsappSessionVenomCore extends WhatsappSession { }); } } + +export class EngineMediaProcessor implements IEngineMediaProcessor { + constructor(public session: WhatsappSessionVenomCore) {} + + hasMedia(message: any): boolean { + if (!message.isMMS || !message.isMedia) { + return message; + } + } + + getMessageId(message: any): string { + return ''; + } + + getMimetype(message: any): string { + return ''; + } + + getMediaBuffer(message: any): Promise { + return Promise.resolve(undefined); + } + + getFilename(message: Message): string | null { + return null; + } +} diff --git a/src/core/session.webjs.core.ts b/src/core/session.webjs.core.ts index afd8b706..aebce269 100644 --- a/src/core/session.webjs.core.ts +++ b/src/core/session.webjs.core.ts @@ -40,14 +40,15 @@ import { SettingsSecurityChangeInfo, } from '../structures/groups.dto'; import { WAMessage } from '../structures/responses.dto'; +import { MeInfo } from '../structures/sessions.dto'; +import { WAMessageRevokedBody } from '../structures/webhooks.dto'; +import { IEngineMediaProcessor } from './abc/media.abc'; import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc'; import { AvailableInPlusVersion, NotImplementedByEngineError, } from './exceptions'; import { QR } from './QR'; -import { MeInfo } from '../structures/sessions.dto'; -import { WAMessageRevokedBody } from '../structures/webhooks.dto'; // eslint-disable-next-line @typescript-eslint/no-var-requires const QRCode = require('qrcode'); @@ -541,12 +542,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { fromMe: message.fromMe, to: message.to, body: message.body, + // Media // @ts-ignore - hasMedia: Boolean(message.mediaUrl), + hasMedia: Boolean(message.media), // @ts-ignore - mediaUrl: message.mediaUrl, + media: message.media, + // @ts-ignore + mediaUrl: message.media?.url, // @ts-ignore - filename: message.rawData?.filename, ack: message.ack, ackName: WAMessageAck[message.ack] || ACK_UNKNOWN, location: message.location, @@ -561,17 +564,35 @@ export class WhatsappSessionWebJSCore extends WhatsappSession { return contact; } - protected async downloadMedia(message: Message) { + protected downloadMedia(message: Message) { + const processor = new EngineMediaProcessor(); + return this.mediaManager.processMedia(processor, message); + } +} + +export class EngineMediaProcessor implements IEngineMediaProcessor { + hasMedia(message: Message): boolean { if (!message.hasMedia) { - return message; + return false; } + // Can't get media for revoked messages + return message.type !== 'revoked'; + } + + getMessageId(message: Message): string { + return ''; + } + + getMimetype(message: Message): string { + return ''; + } + + getMediaBuffer(message: Message): Promise { + return Promise.resolve(undefined); + } + getFilename(message: Message): string | null { // @ts-ignore - message.mediaUrl = await this.storage.save( - message.id._serialized, - '', - undefined, - ); - return message; + return message.rawData?.filename || null; } } diff --git a/src/core/storage.core.ts b/src/core/storage.core.ts index adfe6767..d08eac24 100644 --- a/src/core/storage.core.ts +++ b/src/core/storage.core.ts @@ -6,22 +6,8 @@ import * as path from 'path'; import { SessionConfig } from '../structures/sessions.dto'; import { LocalSessionStorage, - MediaStorage, SessionConfigRepository, } from './abc/storage.abc'; -import { DOCS_URL } from './exceptions'; - -export class MediaStorageCore implements MediaStorage { - async save( - messageId: string, - mimetype: string, - buffer: Buffer, - ): Promise { - return Promise.resolve( - `Media attachment's available only in WAHA Plus version. ${DOCS_URL}`, - ); - } -} class LocalSessionConfigRepository extends SessionConfigRepository { FILENAME = '.waha.session.config.json'; @@ -30,13 +16,13 @@ class LocalSessionConfigRepository extends SessionConfigRepository { super(); } - async get(sessionName: string): Promise { + async get(sessionName: string): Promise { const filepath = this.getFilePath(sessionName); // Check file exists try { await fs.access(filepath, fs.constants.F_OK); } catch (error) { - return undefined; + return null; } // Try to load config @@ -44,13 +30,13 @@ class LocalSessionConfigRepository extends SessionConfigRepository { try { content = await fs.readFile(filepath, 'utf-8'); } catch (error) { - return undefined; + return null; } return JSON.parse(content); } - async save(sessionName: string, config: SessionConfig) { + async save(sessionName: string, config: SessionConfig | null) { // Create folder if not exist const folder = this.storage.getFolderPath(sessionName); await fs.mkdir(folder, { recursive: true }); diff --git a/src/core/swagger.module.core.ts b/src/core/swagger.module.core.ts index 17e35c35..6a4ad797 100644 --- a/src/core/swagger.module.core.ts +++ b/src/core/swagger.module.core.ts @@ -131,7 +131,7 @@ export class SwaggerModuleCore { return document; } - protected setUpAuth(app: INestApplication) { - return undefined; + protected setUpAuth(app: INestApplication): void { + return; } } diff --git a/src/structures/properties.dto.ts b/src/structures/properties.dto.ts index 332b2599..61640fef 100644 --- a/src/structures/properties.dto.ts +++ b/src/structures/properties.dto.ts @@ -1,7 +1,6 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { WAHAEvents } from './enums.dto'; +import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger'; -function ChatIdProperty(options = undefined) { +function ChatIdProperty(options: ApiPropertyOptions | null = null) { options = options || {}; if (!options.example) { options.example = '11111111111@c.us'; diff --git a/src/structures/responses.dto.ts b/src/structures/responses.dto.ts index 39f8b555..f7655feb 100644 --- a/src/structures/responses.dto.ts +++ b/src/structures/responses.dto.ts @@ -9,6 +9,27 @@ export class WALocation { longitude: string; } +export class WAMedia { + @ApiProperty({ + description: 'The URL for the media in the message if any', + example: + 'http://localhost:3000/api/files/false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA.oga', + }) + url: string; + + @ApiProperty({ + description: 'mimetype for the media in the message if any', + example: 'audio/jpeg', + }) + mimetype: string | null; + + @ApiProperty({ + description: 'The original filename in mediaUrl in the message if any', + example: 'example.pdf', + }) + filename: string | null; +} + export class WAMessage { @MessageIdProperty() id: string; @@ -55,19 +76,17 @@ export class WAMessage { }) hasMedia: boolean; + media: WAMedia | null = null; + @ApiProperty({ - description: 'The URL for the media in the message if any', + description: + 'Use `media.url` instead! The URL for the media in the message if any', + deprecated: true, example: 'http://localhost:3000/api/files/false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA.oga', }) mediaUrl: string; - @ApiProperty({ - description: 'The original filename in mediaUrl in the message if any', - example: 'example.pdf', - }) - filename: string; - @ApiProperty({ description: 'ACK status for the message', }) diff --git a/src/structures/sessions.dto.ts b/src/structures/sessions.dto.ts index 98789b21..505b6898 100644 --- a/src/structures/sessions.dto.ts +++ b/src/structures/sessions.dto.ts @@ -65,7 +65,7 @@ export class SessionLogoutRequest { export class SessionDTO { name = 'default'; status: WAHASessionStatus; - config: SessionConfig; + config?: SessionConfig; } export class MeInfo {