import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils'; import { CoreMediaConverter, IMediaConverter, } from '@waha/core/media/IConverter'; import { MessagesForRead } from '@waha/core/utils/convertors'; import { IgnoreJidConfig, isJidNewsletter, JidFilter, } from '@waha/core/utils/jids'; import { Channel, ChannelListResult, ChannelMessage, ChannelSearchByText, ChannelSearchByView, CreateChannelRequest, ListChannelsQuery, PreviewChannelMessages, } from '@waha/structures/channels.dto'; import { ChatSummary, GetChatMessageQuery, GetChatMessagesFilter, GetChatMessagesQuery, OverviewFilter, ReadChatMessagesQuery, ReadChatMessagesResponse, } from '@waha/structures/chats.dto'; import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto'; import { SendListRequest } from '@waha/structures/chatting.list.dto'; import { BinaryFile, RemoteFile } from '@waha/structures/files.dto'; import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto'; import { LidToPhoneNumber } from '@waha/structures/lids.dto'; import { PaginationParams } from '@waha/structures/pagination.dto'; import { MessageSource, WAMessage } from '@waha/structures/responses.dto'; import { BrowserTraceQuery } from '@waha/structures/server.debug.dto'; import { DefaultMap } from '@waha/utils/DefaultMap'; import { generatePrefixedId } from '@waha/utils/ids'; import { LoggerBuilder } from '@waha/utils/logging'; import { complete } from '@waha/utils/reactive/complete'; import { SwitchObservable } from '@waha/utils/reactive/SwitchObservable'; import axios from 'axios'; import axiosRetry from 'axios-retry'; import * as fs from 'fs'; import * as lodash from 'lodash'; import * as NodeCache from 'node-cache'; import { Logger } from 'pino'; import { BehaviorSubject, catchError, delay, filter, of, retry, share, Subject, switchMap, } from 'rxjs'; import { distinctUntilChanged, map } from 'rxjs/operators'; import { MessageId } from 'whatsapp-web.js'; import { ChatRequest, CheckNumberStatusQuery, EditMessageRequest, MessageButtonReply, MessageContactVcardRequest, MessageFileRequest, MessageForwardRequest, MessageImageRequest, MessageLinkCustomPreviewRequest, MessageLinkPreviewRequest, MessageLocationRequest, MessagePollRequest, MessagePollVoteRequest, MessageReactionRequest, MessageReplyRequest, MessageStarRequest, MessageTextRequest, MessageVideoRequest, MessageVoiceRequest, SendSeenRequest, } from '../../structures/chatting.dto'; import { ContactQuery, ContactRequest, ContactUpdateBody, } from '../../structures/contacts.dto'; import { WAHAEngine, WAHAEvents, WAHAPresenceStatus, WAHASessionStatus, } from '../../structures/enums.dto'; import { EventMessageRequest } from '../../structures/events.dto'; import { CreateGroupRequest, GroupField, GroupsListFields, ParticipantsRequest, SettingsSecurityChangeInfo, } from '../../structures/groups.dto'; import { WAHAChatPresences } from '../../structures/presence.dto'; import { MeInfo, ProxyConfig, SessionConfig, } from '../../structures/sessions.dto'; import { DeleteStatusRequest, ImageStatus, TextStatus, VideoStatus, VoiceStatus, } from '../../structures/status.dto'; import { WASessionStatusBody } from '../../structures/webhooks.dto'; import { AvailableInPlusVersion, NotImplementedByEngineError, } from '../exceptions'; import { IMediaManager } from '../media/IMediaManager'; import { QR } from '../QR'; import { DataStore } from './DataStore'; // eslint-disable-next-line @typescript-eslint/no-var-requires const qrcode = require('qrcode-terminal'); axiosRetry(axios, { retries: 3 }); const CHROME_PATH = '/usr/bin/google-chrome-stable'; const CHROMIUM_PATH = '/usr/bin/chromium'; export function getBrowserExecutablePath() { if (fs.existsSync(CHROME_PATH)) { return CHROME_PATH; } return CHROMIUM_PATH; } export function ensureSuffix(phone) { const suffix = '@c.us'; if (phone.includes('@')) { return phone; } return phone + suffix; } export interface SessionParams { name: string; printQR: boolean; mediaManager: IMediaManager; loggerBuilder: LoggerBuilder; sessionStore: DataStore; proxyConfig?: ProxyConfig; // Raw unchanged SessionConfig sessionConfig?: SessionConfig; engineConfig?: any; // Ignore settings ignore: IgnoreJidConfig; } export abstract class WhatsappSession { public engine: WAHAEngine; public name: string; protected mediaManager: IMediaManager; public loggerBuilder: LoggerBuilder; protected logger: Logger; protected sessionStore: DataStore; protected proxyConfig?: ProxyConfig; public sessionConfig?: SessionConfig; protected engineConfig?: any; protected unpairing: boolean = false; protected jids: JidFilter; private _status: WAHASessionStatus; private shouldPrintQR: boolean; protected events2: DefaultMap>; private status$: Subject; protected profilePictures: NodeCache = new NodeCache({ stdTTL: 24 * 60 * 60, // 1 day }); // Save sent messages ids in cache so we can determine if a message was sent // via API or APP private sentMessageIds: NodeCache = new NodeCache({ stdTTL: 10 * 60, // 10 minutes }); public mediaConverter: IMediaConverter = new CoreMediaConverter(); public constructor({ name, printQR, loggerBuilder, sessionStore, proxyConfig, mediaManager, sessionConfig, engineConfig, ignore, }: SessionParams) { this.status$ = new BehaviorSubject(null); this.name = name; this.proxyConfig = proxyConfig; this.loggerBuilder = loggerBuilder; this.logger = loggerBuilder.child({ name: 'WhatsappSession' }); this.events2 = new DefaultMap>( (key) => new SwitchObservable((obs$) => { return obs$.pipe( catchError((err) => { this.logger.error( `Caught error, dropping value from, event: '${key}'`, ); this.logger.error(err, err.stack); throw err; }), filter(Boolean), map((data) => { data._eventId = generatePrefixedId('evt'); data._timestampMs = Date.now(); return data; }), retry(), share(), ); }), ); this.events2.get(WAHAEvents.SESSION_STATUS).switch( this.status$ // initial value is null .pipe(filter(Boolean)) // Wait for WORKING status to get all the info // https://github.com/devlikeapro/waha/issues/409 .pipe( switchMap((status: WAHASessionStatus) => { const me = this.getSessionMeInfo(); const hasMe = !!me?.pushName && !!me?.id; // Delay WORKING by 1 second if condition is met // Usually we get WORKING with all the info after if (status === WAHASessionStatus.WORKING && !hasMe) { return of(status).pipe(delay(2000)); } return of(status); }), // Remove consecutive duplicate WORKING statuses distinctUntilChanged( (prev, curr) => prev === curr && curr === WAHASessionStatus.WORKING, ), ) // Populate the session info .pipe( map((status) => { return { name: this.name, status: status }; }), ), ); this.sessionStore = sessionStore; this.mediaManager = mediaManager; this.sessionConfig = sessionConfig; this.engineConfig = engineConfig; this.shouldPrintQR = printQR; this.logger.info( { ignore: ignore }, 'The session ignores the following chat ids', ); this.jids = new JidFilter(ignore); } public getEventObservable(event: WAHAEvents) { return this.events2.get(event); } protected set status(value: WAHASessionStatus) { if (this.unpairing && value !== WAHASessionStatus.STOPPED) { // In case of unpairing // wait for STOPPED event, ignore the rest return; } this._status = value; this.status$.next(value); } public get status() { return this._status; } getBrowserExecutablePath() { return getBrowserExecutablePath(); } getBrowserArgsForPuppeteer() { // Run optimized version of Chrome // References: // https://github.com/pedroslopez/whatsapp-web.js/issues/1420 // https://github.com/wppconnect-team/wppconnect/issues/1326 // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process // https://www.bannerbear.com/blog/ways-to-speed-up-puppeteer-screenshots/ return [ '--disable-accelerated-2d-canvas', '--disable-application-cache', // DO NOT disable software rasterizer, it will break the video // https://github.com/devlikeapro/waha/issues/629 // '--disable-software-rasterizer', '--disable-client-side-phishing-detection', '--disable-component-update', '--disable-default-apps', '--disable-dev-shm-usage', '--disable-extensions', // '--disable-features=site-per-process', // COMMENTED to test WEBJS stability '--disable-gpu', // COMMENTED to test WEBJS stability '--disable-offer-store-unmasked-wallet-cards', '--disable-offline-load-stale-cache', '--disable-popup-blocking', '--disable-setuid-sandbox', '--disable-site-isolation-trials', '--disable-speech-api', '--disable-sync', '--disable-translate', '--disable-web-security', '--hide-scrollbars', '--ignore-certificate-errors', '--ignore-ssl-errors', // https://github.com/devlikeapro/waha/issues/725 // '--in-process-gpu', // COMMENTED to test WEBJS stability '--metrics-recording-only', '--mute-audio', '--no-default-browser-check', '--no-first-run', '--no-pings', '--no-sandbox', '--no-zygote', '--password-store=basic', '--renderer-process-limit=2', '--safebrowsing-disable-auto-update', '--use-mock-keychain', '--window-size=1280,720', // // Cache options // '--disk-cache-size=1073741824', // 1GB // '--disk-cache-size=0', // '--disable-cache', // '--aggressive-cache-discard', ]; } protected isDebugEnabled() { return this.logger.isLevelEnabled('debug'); } /** Start the session */ abstract start(); /** Stop the session */ abstract stop(): Promise; protected stopEvents() { complete(this.events2); } /* Unpair the account */ async unpair(): Promise { return; } /** * START - Methods for API */ public browserTrace(query: BrowserTraceQuery): Promise { throw new NotImplementedByEngineError(); } /** * Auth methods */ public getQR(): QR { throw new NotImplementedByEngineError(); } public requestCode(phoneNumber: string, method: string, params?: any) { throw new NotImplementedByEngineError(); } abstract getScreenshot(): Promise; public getSessionMeInfo(): MeInfo | null { return null; } /** * Profile methods */ public setProfileName(name: string): Promise { throw new NotImplementedByEngineError(); } public setProfileStatus(status: string): Promise { throw new NotImplementedByEngineError(); } public async updateProfilePicture( file: BinaryFile | RemoteFile | null, ): Promise { if (file) { await this.setProfilePicture(file); } else { await this.deleteProfilePicture(); } // Refresh profile picture after update setTimeout(() => { this.logger.debug('Refreshing my profile picture after update...'); this.refreshMyProfilePicture() .then(() => { this.logger.debug('Refreshed my profile picture after update'); }) .catch((err) => { this.logger.error('Error refreshing my profile picture after update'); this.logger.error(err, err.stack); }); }, 3_000); return true; } protected async refreshMyProfilePicture() { const me = this.getSessionMeInfo(); await this.getContactProfilePicture(me.id, true); } protected setProfilePicture(file: BinaryFile | RemoteFile): Promise { throw new NotImplementedByEngineError(); } protected deleteProfilePicture(): Promise { throw new NotImplementedByEngineError(); } /** * Other methods */ generateNewMessageId(): Promise { throw new NotImplementedByEngineError(); } abstract checkNumberStatus(request: CheckNumberStatusQuery); abstract sendText(request: MessageTextRequest); sendContactVCard(request: MessageContactVcardRequest) { throw new NotImplementedByEngineError(); } sendPoll(request: MessagePollRequest) { throw new NotImplementedByEngineError(); } sendPollVote(request: MessagePollVoteRequest) { throw new NotImplementedByEngineError(); } abstract sendLocation(request: MessageLocationRequest); sendLinkPreview(request: MessageLinkPreviewRequest) { throw new NotImplementedByEngineError(); } sendLinkCustomPreview( request: MessageLinkCustomPreviewRequest, ): Promise { throw new NotImplementedByEngineError(); } abstract forwardMessage(request: MessageForwardRequest): Promise; abstract sendImage(request: MessageImageRequest); abstract sendFile(request: MessageFileRequest); abstract sendVoice(request: MessageVoiceRequest); sendVideo(request: MessageVideoRequest) { throw new NotImplementedByEngineError(); } sendButtons(request: SendButtonsRequest) { throw new NotImplementedByEngineError(); } sendList(request: SendListRequest): Promise { throw new NotImplementedByEngineError(); } sendButtonsReply(request: MessageButtonReply) { throw new NotImplementedByEngineError(); } abstract reply(request: MessageReplyRequest); abstract sendSeen(chat: SendSeenRequest); abstract startTyping(chat: ChatRequest); abstract stopTyping(chat: ChatRequest); abstract setReaction(request: MessageReactionRequest); setStar(request: MessageStarRequest): Promise { throw new NotImplementedByEngineError(); } sendEvent(request: EventMessageRequest): Promise { throw new NotImplementedByEngineError(); } cancelEvent(eventId: string): Promise { throw new NotImplementedByEngineError(); } /** * Chats methods */ public getChats(pagination: PaginationParams) { throw new NotImplementedByEngineError(); } public getChatsOverview( pagination: PaginationParams, filter?: OverviewFilter, ): Promise { throw new NotImplementedByEngineError(); } public deleteChat(chatId) { throw new NotImplementedByEngineError(); } public getChatMessages( chatId: string, query: GetChatMessagesQuery, filter: GetChatMessagesFilter, ): Promise { throw new NotImplementedByEngineError(); } abstract readChatMessages( chatId: string, request: ReadChatMessagesQuery, ): Promise; protected async readChatMessagesWSImpl( chatId: string, request: ReadChatMessagesQuery, ): Promise { const { query, filter } = MessagesForRead(chatId, request); const messages = await this.getChatMessages(chatId, query, filter); this.logger.debug(`Found ${messages.length} messages to read`); if (messages.length === 0) { return { ids: [] }; } const ids = messages.map((m) => m.id); const seen: SendSeenRequest = { chatId: chatId, messageIds: ids, session: '', }; await this.sendSeen(seen); return { ids: ids }; } public getChatMessage( chatId: string, messageId: string, query: GetChatMessageQuery, ): Promise { throw new NotImplementedByEngineError(); } public pinMessage( chatId: string, messageId: string, duration: number, ): Promise { throw new NotImplementedByEngineError(); } public unpinMessage(chatId: string, messageId: string): Promise { throw new NotImplementedByEngineError(); } public deleteMessage(chatId: string, messageId: string) { throw new NotImplementedByEngineError(); } public editMessage( chatId: string, messageId: string, request: EditMessageRequest, ) { throw new NotImplementedByEngineError(); } public clearMessages(chatId) { throw new NotImplementedByEngineError(); } public chatsArchiveChat(chatId: string): Promise { throw new NotImplementedByEngineError(); } public chatsUnarchiveChat(chatId: string): Promise { throw new NotImplementedByEngineError(); } public chatsUnreadChat(chatId: string): Promise { throw new NotImplementedByEngineError(); } /** * Labels methods */ public async getLabel(labelId: string): Promise