Compare commits

...
12 Commits
Author SHA1 Message Date
devlikepro 8402ab896a [core] Fix ack for status@broadcast
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
fix #329
2024-06-01 16:16:19 +07:00
devlikepro d6dc7edf66 [core] WEBJS enable io from chrome if debug enabled 2024-05-31 18:16:34 +07:00
devlikepro 8f6dbf70f7 [core] WEBJS auto restart on FAILED
fix #355
2024-05-31 18:16:34 +07:00
devlikepro bd14d9a71e [core] 2024.5.14 2024-05-31 13:32:57 +07:00
devlikepro 61a0a38dd2 [core] Restart NOWEB every 30 minutes 2024-05-31 12:50:38 +07:00
devlikepro 102f438a99 [core] NOWEB fix for creds.json
fix #347
2024-05-31 12:18:11 +07:00
devlikepro 586a2d1c79 [core] rm import 2024-05-31 11:54:49 +07:00
devlikepro 5b04c39107 [core] move get default engine to EngineConfigService.ts 2024-05-31 11:54:49 +07:00
devlikepro d96e91ea70 [core] Better logging 2024-05-31 11:54:49 +07:00
devlikepro 68ac209aae [core] 2024.5.13
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-05-30 13:21:34 +07:00
devlikepro 90b961f900 [core] Disable QR in console
Fix #351
2024-05-30 13:21:33 +07:00
devlikepro 9945fe0fd8 [core] Remove unused vars 2024-05-30 13:21:33 +07:00
13 changed files with 299 additions and 70 deletions

No files matched your search

+1
View File
@@ -35,6 +35,7 @@
"@nestjs/terminus": "^10.2.3",
"@types/lodash": "^4.14.194",
"@types/ws": "^8.5.4",
"async-lock": "^1.4.1",
"check-disk-space": "^3.4.0",
"class-validator": "^0.12.2",
"del": "^6.0.0",
-32
View File
@@ -112,17 +112,6 @@ export class WhatsappConfigService {
return this.configService.get('WHATSAPP_SESSIONS_MONGO_URL', undefined);
}
getDefaultEngineName(): WAHAEngine {
const value = getEngineName();
if (value in WAHAEngine) {
return WAHAEngine[value];
}
console.log(
`Unknown WhatsApp default engine WHATSAPP_DEFAULT_ENGINE=${value}. Using WEBJS`,
);
return WAHAEngine.WEBJS;
}
get(name: string, defaultValue: any = undefined): any {
return this.configService.get(name, defaultValue);
}
@@ -131,27 +120,6 @@ export class WhatsappConfigService {
return this.configService.get('WHATSAPP_API_KEY', '');
}
getDashboardEnabled(): boolean {
const value = this.configService.get('WAHA_DASHBOARD_ENABLED', 'true');
return parseBool(value);
}
getDashboardUsernamePassword(): [string, string] | null {
if (!this.getDashboardEnabled()) {
return null;
}
const user = this.configService.get('WAHA_DASHBOARD_USERNAME', 'waha');
const password = this.configService.get('WAHA_DASHBOARD_PASSWORD', 'waha');
if (!user || !password) {
console.log(
'Please set up both WAHA_DASHBOARD_USERNAME and WAHA_DASHBOARD_PASSWORD ' +
'to enable swagger authentication.',
);
return null;
}
return [user, password];
}
getHealthMediaFilesThreshold(): number {
return this.configService.get<number>(
'WHATSAPP_HEALTH_MEDIA_FILES_THRESHOLD_MB',
+23
View File
@@ -51,6 +51,9 @@ import { QR } from '../QR';
import { DataStore } from './DataStore';
import { MediaManager } from './media.abc';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const qrcode = require('qrcode-terminal');
const CHROME_PATH = '/usr/bin/google-chrome-stable';
const CHROMIUM_PATH = '/usr/bin/chromium';
@@ -76,6 +79,7 @@ export enum WAHAInternalEvent {
export interface SessionParams {
name: string;
printQR: boolean;
mediaManager: MediaManager;
log: ConsoleLogger;
sessionStore: DataStore;
@@ -97,9 +101,11 @@ export abstract class WhatsappSession {
protected engineConfig?: any;
private _status: WAHASessionStatus;
private shouldPrintQR: boolean;
public constructor({
name,
printQR,
log,
sessionStore,
proxyConfig,
@@ -115,6 +121,7 @@ export abstract class WhatsappSession {
this.mediaManager = mediaManager;
this.sessionConfig = sessionConfig;
this.engineConfig = engineConfig;
this.shouldPrintQR = printQR;
}
protected set status(value: WAHASessionStatus) {
@@ -472,4 +479,20 @@ export abstract class WhatsappSession {
_serialized: messageId,
};
}
protected printQR(qr: QR) {
if (!this.shouldPrintQR) {
return;
}
if (!qr.raw) {
this.log.error(
'QR.raw is not available, can not print it in the console',
);
return;
}
this.log.log(
"You can disable QR in console by setting 'WAHA_PRINT_QR=false' in your environment variables.",
);
qrcode.generate(qr.raw, { small: true });
}
}
+2
View File
@@ -23,6 +23,7 @@ import { WhatsappConfigService } from '../config.service';
import { SessionManager } from './abc/manager.abc';
import { WAHAHealthCheckService } from './abc/WAHAHealthCheckService';
import { DashboardConfigServiceCore } from './config/DashboardConfigServiceCore';
import { EngineConfigService } from './config/EngineConfigService';
import { SwaggerConfigServiceCore } from './config/SwaggerConfigServiceCore';
import { WAHAHealthCheckServiceCore } from './health/WAHAHealthCheckServiceCore';
import { SessionManagerCore } from './manager.core';
@@ -84,6 +85,7 @@ const PROVIDERS = [
DashboardConfigServiceCore,
SwaggerConfigServiceCore,
WhatsappConfigService,
EngineConfigService,
ConsoleLogger,
];
+27
View File
@@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { parseBool } from '../../helpers';
import { WAHAEngine } from '../../structures/enums.dto';
import { getEngineName } from '../../version';
@Injectable()
export class EngineConfigService {
constructor(protected configService: ConfigService) {}
getDefaultEngineName(): WAHAEngine {
const value = getEngineName();
if (value in WAHAEngine) {
return WAHAEngine[value];
}
console.log(
`Unknown WhatsApp default engine WHATSAPP_DEFAULT_ENGINE=${value}. Using WEBJS`,
);
return WAHAEngine.WEBJS;
}
get shouldPrintQR(): boolean {
const value = this.configService.get('WAHA_PRINT_QR', true);
return parseBool(value);
}
}
@@ -1,8 +1,6 @@
import { useMultiFileAuthState } from '@adiwajshing/baileys';
import { Store } from 'whatsapp-web.js';
import { DataStore } from '../../abc/DataStore';
import { LocalStore } from '../../storage/LocalStore';
import { useMultiFileAuthState } from './useMultiFileAuthState';
export class NowebAuthFactoryCore {
buildAuth(store: DataStore, name: string) {
+53 -19
View File
@@ -5,6 +5,7 @@ import makeWASocket, {
getAggregateVotesInPollMessage,
getKeyAuthor,
isJidGroup,
isJidStatusBroadcast,
jidNormalizedUser,
makeCacheableSignalKeyStore,
makeInMemoryStore,
@@ -109,9 +110,13 @@ const PresenceStatuses = {
const ToEnginePresenceStatus = flipObject(PresenceStatuses);
export class WhatsappSessionNoWebCore extends WhatsappSession {
private START_ATTEMPT_DELAY_SECONDS = 2;
private AUTO_RESTART_AFTER_SECONDS = 30 * 60;
engine = WAHAEngine.NOWEB;
authFactory = new NowebAuthFactoryCore();
private restartTimeoutId: null | ReturnType<typeof setTimeout> = null;
private startTimeoutId: null | ReturnType<typeof setTimeout> = null;
private autoRestartTimeoutId: null | ReturnType<typeof setTimeout> = null;
get listenConnectionEventsFromTheStart() {
return true;
@@ -140,7 +145,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
/** caching makes the store faster to send/recv messages */
keys: makeCacheableSignalKeyStore(state.keys, logger),
},
printQRInTerminal: true,
printQRInTerminal: false,
browser: Browsers.ubuntu('Chrome'),
logger: logger,
mobile: false,
@@ -194,6 +199,23 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
this.listenConnectionEvents();
this.events.emit(WAHAInternalEvent.ENGINE_START);
}
this.enableAutoRestart();
}
protected enableAutoRestart() {
if (this.autoRestartTimeoutId) {
return;
}
// add random delay to avoid multiple restarts at the same time
const shiftSeconds = Math.floor(Math.random() * 30);
const delay = this.AUTO_RESTART_AFTER_SECONDS + shiftSeconds;
this.log.debug(`Auto-restart is enabled, after ${delay} seconds`);
this.autoRestartTimeoutId = setTimeout(() => {
this.autoRestartTimeoutId = null;
this.log.log('Auto-restarting the client connection...');
this.sock?.end('auto-restart');
}, delay * SECOND);
}
protected async getMessage(
@@ -213,17 +235,19 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
private restartClient() {
if (this.restartTimeoutId) {
if (this.startTimeoutId) {
this.log.log(
'Request to restart is already in progress, ignoring this request',
);
return;
}
this.log.log('Setting up client restart in 2 seconds...');
this.restartTimeoutId = setTimeout(() => {
this.restartTimeoutId = undefined;
this.log.log(
`Setting up client start in ${this.START_ATTEMPT_DELAY_SECONDS} seconds...`,
);
this.startTimeoutId = setTimeout(() => {
this.startTimeoutId = undefined;
this.buildClient();
}, 2 * SECOND);
}, this.START_ATTEMPT_DELAY_SECONDS * SECOND);
}
protected listenConnectionEvents() {
@@ -241,39 +265,46 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
const shouldReconnect =
lastDisconnect.error?.output?.statusCode !==
DisconnectReason.loggedOut;
this.log.error(
'connection closed due to ',
lastDisconnect.error,
', reconnecting ',
shouldReconnect,
);
this.qr.save('');
// reconnect if not logged out
if (shouldReconnect) {
this.log.log(
`Connection closed due to '${lastDisconnect.error}', reconnecting...`,
);
this.restartClient();
} else {
this.log.error(
`Connection closed due to '${lastDisconnect.error}', do not reconnect the session.`,
);
await this.end();
this.status = WAHASessionStatus.FAILED;
}
}
// Save QR
if (qr) {
const url = await QRCode.toDataURL(qr);
this.qr.save(url, qr);
this.printQR(this.qr);
this.status = WAHASessionStatus.SCAN_QR_CODE;
QRCode.toDataURL(qr).then((url) => {
this.qr.save(url, qr);
});
}
});
}
async stop() {
this.sock?.ev?.removeAllListeners();
this.sock?.ws?.removeAllListeners();
this.sock?.ws?.close();
await this.end();
this.status = WAHASessionStatus.STOPPED;
return;
}
private async end() {
clearTimeout(this.autoRestartTimeoutId);
clearTimeout(this.startTimeoutId);
this.sock?.ev?.removeAllListeners();
this.sock?.ws?.removeAllListeners();
this.sock?.end();
}
async getSessionMeInfo(): Promise<MeInfo | null> {
const me = this.sock.authState?.creds?.me;
if (!me) {
@@ -980,6 +1011,9 @@ function toCusFormat(remoteJid) {
if (isJidGroup(remoteJid)) {
return remoteJid;
}
if (isJidStatusBroadcast(remoteJid)) {
return remoteJid;
}
if (!remoteJid) {
return;
}
@@ -0,0 +1,115 @@
import {
AuthenticationCreds,
AuthenticationState,
proto,
} from '@adiwajshing/baileys';
import { BufferJSON, initAuthCreds } from '@adiwajshing/baileys/lib/Utils';
import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises';
import { join } from 'path';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AsyncLock = require('async-lock');
// We need to lock files due to the fact that we are using async functions to read and write files
// https://github.com/WhiskeySockets/Baileys/issues/794
// https://github.com/nodejs/node/issues/26338
// Default pending is 1000, set it to infinity
// https://github.com/rogierschouten/async-lock/issues/63
const fileLock = new AsyncLock({ maxPending: Infinity });
/**
* stores the full authentication state in a single folder.
* Far more efficient than singlefileauthstate
*
* Again, I wouldn't endorse this for any production level use other than perhaps a bot.
* Would recommend writing an auth state for use with a proper SQL or No-SQL DB
* */
export const useMultiFileAuthState = async (
folder: string,
): Promise<{
state: AuthenticationState;
saveCreds: () => Promise<void>;
}> => {
const writeData = (data: any, file: string) => {
const filePath = join(folder, fixFileName(file));
return fileLock.acquire(filePath, () =>
writeFile(join(filePath), JSON.stringify(data, BufferJSON.replacer)),
);
};
const readData = async (file: string) => {
try {
const filePath = join(folder, fixFileName(file));
const data = await fileLock.acquire(filePath, () =>
readFile(filePath, { encoding: 'utf-8' }),
);
return JSON.parse(data, BufferJSON.reviver);
} catch (error) {
return null;
}
};
const removeData = async (file: string) => {
try {
const filePath = join(folder, fixFileName(file));
await fileLock.acquire(filePath, () => unlink(filePath));
} catch {}
};
const folderInfo = await stat(folder).catch(() => {
return null;
});
if (folderInfo) {
if (!folderInfo.isDirectory()) {
throw new Error(
`found something that is not a directory at ${folder}, either delete it or specify a different location`,
);
}
} else {
await mkdir(folder, { recursive: true });
}
const fixFileName = (file?: string) =>
file?.replace(/\//g, '__')?.replace(/:/g, '-') || '';
const creds: AuthenticationCreds =
(await readData('creds.json')) || initAuthCreds();
return {
state: {
creds,
keys: {
get: async (type, ids) => {
const data = {};
await Promise.all(
ids.map(async (id) => {
let value = await readData(`${type}-${id}.json`);
if (type === 'app-state-sync-key' && value) {
value = proto.Message.AppStateSyncKeyData.fromObject(value);
}
data[id] = value;
}),
);
return data;
},
set: async (data) => {
const tasks: Promise<void>[] = [];
for (const category in data) {
for (const id in data[category]) {
const value = data[category][id];
const file = `${category}-${id}.json`;
tasks.push(value ? writeData(value, file) : removeData(file));
}
}
await Promise.all(tasks);
},
},
},
saveCreds: () => {
return writeData(creds, 'creds.json');
},
};
};
+1 -3
View File
@@ -67,11 +67,9 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
protected getCatchQR() {
return (base64Qrimg, asciiQR, attempts, urlCode) => {
this.qr.save(base64Qrimg, urlCode);
this.printQR(this.qr);
this.status = WAHASessionStatus.SCAN_QR_CODE;
this.log.debug('Number of attempts to read the qrcode: ', attempts);
this.log.log('Terminal qrcode:');
// Log QR image in console without this.log to make it pretty
console.log(asciiQR);
};
}
+60 -9
View File
@@ -31,6 +31,7 @@ import {
import { ContactQuery, ContactRequest } from '../../../structures/contacts.dto';
import {
ACK_UNKNOWN,
SECOND,
WAHAEngine,
WAHAEvents,
WAHAPresenceStatus,
@@ -67,15 +68,21 @@ export interface WebJSConfig {
}
export class WhatsappSessionWebJSCore extends WhatsappSession {
private START_ATTEMPT_DELAY_SECONDS = 2;
engine = WAHAEngine.WEBJS;
protected engineConfig?: WebJSConfig;
private startTimeoutId: null | ReturnType<typeof setTimeout> = null;
private shouldRestart: boolean;
whatsapp: Client;
protected qr: QR;
public constructor(config) {
super(config);
this.qr = new QR();
this.shouldRestart = true;
}
/**
@@ -94,6 +101,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
headless: true,
executablePath: this.getBrowserExecutablePath(),
args: this.getBrowserArgsForPuppeteer(),
dumpio: this.isDebugEnabled(),
},
webVersion: webVersion,
webVersionCache: {
@@ -110,6 +118,27 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return new Client(clientOptions);
}
private restartClient() {
if (!this.shouldRestart) {
this.log.debug("Shouldn't restart the client, ignoring restart request");
return;
}
if (this.startTimeoutId) {
const msg =
'Request to restart is already in progress, ignoring restart request';
this.log.warn(msg);
return;
}
this.log.log(
`Setting up client start in ${this.START_ATTEMPT_DELAY_SECONDS} seconds...`,
);
this.startTimeoutId = setTimeout(() => {
this.startTimeoutId = undefined;
this.start();
}, this.START_ATTEMPT_DELAY_SECONDS * SECOND);
}
protected addProxyConfig(clientOptions: ClientOptions) {
if (this.proxyConfig?.server !== undefined) {
// push the proxy server to the args
@@ -127,8 +156,9 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
}
async start() {
this.status = WAHASessionStatus.STARTING;
protected async init() {
await this.end();
this.shouldRestart = true;
this.whatsapp = await this.buildClient();
this.whatsapp
.initialize()
@@ -137,12 +167,16 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.whatsapp.pupBrowser.on('disconnected', () => {
this.status = WAHASessionStatus.FAILED;
this.log.error('The browser has been disconnected');
this.restartClient();
});
// Listen for page close event
this.whatsapp.pupPage.on('close', () => {
this.status = WAHASessionStatus.FAILED;
this.log.error('The WhatsApp Web page has been closed');
this.restartClient();
});
// Listen for page error event
if (this.isDebugEnabled()) {
this.log.debug("Logging 'console' event for web page");
this.whatsapp.pupPage.on('console', (msg) =>
@@ -156,6 +190,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
.catch((error) => {
this.status = WAHASessionStatus.FAILED;
this.log.error(error);
this.restartClient();
return;
});
if (this.isDebugEnabled()) {
@@ -163,12 +198,30 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
this.listenConnectionEvents();
this.events.emit(WAHAInternalEvent.ENGINE_START);
}
async start() {
this.status = WAHASessionStatus.STARTING;
await this.init();
return this;
}
async stop() {
await this.whatsapp.destroy();
this.shouldRestart = false;
this.status = WAHASessionStatus.STOPPED;
await this.end();
}
private async end() {
try {
this.whatsapp?.removeAllListeners();
clearInterval(this.startTimeoutId);
this.whatsapp?.destroy().catch((error) => {
this.log.debug('Failed to destroy the client', error);
});
} catch (error) {
this.log.error(error);
}
}
async getSessionMeInfo(): Promise<MeInfo | null> {
@@ -197,14 +250,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
protected listenConnectionEvents() {
this.whatsapp.on(Events.QR_RECEIVED, (qr) => {
this.whatsapp.on(Events.QR_RECEIVED, async (qr) => {
this.log.debug('QR received');
// Convert to image and save
QRCode.toDataURL(qr).then((url) => {
this.qr.save(url, qr);
});
// Print in terminal
qrcode.generate(qr, { small: true });
const url = await QRCode.toDataURL(qr);
this.qr.save(url, qr);
this.printQR(this.qr);
this.status = WAHASessionStatus.SCAN_QR_CODE;
});
+4 -1
View File
@@ -20,6 +20,7 @@ import {
import { WebhookConfig } from '../structures/webhooks.config.dto';
import { SessionManager } from './abc/manager.abc';
import { SessionParams, WhatsappSession } from './abc/session.abc';
import { EngineConfigService } from './config/EngineConfigService';
import { WhatsappSessionNoWebCore } from './engines/noweb/session.noweb.core';
import { WhatsappSessionVenomCore } from './engines/venom/session.venom.core';
import { WhatsappSessionWebJSCore } from './engines/webjs/session.webjs.core';
@@ -55,12 +56,13 @@ export class SessionManagerCore extends SessionManager {
constructor(
private config: WhatsappConfigService,
private log: ConsoleLogger,
private engineConfigService: EngineConfigService,
) {
super();
this.log.setContext('SessionManager');
this.session = undefined;
const engineName = this.config.getDefaultEngineName();
const engineName = this.engineConfigService.getDefaultEngineName();
this.EngineClass = this.getEngine(engineName);
this.store = new LocalStoreCore(engineName.toLowerCase());
this.sessionAuthRepository = new LocalSessionAuthRepository(this.store);
@@ -126,6 +128,7 @@ export class SessionManagerCore extends SessionManager {
name,
mediaManager,
log,
printQR: this.engineConfigService.shouldPrintQR,
sessionStore: this.store,
proxyConfig: proxyConfig,
sessionConfig: request.config,
+1 -1
View File
@@ -33,7 +33,7 @@ export function getEngineName(): string {
}
export const VERSION: WAHAEnvironment = {
version: '2024.5.12',
version: '2024.5.14',
engine: getEngineName(),
tier: getWAHAVersion(),
browser: getBrowserExecutablePath(),
+11 -2
View File
@@ -14,10 +14,11 @@ __metadata:
"@adiwajshing/baileys@github:WhiskeySockets/Baileys":
version: 6.7.2
resolution: "@adiwajshing/baileys@https://github.com/WhiskeySockets/Baileys.git#commit=79c33e53b4124735f019c48e8075c9cde807b7e7"
resolution: "@adiwajshing/baileys@https://github.com/WhiskeySockets/Baileys.git#commit=9065ab690fde0641e3eac458c1521e1d151c5050"
dependencies:
"@adiwajshing/keyed-db": ^0.2.4
"@hapi/boom": ^9.1.3
async-lock: ^1.4.1
audio-decode: ^2.1.3
axios: ^1.3.3
cache-manager: 4.0.1
@@ -44,7 +45,7 @@ __metadata:
optional: true
sharp:
optional: true
checksum: dacfec2d2c4767370f2ee9a846e119cedf22d792ae920c5682e66aa67e788ec409d5ee364e6879c7c9e76d00acfb2c26c197d377893aaccc2100f3ae22940d72
checksum: 255c9aa3107a56055b33adbfba5cce41bad16d1b2453a19f3896438a85e191786c144ea73ebdb896f58c7436044aca8e740d5d9e7271c8df12f9e89318abf90f
languageName: node
linkType: hard
@@ -3169,6 +3170,13 @@ __metadata:
languageName: node
linkType: hard
"async-lock@npm:^1.4.1":
version: 1.4.1
resolution: "async-lock@npm:1.4.1"
checksum: 29e70cd892932b7c202437786cedc39ff62123cb6941014739bd3cabd6106326416e9e7c21285a5d1dc042cad239a0f7ec9c44658491ee4a615fd36a21c1d10a
languageName: node
linkType: hard
"async@npm:3.2.3":
version: 3.2.3
resolution: "async@npm:3.2.3"
@@ -12544,6 +12552,7 @@ __metadata:
"@types/ws": ^8.5.4
"@typescript-eslint/eslint-plugin": 3.9.1
"@typescript-eslint/parser": 3.9.1
async-lock: ^1.4.1
check-disk-space: ^3.4.0
class-validator: ^0.12.2
del: ^6.0.0