wppconnect-api / src /api /layers /host.layer.ts
AUXteam's picture
Upload folder using huggingface_hub
4c34106 verified
/*
* This file is part of WPPConnect.
*
* WPPConnect is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* WPPConnect is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with WPPConnect. If not, see <https://www.gnu.org/licenses/>.
*/
import { Page } from 'puppeteer';
import { Logger } from 'winston';
import { CreateConfig, defaultOptions } from '../../config/create-config';
import {
asciiQr,
isAuthenticated,
isInsideChat,
needsToScan,
} from '../../controllers/auth';
import { initWhatsapp, injectApi } from '../../controllers/browser';
import { defaultLogger, LogLevel } from '../../utils/logger';
import { sleep } from '../../utils/sleep';
import { evaluateAndReturn, scrapeImg } from '../helpers';
import {
CatchQRCallback,
HostDevice,
LinkByCodeCallback,
LoadingScreenCallback,
StatusFindCallback,
} from '../model';
import { SocketState } from '../model/enum';
import { ScrapQrcode } from '../model/qrcode';
export class HostLayer {
readonly session: string;
readonly options: CreateConfig;
readonly logger: Logger;
protected autoCloseInterval = null;
protected autoCloseCalled = false;
protected isInitialized = false;
protected isInjected = false;
protected isStarted = false;
protected isLogged = false;
protected isInChat = false;
protected checkStartInterval?: NodeJS.Timer = null;
protected urlCode = '';
protected attempt = 0;
public catchQR?: CatchQRCallback = null;
public statusFind?: StatusFindCallback = null;
public onLoadingScreen?: LoadingScreenCallback = null;
public catchLinkCode?: LinkByCodeCallback = null;
constructor(public page: Page, session?: string, options?: CreateConfig) {
this.session = session;
this.options = { ...defaultOptions, ...options };
this.logger = this.options.logger || defaultLogger;
this.log('info', 'Initializing...');
this.initialize();
}
protected log(level: LogLevel, message: string, meta: object = {}) {
this.logger.log({
level,
message,
session: this.session,
type: 'client',
...meta,
});
}
protected async initialize() {
this.page.on('close', () => {
this.cancelAutoClose();
this.log('verbose', 'Page Closed', { type: 'page' });
});
this.page.on('load', () => {
this.log('verbose', 'Page loaded', { type: 'page' });
this.afterPageLoad();
});
this.isInitialized = true;
}
protected async afterPageLoad() {
this.log('verbose', 'Injecting wapi.js');
const options = {
deviceName: this.options.deviceName,
disableGoogleAnalytics: this.options.disableGoogleAnalytics,
googleAnalyticsId: this.options.googleAnalyticsId,
linkPreviewApiServers: this.options.linkPreviewApiServers,
poweredBy: this.options.poweredBy,
};
await evaluateAndReturn(
this.page,
(options) => {
(window as any).WPPConfig = options;
},
options
);
this.isInjected = false;
await injectApi(this.page, this.onLoadingScreen)
.then(() => {
this.isInjected = true;
this.log('verbose', 'wapi.js injected');
this.afterPageScriptInjected();
})
.catch((e) => {
console.log(e);
this.log('verbose', 'wapi.js failed');
this.log('error', e);
});
}
protected async afterPageScriptInjected() {
this.getWAVersion()
.then((version) => {
this.log('info', `WhatsApp WEB version: ${version}`);
})
.catch(() => null);
this.getWAJSVersion()
.then((version) => {
this.log('info', `WA-JS version: ${version}`);
})
.catch(() => null);
evaluateAndReturn(this.page, () => {
WPP.on('conn.auth_code_change', (window as any).checkQrCode);
}).catch(() => null);
evaluateAndReturn(this.page, () => {
WPP.on('conn.main_ready', (window as any).checkInChat);
}).catch(() => null);
this.checkInChat();
this.checkQrCode();
}
public async start() {
if (this.isStarted) {
return;
}
this.isStarted = true;
await initWhatsapp(
this.page,
null,
false,
this.options.whatsappVersion,
this.options.proxy,
this.log.bind(this)
);
await this.page.exposeFunction('checkQrCode', () => this.checkQrCode());
/*await this.page.exposeFunction('loginByCode', (phone: string) =>
this.loginByCode(phone)
);*/
await this.page.exposeFunction('checkInChat', () => this.checkInChat());
this.checkStartInterval = setInterval(() => this.checkStart(), 5000);
this.page.on('close', () => {
clearInterval(this.checkStartInterval as NodeJS.Timeout);
});
}
protected async checkStart() {
needsToScan(this.page)
.then((need) => {})
.catch(() => null);
}
protected async checkQrCode() {
const needScan = await needsToScan(this.page).catch(() => null);
this.isLogged = !needScan;
if (!needScan) {
this.attempt = 0;
return;
}
const result = await this.getQrCode();
if (!result?.urlCode || this.urlCode === result.urlCode) {
return;
}
if (typeof this.options.phoneNumber === 'string') {
return this.loginByCode(this.options.phoneNumber);
}
this.urlCode = result.urlCode;
this.attempt++;
let qr = '';
if (this.options.logQR || this.catchQR) {
qr = await asciiQr(this.urlCode);
}
if (this.options.logQR) {
this.log(
'info',
`Waiting for QRCode Scan (Attempt ${this.attempt})...:\n${qr}`,
{ code: this.urlCode }
);
} else {
this.log('verbose', `Waiting for QRCode Scan: Attempt ${this.attempt}`);
}
this.catchQR?.(result.base64Image, qr, this.attempt, result.urlCode);
}
protected async loginByCode(phone: string) {
const code = await evaluateAndReturn(
this.page,
async ({ phone }) => {
return JSON.parse(
JSON.stringify(await WPP.conn.genLinkDeviceCodeForPhoneNumber(phone))
);
},
{ phone }
);
if (this.options.logQR) {
this.log('info', `Waiting for Login By Code (Code: ${code})\n`);
} else {
this.log('verbose', `Waiting for Login By Code`);
}
this.catchLinkCode?.(code);
}
protected async checkInChat() {
const inChat = await isInsideChat(this.page).catch(() => null);
this.isInChat = !!inChat;
if (!inChat) {
return;
}
this.log('http', 'Connected');
this.statusFind?.('inChat', this.session);
}
protected tryAutoClose() {
if (this.autoCloseInterval) {
this.cancelAutoClose();
}
if (
(this.options.autoClose > 0 || this.options.deviceSyncTimeout > 0) &&
!this.autoCloseInterval &&
!this.page.isClosed()
) {
this.log('info', 'Closing the page');
this.autoCloseCalled = true;
this.statusFind && this.statusFind('autocloseCalled', this.session);
try {
this.page.close();
} catch (error) {}
}
}
protected startAutoClose(time: number | null = null) {
if (time === null || time === undefined) {
time = this.options.autoClose;
}
if (time > 0 && !this.autoCloseInterval) {
const seconds = Math.round(time / 1000);
this.log('info', `Auto close configured to ${seconds}s`);
let remain = seconds;
this.autoCloseInterval = setInterval(() => {
if (this.page.isClosed()) {
this.cancelAutoClose();
return;
}
remain -= 1;
if (remain % 10 === 0 || remain <= 5) {
this.log('http', `Auto close remain: ${remain}s`);
}
if (remain <= 0) {
this.tryAutoClose();
}
}, 1000);
}
}
protected cancelAutoClose() {
clearInterval(this.autoCloseInterval);
this.autoCloseInterval = null;
}
public async getQrCode() {
let qrResult: ScrapQrcode | undefined;
qrResult = await scrapeImg(this.page).catch(() => undefined);
return qrResult;
}
public async waitForQrCodeScan() {
if (!this.isStarted) {
throw new Error('waitForQrCodeScan error: Session not started');
}
while (!this.page.isClosed() && !this.isLogged) {
await sleep(200);
const needScan = await needsToScan(this.page).catch(() => null);
this.isLogged = !needScan;
}
}
public async waitForInChat() {
if (!this.isStarted) {
throw new Error('waitForInChat error: Session not started');
}
if (!this.isLogged) {
return false;
}
const start = Date.now();
while (!this.page.isClosed() && this.isLogged && !this.isInChat) {
if (
this.options.deviceSyncTimeout > 0 &&
Date.now() - start >= this.options.deviceSyncTimeout
) {
return false;
}
await sleep(1000);
const inChat = isInsideChat(this.page).catch(() => null);
this.isInChat = !!inChat;
}
return this.isInChat;
}
public async waitForPageLoad() {
while (!this.isInjected) {
await sleep(200);
}
await this.page.waitForFunction(() => WPP.isReady).catch(() => {});
}
public async waitForLogin() {
this.log('http', 'Waiting page load');
await this.waitForPageLoad();
this.log('http', 'Checking is logged...');
let authenticated = await isAuthenticated(this.page).catch(() => null);
this.startAutoClose();
if (authenticated === false) {
this.log(
'http',
typeof this.options.phoneNumber === 'string'
? 'Waiting for Login by Code...'
: 'Waiting for QRCode Scan...'
);
this.statusFind?.('notLogged', this.session);
await this.waitForQrCodeScan();
this.log(
'http',
typeof this.options.phoneNumber === 'string'
? 'Checking Login by Code status...'
: 'Checking QRCode status...'
);
// Wait for interface update
await sleep(200);
authenticated = await isAuthenticated(this.page).catch(() => null);
if (authenticated === null) {
this.log('warn', 'Failed to authenticate');
this.statusFind?.('qrReadError', this.session);
} else if (authenticated) {
this.log('http', 'Login with success');
this.statusFind?.('qrReadSuccess', this.session);
} else {
this.log('warn', 'Login Fail');
this.statusFind?.('qrReadFail', this.session);
this.tryAutoClose();
throw new Error('Failed to read the QRCode');
}
} else if (authenticated === true) {
this.log('http', 'Authenticated');
this.statusFind?.('isLogged', this.session);
}
if (authenticated === true) {
// Reinicia o contador do autoclose
this.cancelAutoClose();
// Wait for interface update
await sleep(200);
this.startAutoClose(this.options.deviceSyncTimeout);
this.log('http', 'Checking phone is connected...');
const inChat = await this.waitForInChat();
if (!inChat) {
this.log('warn', 'Phone not connected');
this.statusFind?.('phoneNotConnected', this.session);
this.tryAutoClose();
throw new Error('Phone not connected');
}
this.cancelAutoClose();
return true;
}
if (authenticated === false) {
this.tryAutoClose();
this.log('warn', 'Not logged');
throw new Error('Not logged');
}
this.tryAutoClose();
if (this.autoCloseCalled) {
this.log('error', 'Auto Close Called');
throw new Error('Auto Close Called');
}
if (this.page.isClosed()) {
this.log('error', 'Page Closed');
throw new Error('Page Closed');
}
this.log('error', 'Unknow error');
throw new Error('Unknow error');
}
/**
* @category Host
* @returns Current host device details
*/
public async getHostDevice(): Promise<HostDevice> {
return await evaluateAndReturn(this.page, () => WAPI.getHost());
}
/**
* @category Host
* @returns Current wid connected
*/
public async getWid(): Promise<string> {
return await evaluateAndReturn(this.page, () => WAPI.getWid());
}
/**
* Retrieves WA version
* @category Host
*/
public async getWAVersion() {
await this.page
.waitForFunction(() => WAPI.getWAVersion())
.catch(() => null);
return await evaluateAndReturn(this.page, () => WAPI.getWAVersion());
}
/**
* Retrieves WA-JS version
* @category Host
*/
public async getWAJSVersion() {
await this.page.waitForFunction(() => WPP.version).catch(() => null);
return await evaluateAndReturn(this.page, () => WPP.version);
}
/**
* Retrieves the connection state
* @category Host
*/
public async getConnectionState(): Promise<SocketState> {
return await evaluateAndReturn(this.page, () => {
return WPP.whatsapp.Socket.state as SocketState;
});
}
/**
* Retrieves if the phone is online. Please note that this may not be real time.
* @category Host
*/
public async isConnected() {
return await evaluateAndReturn(this.page, () => WAPI.isConnected());
}
/**
* Check is online
* @category Host
*/
public async isOnline(): Promise<boolean> {
return await evaluateAndReturn(this.page, () => WPP.conn.isOnline());
}
/**
* Retrieves if the phone is online. Please note that this may not be real time.
* @category Host
*/
public async isLoggedIn() {
return await evaluateAndReturn(this.page, () => WAPI.isLoggedIn());
}
/**
* Retrieves Battery Level
* @category Host
*/
public async getBatteryLevel() {
return await evaluateAndReturn(this.page, () => WAPI.getBatteryLevel());
}
/**
* Start phone Watchdog, forcing the phone connection verification.
*
* @category Host
* @param interval interval number in miliseconds
*/
public async startPhoneWatchdog(interval: number = 15000) {
return await evaluateAndReturn(
this.page,
(interval) => WAPI.startPhoneWatchdog(interval),
interval
);
}
/**
* Stop phone Watchdog, more details in {@link startPhoneWatchdog}
* @category Host
*/
public async stopPhoneWatchdog(interval: number) {
return await evaluateAndReturn(this.page, () => WAPI.stopPhoneWatchdog());
}
/**
* Check the current session is an MultiDevice session
* @category Host
*/
public async isMultiDevice() {
return await evaluateAndReturn(this.page, () => WPP.conn.isMultiDevice());
}
/**
* Retrieve main interface is authenticated, loaded and synced
* @category Host
*/
public async isMainReady() {
return await evaluateAndReturn(this.page, () => WPP.conn.isMainReady());
}
/**
* Retrieve if is authenticated
* @category Host
*/
public async isAuthenticated() {
return await evaluateAndReturn(this.page, () => WPP.conn.isAuthenticated());
}
/**
* Retrieve if main interface is authenticated and loaded, bot not synced
* @category Host
*/
public async isMainLoaded() {
return await evaluateAndReturn(this.page, () => WPP.conn.isMainLoaded());
}
/**
* Retrieve if main interface is initializing
* @category Host
*/
public async isMainInit() {
return await evaluateAndReturn(this.page, () => WPP.conn.isMainInit());
}
/**
* Join or leave of WhatsApp Web beta program.
* Will return the value seted
* @category Host
*/
public async joinWebBeta(value: boolean): Promise<boolean> {
return await evaluateAndReturn(
this.page,
(value) => WPP.conn.joinWebBeta(value),
value
);
}
/**
* Get WhatsApp build constants
* @category Host
* @returns Build constants information
*/
public async getBuildConstants() {
return await evaluateAndReturn(this.page, () =>
WPP.conn.getBuildConstants()
);
}
/**
* Check if the account has been migrated to LID
* @category Host
* @returns true if the account has been migrated to LID, false otherwise
*/
public async isLidMigrated(): Promise<boolean> {
return await evaluateAndReturn(
this.page,
() => WPP.whatsapp.functions.isLidMigrated() as boolean
);
}
}