import { exposeFunctionIfAbsent } from '@waha/core/engines/webjs/Puppeteer'; import { WebJSPresence } from '@waha/core/engines/webjs/types'; import { GetChatMessagesFilter } from '@waha/structures/chats.dto'; import { Label } from '@waha/structures/labels.dto'; import { LidToPhoneNumber } from '@waha/structures/lids.dto'; import { PaginationParams } from '@waha/structures/pagination.dto'; import { TextStatus } from '@waha/structures/status.dto'; import { sleep } from '@waha/utils/promiseTimeout'; import { EventEmitter } from 'events'; import * as lodash from 'lodash'; import { Page } from 'puppeteer'; import { Client, Events } from 'whatsapp-web.js'; import { Message } from 'whatsapp-web.js/src/structures'; import { CallErrorEvent, PAGE_CALL_ERROR_EVENT, WPage } from './WPage'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { LoadWAHA } = require('./_WAHA.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires const { LoadLodash } = require('./_lodash.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires const { LoadPaginator } = require('./_Paginator.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory'); export class WebjsClientCore extends Client { public events = new EventEmitter(); private wpage: WPage = null; constructor( options, protected tags: boolean, ) { super(options); // Wait until it's READY and inject more utils this.on(Events.READY, async () => { await this.attachCustomEventListeners(); await this.injectWaha(); }); } async initialize() { const result = await super.initialize(); if (this.pupPage && !(this.pupPage instanceof WPage)) { this.wpage = new WPage(this.pupPage); this.wpage.on(PAGE_CALL_ERROR_EVENT as any, (event: CallErrorEvent) => { this.events.emit(PAGE_CALL_ERROR_EVENT as any, event); }); this.pupPage = this.wpage as any as Page; } return result; } async injectWaha() { await this.pupPage.evaluate(LoadLodash); await this.pupPage.evaluate(LoadPaginator); await this.pupPage.evaluate(LoadWAHA); } /** * @result indicating whether the UX fresh look was successfully hidden. */ hideUXFreshLook(): Promise { return this.pupPage.evaluate(() => { const WAWebUserPrefsUiRefresh = window.require('WAWebUserPrefsUiRefresh'); if (!WAWebUserPrefsUiRefresh) { return false; } if (WAWebUserPrefsUiRefresh.getUiRefreshNuxAcked()) { return false; } WAWebUserPrefsUiRefresh.incrementNuxViewCount(); WAWebUserPrefsUiRefresh.setUiRefreshNuxAcked(true); const WAWebModalManager = window.require('WAWebModalManager'); WAWebModalManager.ModalManager.close(); return true; }); } async attachCustomEventListeners() { await exposeFunctionIfAbsent( this.pupPage, 'onNewMessageId', (messageId: string) => { this.events.emit('message.id', { id: messageId }); return; }, ); if (this.tags) { await this.attachTagsEvents(); } } async attachTagsEvents() { await this.pupPage.evaluate(() => { // @ts-ignore if (window.decodeStanzaBack) { return; } const tags = ['receipt', 'presence', 'chatstate']; // @ts-ignore window.decodeStanzaBack = window.Store.SocketWap.decodeStanza; // @ts-ignore window.Store.SocketWap.decodeStanza = async (...args) => { // @ts-ignore const result = await window.decodeStanzaBack(...args); if (tags.includes(result?.tag)) { // @ts-ignore setTimeout(() => window.onTag(result), 0); } return result; }; }); } async destroy() { this.events.removeAllListeners(); this.wpage?.removeAllListeners(); await super.destroy(); } async setPushName(name: string) { await this.pupPage.evaluate(async (pushName) => { return await window['WAHA'].WAWebSetPushnameConnAction.setPushname( pushName, ); }, name); if (this.info) { this.info.pushname = name; } } async unpair() { await this.pupPage.evaluate(async () => { if ( // @ts-ignore window.Store && // @ts-ignore window.Store.AppState && // @ts-ignore typeof window.Store.AppState.logout === 'function' ) { // @ts-ignore await window.Store.AppState.logout(); } }); } async createLabel(name: string, color: number): Promise { const labelId: number = (await this.pupPage.evaluate( async (name, color) => { // @ts-ignore return await window.WAHA.WAWebBizLabelEditingAction.labelAddAction( name, color, ); }, name, color, )) as any; return labelId; } async deleteLabel(label: Label) { return await this.pupPage.evaluate(async (label) => { // @ts-ignore return await window.WAHA.WAWebBizLabelEditingAction.labelDeleteAction( label.id, label.name, label.color, ); }, label); } async updateLabel(label: Label) { return await this.pupPage.evaluate(async (label) => { // @ts-ignore return await window.WAHA.WAWebBizLabelEditingAction.labelEditAction( label.id, label.name, undefined, // predefinedId label.color, ); }, label); } async getChats(pagination?: PaginationParams, filter?: { ids?: string[] }) { if (lodash.isEmpty(pagination)) { return await super.getChats(); } // Get paginated chats pagination.limit ||= Infinity; pagination.offset ||= 0; const chats = await this.pupPage.evaluate( async (pagination, filter) => { // @ts-ignore return await window.WAHA.getChats(pagination, filter); }, pagination, filter, ); return chats.map((chat) => ChatFactory.create(this, chat)); } async sendTextStatus(status: TextStatus) { // Convert from hex to number const waColor = 'FF' + status.backgroundColor.replace('#', ''); const color = parseInt(waColor, 16); const textStatus = { text: status.text, color: color, font: status.font, }; const sentMsg = await this.pupPage.evaluate(async (status) => { // @ts-ignore await window.Store.SendStatus.sendStatusTextMsgAction(status); // @ts-ignore const meUser = window.Store.User.getMeUser(); // @ts-ignore const myStatus = window.Store.Status.getModelsArray().findLast( (x) => x.id == meUser, ); if (!myStatus) { return undefined; } // @ts-ignore const msg = myStatus.msgs.last(); // @ts-ignore return msg ? window.WWebJS.getMessageModel(msg) : undefined; }, textStatus); return sentMsg ? new Message(this, sentMsg) : undefined; } async getMessages( chatId: string, filter: GetChatMessagesFilter, pagination: PaginationParams, ) { const messages = await this.pupPage.evaluate( async (chatId, filter, pagination) => { pagination.limit ||= Infinity; pagination.offset ||= 0; const msgFilter = (m) => { if (m.isNotification) { return false; } if ( filter['filter.fromMe'] != null && m.id.fromMe !== filter['filter.fromMe'] ) { return false; } if ( filter['filter.timestamp.gte'] != null && m.t < filter['filter.timestamp.gte'] ) { return false; } if ( filter['filter.timestamp.lte'] != null && m.t > filter['filter.timestamp.lte'] ) { return false; } if (filter['filter.ack'] != null && m.ack !== filter['filter.ack']) { return false; } return true; }; // @ts-ignore const chat = await window.WWebJS.getChat(chatId, { getAsModel: false }); let msgs = chat.msgs.getModelsArray().filter(msgFilter); while (msgs.length < pagination.limit + pagination.offset) { const loadedMessages = // @ts-ignore await window.Store.ConversationMsgs.loadEarlierMsgs(chat); if (!loadedMessages || loadedMessages.length == 0) break; msgs = [...loadedMessages.filter(msgFilter), ...msgs]; msgs = msgs.sort((a, b) => b.t - a.t); // Check if the earliest message is already outside the timerange filter const earliest = msgs[msgs.length - 1]; if (earliest.t < (filter['filter.timestamp.gte'] || Infinity)) { // Add only messages that pass the filter and stop loading more break; } } if (msgs.length > pagination.limit + pagination.offset) { // sort by t - new first msgs = msgs.sort((a, b) => b.t - a.t); msgs = msgs.slice( pagination.offset, pagination.limit + pagination.offset, ); } // @ts-ignore return msgs.map((m) => window.WWebJS.getMessageModel(m)); }, chatId, filter, pagination, ); return messages.map((m) => new Message(this, m)); } public async getAllLids( pagination: PaginationParams, ): Promise> { const lids: Array = (await this.pupPage.evaluate( async (pagination) => { pagination.limit ||= Infinity; pagination.offset ||= 0; pagination.sortBy ||= 'lid'; // @ts-ignore const WAWebApiContact = window.Store.LidUtils; await WAWebApiContact.warmUpAllLidPnMappings(); const lidMap = WAWebApiContact.lidPnCache['$1']; const values = Array.from(lidMap.values()); const result = values.map((map) => { return { // @ts-ignore lid: map.lid._serialized, // @ts-ignore pn: map.phoneNumber._serialized, }; }); // @ts-ignore const paginator = new window.Paginator(pagination); const page = paginator.apply(result); return page; }, pagination, )) as any; return lids; } public async getLidsCount(): Promise { const count: number = (await this.pupPage.evaluate(async () => { // @ts-ignore const WAWebApiContact = window.Store.LidUtils; await WAWebApiContact.warmUpAllLidPnMappings(); const lidMap = WAWebApiContact.lidPnCache['$1']; return lidMap.size; })) as any; return count; } public async findPNByLid(lid: string): Promise { const pn = await this.pupPage.evaluate(async (lid) => { // @ts-ignore const WAWebApiContact = window.Store.LidUtils; // @ts-ignore const WAWebWidFactory = window.Store.WidFactory; const wid = WAWebWidFactory.createWid(lid); const result = WAWebApiContact.getPhoneNumber(wid); return result ? result._serialized : null; }, lid); return pn; } public async findLIDByPhoneNumber(phoneNumber: string): Promise { const lid: string = (await this.pupPage.evaluate(async (pn) => { // @ts-ignore const WAWebApiContact = window.Store.LidUtils; // @ts-ignore const WAWebWidFactory = window.Store.WidFactory; const wid = WAWebWidFactory.createWid(pn); const result = WAWebApiContact.getCurrentLid(wid); return result ? result._serialized : null; }, phoneNumber)) as any; return lid; } /** * Presences methods */ public async subscribePresence(chatId: string): Promise { await this.pupPage.evaluate(async (chatId) => { const d = require; const WidFactory = d('WAWebWidFactory'); const wid = WidFactory.createWidFromWidLike(chatId); const chat = d('WAWebChatCollection').ChatCollection.get(wid); const tc = chat == null ? void 0 : chat.getTcToken(); await d('WAWebContactPresenceBridge').subscribePresence(wid, tc); }, chatId); } private async getCurrentPresence(chatId: string): Promise { const result = await this.pupPage.evaluate(async (chatId) => { const d = require; const WidFactory = d('WAWebWidFactory'); const PresenceCollection = d( 'WAWebPresenceCollection', ).PresenceCollection; const wid = WidFactory.createWidFromWidLike(chatId); const presence = PresenceCollection.get(wid); if (!presence) { return []; } let chatstates = []; if (chatId.endsWith('@c.us')) { chatstates = [presence.chatstate]; } else { chatstates = presence.chatstates.getModelsArray(); } return chatstates.map((chatstate) => { return { participant: chatstate.id._serialized, lastSeen: chatstate.t, state: chatstate.type, }; }); }, chatId); return result; } public async getPresence(chatId: string): Promise { await this.sendPresenceAvailable(); await this.subscribePresence(chatId); await sleep(3_000); return await this.getCurrentPresence(chatId); } }