Farewell, Webpack! Re-injection, Pairing Code login, and 2.24x jumbofix (#2816)

* fix(webpack-exodus): Added the new patch

* fix(webpack-exodus): Fixed v2.3000.x and v2.24

* chore(webpack-exodus): eslint

* fix(webpack-exodus): Fixed Store

* fix(webpack-exodus): slight store fix

* chore(webpack-exodus): fix example so that I don't crash on every load

* chore: eslint fix

* fix(webpack-exodus): prevent confusion with build systems

* fix(webpack-exodus): Add proper groups module

* fix(webpack-exodus): Fix order queries

* fix(webpack-exodus): Bad export name

* feat(webpack-exodus): Re-injection, No more selectors, Pairing Code auth

* fix(webpack-exodus): Eslint and better login/logout handling

* fix(webpack-exodus): 2.24 compatibility

* revert testing options

* chore(webpack-exodus): ESLint

* chore(webpack-exodus): final eslint tweak (did not commit with last commit for some reason)

* style: fix broken link in readme file

* fix(webpack-exodus): Fix forwarding messages

* fix(webpack-exodus): finish forwarding function

* Making the ESLint god happy

* Fix window.Store.ProfilePic.profilePicFind is not a function error (#3001)

* Change window.Store.ProfilePic.profilePicFind to window.Store.ProfilePic.requestProfilePicFromServer

* Prevent breaking v2.2x

* refactor: make it more readable

---------

Co-authored-by: alechkos <93551621+alechkos@users.noreply.github.com>

* Sanitize, improves and fixes

* make ESList god happy

* fix getInviteCode for group (#3007) (#3029)

* fix getInviteCode for group (#3007)

* move changes to proper getInviteCode method.

* style fix

* refactor: add try-catch in a case the user is not a group admin

* refactor: handle promise properly

---------

Co-authored-by: Sven Neumann <s.neumann@websix.de>
Co-authored-by: alechkos <93551621+alechkos@users.noreply.github.com>

* fixes

* Update LocalWebCache.js

* fix delete on 2.3000 (#3048)

* fix delete on 2.3000

* fix compareWwebVersions

* fix: use proper comparison operator

---------

Co-authored-by: alechkos <93551621+alechkos@users.noreply.github.com>

* Implement bot invoking capabilities (#3009)

* Implement bot invoking capabilities

* Update index.d.ts

* Eslint

* Webpack exodus fix message_reaction event (#3099)

* exposed WAWebAddonReactionTableMode

* capture reaction using WAWebAddonReactionTableMode

* move compareWwebVersions and check version before trying to expose new store

* message_reaction on example.js (#3102)

* handle message_reaction event on example.js

* log wweb version on ready

* fix reactions on pvt chats (#3104)

* New group function

* new group settings

* Update Client.js

---------

Co-authored-by: alechkos <93551621+alechkos@users.noreply.github.com>
Co-authored-by: tuyuribr <45042245+tuyuribr@users.noreply.github.com>
Co-authored-by: Seow Zhen Jun <56989187+seowzhenjun0126@users.noreply.github.com>
Co-authored-by: Sven <sven2neumann@gmail.com>
Co-authored-by: Sven Neumann <s.neumann@websix.de>
Co-authored-by: Jorge Rocha Gualtieri <jorge@jrg.com.br>
Co-authored-by: Mateus Mercer <accounts@matbm.net>
This commit is contained in:
Rajeh Taher 2024-07-24 04:19:22 +03:00 • committed by GitHub
commit 13ce571c64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 986 additions and 777 deletions

View file

@ -5,19 +5,30 @@ const client = new Client({
// proxyAuthentication: { username: 'username', password: 'password' },
puppeteer: {
// args: ['--proxy-server=proxy-server-that-requires-authentication.example.com'],
headless: false
headless: false,
}
});
// client initialize does not finish at ready now.
client.initialize();
client.on('loading_screen', (percent, message) => {
console.log('LOADING SCREEN', percent, message);
});
client.on('qr', (qr) => {
// Pairing code only needs to be requested once
let pairingCodeRequested = false;
client.on('qr', async (qr) => {
// NOTE: This event will not be fired if a session is specified.
console.log('QR RECEIVED', qr);
// paiuting code example
const pairingCodeEnabled = false;
if (pairingCodeEnabled && !pairingCodeRequested) {
const pairingCode = await client.requestPairingCode('96170100100'); // enter the target phone number
console.log('Pairing code enabled, code: '+ pairingCode);
pairingCodeRequested = true;
}
});
client.on('authenticated', () => {
@ -29,8 +40,18 @@ client.on('auth_failure', msg => {
console.error('AUTHENTICATION FAILURE', msg);
});
client.on('ready', () => {
client.on('ready', async () => {
console.log('READY');
const debugWWebVersion = await client.getWWebVersion();
console.log(`WWebVersion = ${debugWWebVersion}`);
client.pupPage.on('pageerror', function(err) {
console.log('Page error: ' + err.toString());
});
client.pupPage.on('error', function(err) {
console.log('Page error: ' + err.toString());
});
});
client.on('message', async msg => {
@ -418,7 +439,7 @@ client.on('message', async msg => {
requesterIds: ['number1@c.us', 'number2@c.us'],
sleep: null
});
} else {
} else if (msg.body === '!pinmsg') {
/**
* Pins a message in a chat, a method takes a number in seconds for the message to be pinned.
* WhatsApp default values for duration to pass to the method are:
@ -595,6 +616,10 @@ client.on('group_membership_request', async (notification) => {
await client.rejectGroupMembershipRequests(notification.chatId, notification.author);
});
client.on('message_reaction', async (reaction) => {
console.log('REACTION RECEIVED', reaction);
});
client.on('vote_update', (vote) => {
/** The vote that was affected: */
console.log(vote);

16
index.d.ts vendored
View file

@ -2,6 +2,7 @@
import { EventEmitter } from 'events'
import { RequestInit } from 'node-fetch'
import * as puppeteer from 'puppeteer'
import InterfaceController from './src/util/InterfaceController'
declare namespace WAWebJS {
@ -17,6 +18,9 @@ declare namespace WAWebJS {
/** Puppeteer browser running WhatsApp Web */
pupBrowser?: puppeteer.Browser
/** Client interactivity interface */
interface?: InterfaceController
/**Accepts an invitation to join a group */
acceptInvite(inviteCode: string): Promise<string>
@ -111,6 +115,14 @@ declare namespace WAWebJS {
*/
muteChat(chatId: string, unmuteDate?: Date): Promise<void>
/**
* Request authentication via pairing code instead of QR code
* @param phoneNumber - Phone number in international, symbol-free format (e.g. 12025550108 for US, 551155501234 for Brazil)
* @param showNotification - Show notification to pair on phone number
* @returns {Promise<string>} - Returns a pairing code in format "ABCDEFGH"
*/
requestPairingCode(phoneNumber: string, showNotification = true): Promise<string>
/** Force reset of connection state for the client */
resetState(): Promise<void>
@ -215,7 +227,7 @@ declare namespace WAWebJS {
/** Emitted when the client has been disconnected */
on(event: 'disconnected', listener: (
/** reason that caused the disconnect */
reason: WAState | "NAVIGATION"
reason: WAState | "LOGOUT"
) => void): this
/** Emitted when a user joins the chat via invite link or is added by an admin */
@ -1097,6 +1109,8 @@ declare namespace WAWebJS {
}[]
/** Send 'seen' status */
sendSeen?: boolean
/** Bot Wid when doing a bot mention like @Meta AI */
invokedBotWid?: string
/** Media to be sent */
media?: MessageMedia
/** Extra options */

View file

@ -27,7 +27,6 @@ module.exports = {
NoAuth: require('./src/authStrategies/NoAuth'),
LocalAuth: require('./src/authStrategies/LocalAuth'),
RemoteAuth: require('./src/authStrategies/RemoteAuth'),
LegacySessionAuth: require('./src/authStrategies/LegacySessionAuth'),
...Constants
};

View file

@ -7,12 +7,15 @@ const moduleRaid = require('@pedroslopez/moduleraid/moduleraid');
const Util = require('./util/Util');
const InterfaceController = require('./util/InterfaceController');
const { WhatsWebURL, DefaultOptions, Events, WAState } = require('./util/Constants');
const { ExposeStore, LoadUtils } = require('./util/Injected');
const { ExposeAuthStore } = require('./util/Injected/AuthStore/AuthStore');
const { ExposeStore } = require('./util/Injected/Store');
const { ExposeLegacyAuthStore } = require('./util/Injected/AuthStore/LegacyAuthStore');
const { ExposeLegacyStore } = require('./util/Injected/LegacyStore');
const { LoadUtils } = require('./util/Injected/Utils');
const ChatFactory = require('./factories/ChatFactory');
const ContactFactory = require('./factories/ContactFactory');
const WebCacheFactory = require('./webCache/WebCacheFactory');
const { ClientInfo, Message, MessageMedia, Contact, Location, Poll, PollVote, GroupNotification, Label, Call, Buttons, List, Reaction } = require('./structures');
const LegacySessionAuth = require('./authStrategies/LegacySessionAuth');
const NoAuth = require('./authStrategies/NoAuth');
/**
@ -63,37 +66,227 @@ class Client extends EventEmitter {
this.options = Util.mergeDefault(DefaultOptions, options);
if(!this.options.authStrategy) {
if(Object.prototype.hasOwnProperty.call(this.options, 'session')) {
process.emitWarning(
'options.session is deprecated and will be removed in a future release due to incompatibility with multi-device. ' +
'Use the LocalAuth authStrategy, don\'t pass in a session as an option, or suppress this warning by using the LegacySessionAuth strategy explicitly (see https://wwebjs.dev/guide/authentication.html#legacysessionauth-strategy).',
'DeprecationWarning'
);
this.authStrategy = new LegacySessionAuth({
session: this.options.session,
restartOnAuthFail: this.options.restartOnAuthFail
});
} else {
this.authStrategy = new NoAuth();
}
} else {
this.authStrategy = this.options.authStrategy;
}
this.authStrategy.setup(this);
/**
* @type {puppeteer.Browser}
*/
this.pupBrowser = null;
/**
* @type {puppeteer.Page}
*/
this.pupPage = null;
this.currentIndexHtml = null;
this.lastLoggedOut = false;
Util.setFfmpegPath(this.options.ffmpegPath);
}
/**
* Injection logic
* Private function
* @property {boolean} reinject is this a reinject?
*/
async inject(reinject = false) {
await this.pupPage.waitForFunction('window.Debug?.VERSION != undefined', {timeout: this.options.authTimeoutMs});
const version = await this.getWWebVersion();
const isCometOrAbove = parseInt(version.split('.')?.[1]) >= 3000;
if (isCometOrAbove) {
await this.pupPage.evaluate(ExposeAuthStore);
} else {
await this.pupPage.evaluate(ExposeLegacyAuthStore, moduleRaid.toString());
}
const needAuthentication = await this.pupPage.evaluate(async () => {
let state = window.AuthStore.AppState.state;
if (state === 'OPENING' || state === 'UNLAUNCHED' || state === 'PAIRING') {
// wait till state changes
await new Promise(r => {
window.AuthStore.AppState.on('change:state', function waitTillInit(_AppState, state) {
if (state !== 'OPENING' && state !== 'UNLAUNCHED' && state !== 'PAIRING') {
window.AuthStore.AppState.off('change:state', waitTillInit);
r();
}
});
});
}
state = window.AuthStore.AppState.state;
return state == 'UNPAIRED' || state == 'UNPAIRED_IDLE';
});
if (needAuthentication) {
const { failed, failureEventPayload, restart } = await this.authStrategy.onAuthenticationNeeded();
if(failed) {
/**
* Emitted when there has been an error while trying to restore an existing session
* @event Client#auth_failure
* @param {string} message
*/
this.emit(Events.AUTHENTICATION_FAILURE, failureEventPayload);
await this.destroy();
if (restart) {
// session restore failed so try again but without session to force new authentication
return this.initialize();
}
return;
}
// Register qr events
let qrRetries = 0;
const injected = await this.pupPage.evaluate(() => {
return typeof window.onQRChangedEvent !== 'undefined';
});
if (!injected) {
await this.pupPage.exposeFunction('onQRChangedEvent', async (qr) => {
/**
* Emitted when a QR code is received
* @event Client#qr
* @param {string} qr QR Code
*/
this.emit(Events.QR_RECEIVED, qr);
if (this.options.qrMaxRetries > 0) {
qrRetries++;
if (qrRetries > this.options.qrMaxRetries) {
this.emit(Events.DISCONNECTED, 'Max qrcode retries reached');
await this.destroy();
}
}
});
}
await this.pupPage.evaluate(async () => {
const registrationInfo = await window.AuthStore.RegistrationUtils.waSignalStore.getRegistrationInfo();
const noiseKeyPair = await window.AuthStore.RegistrationUtils.waNoiseInfo.get();
const staticKeyB64 = window.AuthStore.Base64Tools.encodeB64(noiseKeyPair.staticKeyPair.pubKey);
const identityKeyB64 = window.AuthStore.Base64Tools.encodeB64(registrationInfo.identityKeyPair.pubKey);
const advSecretKey = await window.AuthStore.RegistrationUtils.getADVSecretKey();
const platform = window.AuthStore.RegistrationUtils.DEVICE_PLATFORM;
const getQR = (ref) => ref + ',' + staticKeyB64 + ',' + identityKeyB64 + ',' + advSecretKey + ',' + platform;
window.onQRChangedEvent(getQR(window.AuthStore.Conn.ref)); // initial qr
window.AuthStore.Conn.on('change:ref', (_, ref) => { window.onQRChangedEvent(getQR(ref)); }); // future QR changes
});
}
if (!reinject) {
await this.pupPage.exposeFunction('onAuthAppStateChangedEvent', async (state) => {
if (state == 'UNPAIRED_IDLE') {
// refresh qr code
window.Store.Cmd.refreshQR();
}
});
await this.pupPage.exposeFunction('onAppStateHasSyncedEvent', async () => {
const authEventPayload = await this.authStrategy.getAuthEventPayload();
/**
* Emitted when authentication is successful
* @event Client#authenticated
*/
this.emit(Events.AUTHENTICATED, authEventPayload);
const injected = await this.pupPage.evaluate(async () => {
return typeof window.Store !== 'undefined' && typeof window.WWebJS !== 'undefined';
});
if (!injected) {
if (this.options.webVersionCache.type === 'local' && this.currentIndexHtml) {
const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache;
const webCache = WebCacheFactory.createWebCache(webCacheType, webCacheOptions);
await webCache.persist(this.currentIndexHtml, version);
}
if (isCometOrAbove) {
await this.pupPage.evaluate(ExposeStore);
} else {
// make sure all modules are ready before injection
// 2 second delay after authentication makes sense and does not need to be made dyanmic or removed
await new Promise(r => setTimeout(r, 2000));
await this.pupPage.evaluate(ExposeLegacyStore);
}
// Check window.Store Injection
await this.pupPage.waitForFunction('window.Store != undefined');
/**
* Current connection information
* @type {ClientInfo}
*/
this.info = new ClientInfo(this, await this.pupPage.evaluate(() => {
return { ...window.Store.Conn.serialize(), wid: window.Store.User.getMeUser() };
}));
this.interface = new InterfaceController(this);
//Load util functions (serializers, helper functions)
await this.pupPage.evaluate(LoadUtils);
await this.attachEventListeners(reinject);
reinject = true;
}
/**
* Emitted when the client has initialized and is ready to receive messages.
* @event Client#ready
*/
this.emit(Events.READY);
this.authStrategy.afterAuthReady();
});
let lastPercent = null;
await this.pupPage.exposeFunction('onOfflineProgressUpdateEvent', async (percent) => {
if (lastPercent !== percent) {
lastPercent = percent;
this.emit(Events.LOADING_SCREEN, percent, 'WhatsApp'); // Message is hardcoded as "WhatsApp" for now
}
});
}
const logoutCatchInjected = await this.pupPage.evaluate(() => {
return typeof window.onLogoutEvent !== 'undefined';
});
if (!logoutCatchInjected) {
await this.pupPage.exposeFunction('onLogoutEvent', async () => {
this.lastLoggedOut = true;
await this.pupPage.waitForNavigation({waitUntil: 'load', timeout: 5000}).catch((_) => _);
});
}
await this.pupPage.evaluate(() => {
window.AuthStore.AppState.on('change:state', (_AppState, state) => { window.onAuthAppStateChangedEvent(state); });
window.AuthStore.AppState.on('change:hasSynced', () => { window.onAppStateHasSyncedEvent(); });
window.AuthStore.Cmd.on('offline_progress_update', () => {
window.onOfflineProgressUpdateEvent(window.AuthStore.OfflineMessageHandler.getOfflineDeliveryProgress());
});
window.AuthStore.Cmd.on('logout', async () => {
await window.onLogoutEvent();
});
});
}
/**
* Sets up events and requirements, kicks off authentication request
*/
async initialize() {
let [browser, page] = [null, null];
let
/**
* @type {puppeteer.Browser}
*/
browser,
/**
* @type {puppeteer.Page}
*/
page;
browser = null;
page = null;
await this.authStrategy.beforeBrowserInitialized();
@ -126,7 +319,8 @@ class Client extends EventEmitter {
await this.authStrategy.afterBrowserInitialized();
await this.initWebVersionCache();
// ocVesion (isOfficialClient patch)
// ocVersion (isOfficialClient patch)
// remove after 2.3000.x hard release
await page.evaluateOnNewDocument(() => {
const originalError = Error;
//eslint-disable-next-line no-global-assign
@ -144,236 +338,42 @@ class Client extends EventEmitter {
referer: 'https://whatsapp.com/'
});
await page.evaluate(`function getElementByXpath(path) {
return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}`);
await this.inject();
let lastPercent = null,
lastPercentMessage = null;
await page.exposeFunction('loadingScreen', async (percent, message) => {
if (lastPercent !== percent || lastPercentMessage !== message) {
this.emit(Events.LOADING_SCREEN, percent, message);
lastPercent = percent;
lastPercentMessage = message;
this.pupPage.on('framenavigated', async (frame) => {
if(frame.url().includes('post_logout=1') || this.lastLoggedOut) {
this.emit(Events.DISCONNECTED, 'LOGOUT');
await this.authStrategy.logout();
await this.authStrategy.beforeBrowserInitialized();
await this.authStrategy.afterBrowserInitialized();
this.lastLoggedOut = false;
}
await this.inject(true);
});
await page.evaluate(
async function (selectors) {
var observer = new MutationObserver(function () {
let progressBar = window.getElementByXpath(
selectors.PROGRESS
);
let progressMessage = window.getElementByXpath(
selectors.PROGRESS_MESSAGE
);
if (progressBar) {
window.loadingScreen(
progressBar.value,
progressMessage.innerText
);
}
});
observer.observe(document, {
attributes: true,
childList: true,
characterData: true,
subtree: true,
});
},
{
PROGRESS: '//*[@id=\'app\']/div/div/div[2]/progress',
PROGRESS_MESSAGE: '//*[@id=\'app\']/div/div/div[3]',
}
);
const INTRO_IMG_SELECTOR = '[data-icon=\'search\']';
const INTRO_QRCODE_SELECTOR = 'div[data-ref] canvas';
// Checks which selector appears first
const needAuthentication = await Promise.race([
new Promise(resolve => {
page.waitForSelector(INTRO_IMG_SELECTOR, { timeout: this.options.authTimeoutMs })
.then(() => resolve(false))
.catch((err) => resolve(err));
}),
new Promise(resolve => {
page.waitForSelector(INTRO_QRCODE_SELECTOR, { timeout: this.options.authTimeoutMs })
.then(() => resolve(true))
.catch((err) => resolve(err));
})
]);
// Checks if an error occurred on the first found selector. The second will be discarded and ignored by .race;
if (needAuthentication instanceof Error) throw needAuthentication;
// Scan-qrcode selector was found. Needs authentication
if (needAuthentication) {
const { failed, failureEventPayload, restart } = await this.authStrategy.onAuthenticationNeeded();
if(failed) {
/**
* Emitted when there has been an error while trying to restore an existing session
* @event Client#auth_failure
* @param {string} message
*/
this.emit(Events.AUTHENTICATION_FAILURE, failureEventPayload);
await this.destroy();
if (restart) {
// session restore failed so try again but without session to force new authentication
return this.initialize();
}
return;
}
const QR_CONTAINER = 'div[data-ref]';
const QR_RETRY_BUTTON = 'div[data-ref] > span > button';
let qrRetries = 0;
await page.exposeFunction('qrChanged', async (qr) => {
/**
* Emitted when a QR code is received
* @event Client#qr
* @param {string} qr QR Code
*/
this.emit(Events.QR_RECEIVED, qr);
if (this.options.qrMaxRetries > 0) {
qrRetries++;
if (qrRetries > this.options.qrMaxRetries) {
this.emit(Events.DISCONNECTED, 'Max qrcode retries reached');
await this.destroy();
}
}
});
await page.evaluate(function (selectors) {
const qr_container = document.querySelector(selectors.QR_CONTAINER);
window.qrChanged(qr_container.dataset.ref);
const obs = new MutationObserver((muts) => {
muts.forEach(mut => {
// Listens to qr token change
if (mut.type === 'attributes' && mut.attributeName === 'data-ref') {
window.qrChanged(mut.target.dataset.ref);
}
// Listens to retry button, when found, click it
else if (mut.type === 'childList') {
const retry_button = document.querySelector(selectors.QR_RETRY_BUTTON);
if (retry_button) retry_button.click();
}
});
});
obs.observe(qr_container.parentElement, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ['data-ref'],
});
}, {
QR_CONTAINER,
QR_RETRY_BUTTON
});
// Wait for code scan
try {
await page.waitForSelector(INTRO_IMG_SELECTOR, { timeout: 0 });
} catch(error) {
if (
error.name === 'ProtocolError' &&
error.message &&
error.message.match(/Target closed/)
) {
// something has called .destroy() while waiting
return;
}
throw error;
}
}
await page.evaluate(() => {
/**
* Helper function that compares between two WWeb versions. Its purpose is to help the developer to choose the correct code implementation depending on the comparison value and the WWeb version.
* @param {string} lOperand The left operand for the WWeb version string to compare with
* @param {string} operator The comparison operator
* @param {string} rOperand The right operand for the WWeb version string to compare with
* @returns {boolean} Boolean value that indicates the result of the comparison
*/
window.compareWwebVersions = (lOperand, operator, rOperand) => {
if (!['>', '>=', '<', '<=', '='].includes(operator)) {
throw new class _ extends Error {
constructor(m) { super(m); this.name = 'CompareWwebVersionsError'; }
}('Invalid comparison operator is provided');
}
if (typeof lOperand !== 'string' || typeof rOperand !== 'string') {
throw new class _ extends Error {
constructor(m) { super(m); this.name = 'CompareWwebVersionsError'; }
}('A non-string WWeb version type is provided');
}
lOperand = lOperand.replace(/-beta$/, '');
rOperand = rOperand.replace(/-beta$/, '');
while (lOperand.length !== rOperand.length) {
lOperand.length > rOperand.length
? rOperand = rOperand.concat('0')
: lOperand = lOperand.concat('0');
}
lOperand = Number(lOperand.replace(/\./g, ''));
rOperand = Number(rOperand.replace(/\./g, ''));
return (
operator === '>' ? lOperand > rOperand :
operator === '>=' ? lOperand >= rOperand :
operator === '<' ? lOperand < rOperand :
operator === '<=' ? lOperand <= rOperand :
operator === '=' ? lOperand === rOperand :
false
);
};
});
await page.evaluate(ExposeStore, moduleRaid.toString());
const authEventPayload = await this.authStrategy.getAuthEventPayload();
/**
* Emitted when authentication is successful
* @event Client#authenticated
* Request authentication via pairing code instead of QR code
* @param {string} phoneNumber - Phone number in international, symbol-free format (e.g. 12025550108 for US, 551155501234 for Brazil)
* @param {boolean} showNotification - Show notification to pair on phone number
* @returns {Promise<string>} - Returns a pairing code in format "ABCDEFGH"
*/
this.emit(Events.AUTHENTICATED, authEventPayload);
// Check window.Store Injection
await page.waitForFunction('window.Store != undefined');
await page.evaluate(async () => {
// safely unregister service workers
const registrations = await navigator.serviceWorker.getRegistrations();
for (let registration of registrations) {
registration.unregister();
async requestPairingCode(phoneNumber, showNotification = true) {
return await this.pupPage.evaluate(async (phoneNumber, showNotification) => {
window.AuthStore.PairingCodeLinkUtils.setPairingType('ALT_DEVICE_LINKING');
await window.AuthStore.PairingCodeLinkUtils.initializeAltDeviceLinking();
return window.AuthStore.PairingCodeLinkUtils.startAltLinkingFlow(phoneNumber, showNotification);
}, phoneNumber, showNotification);
}
});
//Load util functions (serializers, helper functions)
await page.evaluate(LoadUtils);
// Expose client info
/**
* Current connection information
* @type {ClientInfo}
* Attach event listeners to WA Web
* Private function
* @property {boolean} reinject is this a reinject?
*/
this.info = new ClientInfo(this, await page.evaluate(() => {
return { ...window.Store.Conn.serialize(), wid: window.Store.User.getMeUser() };
}));
// Add InterfaceController
this.interface = new InterfaceController(this);
// Register events
await page.exposeFunction('onAddMessageEvent', msg => {
async attachEventListeners(reinject = false) {
if (!reinject) {
await this.pupPage.exposeFunction('onAddMessageEvent', msg => {
if (msg.type === 'gp2') {
const notification = new GroupNotification(this, msg);
if (['add', 'invite', 'linked_group_join'].includes(msg.subtype)) {
@ -440,7 +440,7 @@ class Client extends EventEmitter {
let last_message;
await page.exposeFunction('onChangeMessageTypeEvent', (msg) => {
await this.pupPage.exposeFunction('onChangeMessageTypeEvent', (msg) => {
if (msg.type === 'revoked') {
const message = new Message(this, msg);
@ -461,7 +461,7 @@ class Client extends EventEmitter {
});
await page.exposeFunction('onChangeMessageEvent', (msg) => {
await this.pupPage.exposeFunction('onChangeMessageEvent', (msg) => {
if (msg.type !== 'revoked') {
last_message = msg;
@ -499,7 +499,7 @@ class Client extends EventEmitter {
}
});
await page.exposeFunction('onRemoveMessageEvent', (msg) => {
await this.pupPage.exposeFunction('onRemoveMessageEvent', (msg) => {
if (!msg.isNewMsg) return;
@ -514,7 +514,7 @@ class Client extends EventEmitter {
});
await page.exposeFunction('onMessageAckEvent', (msg, ack) => {
await this.pupPage.exposeFunction('onMessageAckEvent', (msg, ack) => {
const message = new Message(this, msg);
@ -528,7 +528,7 @@ class Client extends EventEmitter {
});
await page.exposeFunction('onChatUnreadCountEvent', async (data) =>{
await this.pupPage.exposeFunction('onChatUnreadCountEvent', async (data) =>{
const chat = await this.getChatById(data.id);
/**
@ -537,7 +537,7 @@ class Client extends EventEmitter {
this.emit(Events.UNREAD_COUNT, chat);
});
await page.exposeFunction('onMessageMediaUploadedEvent', (msg) => {
await this.pupPage.exposeFunction('onMessageMediaUploadedEvent', (msg) => {
const message = new Message(this, msg);
@ -549,8 +549,7 @@ class Client extends EventEmitter {
this.emit(Events.MEDIA_UPLOADED, message);
});
await page.exposeFunction('onAppStateChangedEvent', async (state) => {
await this.pupPage.exposeFunction('onAppStateChangedEvent', async (state) => {
/**
* Emitted when the connection state changes
* @event Client#change_state
@ -574,7 +573,7 @@ class Client extends EventEmitter {
/**
* Emitted when the client has been disconnected
* @event Client#disconnected
* @param {WAState|"NAVIGATION"} reason reason that caused the disconnect
* @param {WAState|"LOGOUT"} reason reason that caused the disconnect
*/
await this.authStrategy.disconnect();
this.emit(Events.DISCONNECTED, state);
@ -582,7 +581,7 @@ class Client extends EventEmitter {
}
});
await page.exposeFunction('onBatteryStateChangedEvent', (state) => {
await this.pupPage.exposeFunction('onBatteryStateChangedEvent', (state) => {
const { battery, plugged } = state;
if (battery === undefined) return;
@ -598,7 +597,7 @@ class Client extends EventEmitter {
this.emit(Events.BATTERY_CHANGED, { battery, plugged });
});
await page.exposeFunction('onIncomingCall', (call) => {
await this.pupPage.exposeFunction('onIncomingCall', (call) => {
/**
* Emitted when a call is received
* @event Client#incoming_call
@ -616,7 +615,7 @@ class Client extends EventEmitter {
this.emit(Events.INCOMING_CALL, cll);
});
await page.exposeFunction('onReaction', (reactions) => {
await this.pupPage.exposeFunction('onReaction', (reactions) => {
for (const reaction of reactions) {
/**
* Emitted when a reaction is sent, received, updated or removed
@ -637,7 +636,7 @@ class Client extends EventEmitter {
}
});
await page.exposeFunction('onRemoveChatEvent', async (chat) => {
await this.pupPage.exposeFunction('onRemoveChatEvent', async (chat) => {
const _chat = await this.getChatById(chat.id);
/**
@ -648,7 +647,7 @@ class Client extends EventEmitter {
this.emit(Events.CHAT_REMOVED, _chat);
});
await page.exposeFunction('onArchiveChatEvent', async (chat, currState, prevState) => {
await this.pupPage.exposeFunction('onArchiveChatEvent', async (chat, currState, prevState) => {
const _chat = await this.getChatById(chat.id);
/**
@ -661,7 +660,7 @@ class Client extends EventEmitter {
this.emit(Events.CHAT_ARCHIVED, _chat, currState, prevState);
});
await page.exposeFunction('onEditMessageEvent', (msg, newBody, prevBody) => {
await this.pupPage.exposeFunction('onEditMessageEvent', (msg, newBody, prevBody) => {
if(msg.type === 'revoked'){
return;
@ -676,7 +675,7 @@ class Client extends EventEmitter {
this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody);
});
await page.exposeFunction('onAddMessageCiphertextEvent', msg => {
await this.pupPage.exposeFunction('onAddMessageCiphertextEvent', msg => {
/**
* Emitted when messages are edited
@ -685,8 +684,9 @@ class Client extends EventEmitter {
*/
this.emit(Events.MESSAGE_CIPHERTEXT, new Message(this, msg));
});
}
await page.exposeFunction('onPollVoteEvent', (vote) => {
await this.pupPage.exposeFunction('onPollVoteEvent', (vote) => {
const _vote = new PollVote(this, vote);
/**
* Emitted when some poll option is selected or deselected,
@ -696,7 +696,7 @@ class Client extends EventEmitter {
this.emit(Events.VOTE_UPDATE, _vote);
});
await page.evaluate(() => {
await this.pupPage.evaluate(() => {
window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(window.WWebJS.getMessageModel(msg)); });
window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(window.WWebJS.getMessageModel(msg)); });
window.Store.Msg.on('change:ack', (msg, ack) => { window.onMessageAckEvent(window.WWebJS.getMessageModel(msg), ack); });
@ -725,7 +725,23 @@ class Client extends EventEmitter {
pollVoteModel && window.onPollVoteEvent(pollVoteModel);
});
{
if (window.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.1014111620')) {
const module = window.Store.AddonReactionTable;
const ogMethod = module.bulkUpsert;
module.bulkUpsert = ((...args) => {
window.onReaction(args[0].map(reaction => {
const msgKey = reaction.id;
const parentMsgKey = reaction.reactionParentKey;
const timestamp = reaction.reactionTimestamp / 1000;
const sender = reaction.author ?? reaction.from;
const senderUserJid = sender._serialized;
return {...reaction, msgKey, parentMsgKey, senderUserJid, timestamp };
}));
return ogMethod(...args);
}).bind(module);
} else {
const module = window.Store.createOrUpdateReactionsModule;
const ogMethod = module.createOrUpdateReactions;
module.createOrUpdateReactions = ((...args) => {
@ -741,23 +757,6 @@ class Client extends EventEmitter {
}).bind(module);
}
});
/**
* Emitted when the client has initialized and is ready to receive messages.
* @event Client#ready
*/
this.emit(Events.READY);
this.authStrategy.afterAuthReady();
// Disconnect when navigating away when in PAIRING state (detect logout)
this.pupPage.on('framenavigated', async () => {
const appState = await this.getState();
if(!appState || appState === WAState.PAIRING) {
await this.authStrategy.disconnect();
this.emit(Events.DISCONNECTED, 'NAVIGATION');
await this.destroy();
}
});
}
async initWebVersionCache() {
@ -783,7 +782,8 @@ class Client extends EventEmitter {
} else {
this.pupPage.on('response', async (res) => {
if(res.ok() && res.url() === WhatsWebURL) {
await webCache.persist(await res.text());
const indexHtml = await res.text();
this.currentIndexHtml = indexHtml;
}
});
}
@ -861,6 +861,7 @@ class Client extends EventEmitter {
* @property {GroupMention[]} [groupMentions] - An array of object that handle group mentions
* @property {string[]} [mentions] - User IDs to mention in the message
* @property {boolean} [sendSeen=true] - Mark the conversation as seen after sending the message
* @property {string} [invokedBotWid=undefined] - Bot Wid when doing a bot mention like @Meta AI
* @property {string} [stickerAuthor=undefined] - Sets the author of the sticker, (if sendMediaAsSticker is true).
* @property {string} [stickerName=undefined] - Sets the name of the sticker, (if sendMediaAsSticker is true).
* @property {string[]} [stickerCategories=undefined] - Sets the categories of the sticker, (if sendMediaAsSticker is true). Provide emoji char array, can be null.
@ -894,9 +895,10 @@ class Client extends EventEmitter {
sendMediaAsDocument: options.sendMediaAsDocument,
caption: options.caption,
quotedMessageId: options.quotedMessageId,
parseVCards: options.parseVCards === false ? false : true,
parseVCards: options.parseVCards !== false,
mentionedJidList: options.mentions || [],
groupMentions: options.groupMentions,
invokedBotWid: options.invokedBotWid,
extraOptions: options.extra
};
@ -1102,14 +1104,8 @@ class Client extends EventEmitter {
async setDisplayName(displayName) {
const couldSet = await this.pupPage.evaluate(async displayName => {
if(!window.Store.Conn.canSetMyPushname()) return false;
if(window.Store.MDBackend) {
await window.Store.Settings.setPushname(displayName);
return true;
} else {
const res = await window.Store.Wap.setPushname(displayName);
return !res.status || res.status === 200;
}
}, displayName);
return couldSet;
@ -1250,7 +1246,9 @@ class Client extends EventEmitter {
const profilePic = await this.pupPage.evaluate(async contactId => {
try {
const chatWid = window.Store.WidFactory.createWid(contactId);
return await window.Store.ProfilePic.profilePicFind(chatWid);
return window.compareWwebVersions(window.Debug.VERSION, '<', '2.3000.0')
? await window.Store.ProfilePic.profilePicFind(chatWid)
: await window.Store.ProfilePic.requestProfilePicFromServer(chatWid);
} catch (err) {
if(err.name === 'ServerStatusCodeError') return undefined;
throw err;
@ -1416,10 +1414,18 @@ class Client extends EventEmitter {
try {
createGroupResult = await window.Store.GroupUtils.createGroup(
title,
participantWids,
messageTimer,
parentGroupWid
{
'memberAddMode': options.memberAddMode === undefined ? true : options.memberAddMode,
'membershipApprovalMode': options.membershipApprovalMode === undefined ? false : options.membershipApprovalMode,
'announce': options.announce === undefined ? true : options.announce,
'ephemeralDuration': messageTimer,
'full': undefined,
'parentGroupId': parentGroupWid,
'restrict': options.restrict === undefined ? true : options.restrict,
'thumb': undefined,
'title': title,
},
participantWids
);
} catch (err) {
return 'CreateGroupError: An unknown error occupied while creating a group';
@ -1431,7 +1437,7 @@ class Client extends EventEmitter {
const statusCode = participant.error ?? 200;
if (autoSendInviteV4 && statusCode === 403) {
window.Store.ContactCollection.gadd(participant.wid, { silent: true });
window.Store.Contact.gadd(participant.wid, { silent: true });
const addParticipantResult = await window.Store.GroupInviteV4.sendGroupInviteMessage(
await window.Store.Chat.find(participant.wid),
createGroupResult.wid._serialized,

View file

@ -1,72 +0,0 @@
'use strict';
const BaseAuthStrategy = require('./BaseAuthStrategy');
/**
* Legacy session auth strategy
* Not compatible with multi-device accounts.
* @param {object} options - options
* @param {string} options.restartOnAuthFail - Restart client with a new session (i.e. use null 'session' var) if authentication fails
* @param {object} options.session - Whatsapp session to restore. If not set, will start a new session
* @param {string} options.session.WABrowserId
* @param {string} options.session.WASecretBundle
* @param {string} options.session.WAToken1
* @param {string} options.session.WAToken2
*/
class LegacySessionAuth extends BaseAuthStrategy {
constructor({ session, restartOnAuthFail }={}) {
super();
this.session = session;
this.restartOnAuthFail = restartOnAuthFail;
}
async afterBrowserInitialized() {
if(this.session) {
await this.client.pupPage.evaluateOnNewDocument(session => {
if (document.referrer === 'https://whatsapp.com/') {
localStorage.clear();
localStorage.setItem('WABrowserId', session.WABrowserId);
localStorage.setItem('WASecretBundle', session.WASecretBundle);
localStorage.setItem('WAToken1', session.WAToken1);
localStorage.setItem('WAToken2', session.WAToken2);
}
localStorage.setItem('remember-me', 'true');
}, this.session);
}
}
async onAuthenticationNeeded() {
if(this.session) {
this.session = null;
return {
failed: true,
restart: this.restartOnAuthFail,
failureEventPayload: 'Unable to log in. Are the session details valid?'
};
}
return { failed: false };
}
async getAuthEventPayload() {
const isMD = await this.client.pupPage.evaluate(() => {
return window.Store.MDBackend;
});
if(isMD) throw new Error('Authenticating via JSON session is not supported for MultiDevice-enabled WhatsApp accounts.');
const localStorage = JSON.parse(await this.client.pupPage.evaluate(() => {
return JSON.stringify(window.localStorage);
}));
return {
WABrowserId: localStorage.WABrowserId,
WASecretBundle: localStorage.WASecretBundle,
WAToken1: localStorage.WAToken1,
WAToken2: localStorage.WAToken2
};
}
}
module.exports = LegacySessionAuth;

View file

@ -44,7 +44,10 @@ class LocalAuth extends BaseAuthStrategy {
async logout() {
if (this.userDataDir) {
return (fs.rmSync ? fs.rmSync : fs.rmdirSync).call(this, this.userDataDir, { recursive: true, force: true });
await fs.promises.rm(this.userDataDir, { recursive: true, force: true })
.catch((e) => {
throw new Error(e);
});
}
}

View file

@ -98,7 +98,7 @@ class GroupChat extends Chat {
419: 'The participant can\'t be added because the group is full'
};
await window.Store.GroupMetadata.queryAndUpdate(groupWid);
await window.Store.GroupQueryAndUpdate(groupWid);
const groupMetadata = group.groupMetadata;
const groupParticipants = groupMetadata?.participants;
@ -152,7 +152,7 @@ class GroupChat extends Chat {
if (autoSendInviteV4 && rpcResultCode === 403) {
let userChat, isInviteV4Sent = false;
window.Store.ContactCollection.gadd(pWid, { silent: true });
window.Store.Contact.gadd(pWid, { silent: true });
if (rpcResult.name === 'ParticipantRequestCodeCanBeSent' &&
(userChat = await window.Store.Chat.find(pWid))) {
@ -380,10 +380,18 @@ class GroupChat extends Chat {
async getInviteCode() {
const codeRes = await this.client.pupPage.evaluate(async chatId => {
const chatWid = window.Store.WidFactory.createWid(chatId);
return window.Store.GroupInvite.queryGroupInviteCode(chatWid);
try {
return window.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.0')
? await window.Store.GroupInvite.queryGroupInviteCode(chatWid, true)
: await window.Store.GroupInvite.queryGroupInviteCode(chatWid);
}
catch (err) {
if(err.name === 'ServerStatusCodeError') return undefined;
throw err;
}
}, this.id._serialized);
return codeRes.code;
return codeRes?.code;
}
/**

View file

@ -434,10 +434,7 @@ class Message extends Base {
const chatId = typeof chat === 'string' ? chat : chat.id._serialized;
await this.client.pupPage.evaluate(async (msgId, chatId) => {
let msg = window.Store.Msg.get(msgId);
let chat = window.Store.Chat.get(chatId);
return await chat.forwardMessages([msg]);
return window.WWebJS.forwardMessage(chatId, msgId);
}, this.id._serialized, chatId);
}
@ -508,8 +505,12 @@ class Message extends Base {
const canRevoke = window.Store.MsgActionChecks.canSenderRevokeMsg(msg) || window.Store.MsgActionChecks.canAdminRevokeMsg(msg);
if (everyone && canRevoke) {
if (window.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.0')) {
return window.Store.Cmd.sendRevokeMsgs(chat, { list: [msg], type: 'message' }, { clearMedia: true });
} else {
return window.Store.Cmd.sendRevokeMsgs(chat, [msg], { clearMedia: true, type: msg.id.fromMe ? 'Sender' : 'Admin' });
}
}
return window.Store.Cmd.sendDeleteMsgs(chat, [msg], true);
}, this.id._serialized, everyone);

View file

@ -0,0 +1,17 @@
'use strict';
exports.ExposeAuthStore = () => {
window.AuthStore = {};
window.AuthStore.AppState = window.require('WAWebSocketModel').Socket;
window.AuthStore.Cmd = window.require('WAWebCmd').Cmd;
window.AuthStore.Conn = window.require('WAWebConnModel').Conn;
window.AuthStore.OfflineMessageHandler = window.require('WAWebOfflineHandler').OfflineMessageHandler;
window.AuthStore.PairingCodeLinkUtils = window.require('WAWebAltDeviceLinkingApi');
window.AuthStore.Base64Tools = window.require('WABase64');
window.AuthStore.RegistrationUtils = {
...window.require('WAWebCompanionRegClientUtils'),
...window.require('WAWebAdvSignatureApi'),
...window.require('WAWebUserPrefsInfoStore'),
...window.require('WAWebSignalStoreApi'),
};
};

View file

@ -0,0 +1,22 @@
'use strict';
//TODO: To be removed by version 2.3000.x hard release
exports.ExposeLegacyAuthStore = (moduleRaidStr) => {
eval('var moduleRaid = ' + moduleRaidStr);
// eslint-disable-next-line no-undef
window.mR = moduleRaid();
window.AuthStore = {};
window.AuthStore.AppState = window.mR.findModule('Socket')[0].Socket;
window.AuthStore.Cmd = window.mR.findModule('Cmd')[0].Cmd;
window.AuthStore.Conn = window.mR.findModule('Conn')[0].Conn;
window.AuthStore.OfflineMessageHandler = window.mR.findModule('OfflineMessageHandler')[0].OfflineMessageHandler;
window.AuthStore.PairingCodeLinkUtils = window.mR.findModule('initializeAltDeviceLinking')[0];
window.AuthStore.Base64Tools = window.mR.findModule('encodeB64')[0];
window.AuthStore.RegistrationUtils = {
...window.mR.findModule('getCompanionWebClientFromBrowser')[0],
...window.mR.findModule('verifyKeyIndexListAccountSignature')[0],
...window.mR.findModule('waNoiseInfo')[0],
...window.mR.findModule('waSignalStore')[0],
};
};

View file

@ -0,0 +1,146 @@
'use strict';
//TODO: To be removed by version 2.3000.x hard release
// Exposes the internal Store to the WhatsApp Web client
exports.ExposeLegacyStore = () => {
window.Store = Object.assign({}, window.mR.findModule(m => m.default && m.default.Chat)[0].default);
window.Store.AppState = window.mR.findModule('Socket')[0].Socket;
window.Store.Conn = window.mR.findModule('Conn')[0].Conn;
window.Store.BlockContact = window.mR.findModule('blockContact')[0];
window.Store.Call = window.mR.findModule((module) => module.default && module.default.Call)[0].default.Call;
window.Store.Cmd = window.mR.findModule('Cmd')[0].Cmd;
window.Store.CryptoLib = window.mR.findModule('decryptE2EMedia')[0];
window.Store.DownloadManager = window.mR.findModule('downloadManager')[0].downloadManager;
window.Store.GroupMetadata = window.mR.findModule('GroupMetadata')[0].default.GroupMetadata;
window.Store.GroupQueryAndUpdate = window.mR.findModule('queryAndUpdateGroupMetadataById')[0].queryAndUpdateGroupMetadataById;
window.Store.Label = window.mR.findModule('LabelCollection')[0].LabelCollection;
window.Store.MediaPrep = window.mR.findModule('prepRawMedia')[0];
window.Store.MediaObject = window.mR.findModule('getOrCreateMediaObject')[0];
window.Store.NumberInfo = window.mR.findModule('formattedPhoneNumber')[0];
window.Store.MediaTypes = window.mR.findModule('msgToMediaType')[0];
window.Store.MediaUpload = window.mR.findModule('uploadMedia')[0];
window.Store.MsgKey = window.mR.findModule((module) => module.default && module.default.fromString)[0].default;
window.Store.OpaqueData = window.mR.findModule(module => module.default && module.default.createFromData)[0].default;
window.Store.QueryProduct = window.mR.findModule('queryProduct')[0];
window.Store.QueryOrder = window.mR.findModule('queryOrder')[0];
window.Store.SendClear = window.mR.findModule('sendClear')[0];
window.Store.SendDelete = window.mR.findModule('sendDelete')[0];
window.Store.SendMessage = window.mR.findModule('addAndSendMsgToChat')[0];
window.Store.EditMessage = window.mR.findModule('addAndSendMessageEdit')[0];
window.Store.SendSeen = window.mR.findModule('sendSeen')[0];
window.Store.User = window.mR.findModule('getMaybeMeUser')[0];
window.Store.ContactMethods = window.mR.findModule('getUserid')[0];
window.Store.UploadUtils = window.mR.findModule((module) => (module.default && module.default.encryptAndUpload) ? module.default : null)[0].default;
window.Store.UserConstructor = window.mR.findModule((module) => (module.default && module.default.prototype && module.default.prototype.isServer && module.default.prototype.isUser) ? module.default : null)[0].default;
window.Store.Validators = window.mR.findModule('findLinks')[0];
window.Store.VCard = window.mR.findModule('vcardFromContactModel')[0];
window.Store.WidFactory = window.mR.findModule('createWid')[0];
window.Store.ProfilePic = window.mR.findModule('profilePicResync')[0];
window.Store.PresenceUtils = window.mR.findModule('sendPresenceAvailable')[0];
window.Store.ChatState = window.mR.findModule('sendChatStateComposing')[0];
window.Store.findCommonGroups = window.mR.findModule('findCommonGroups')[0].findCommonGroups;
window.Store.StatusUtils = window.mR.findModule('setMyStatus')[0];
window.Store.ConversationMsgs = window.mR.findModule('loadEarlierMsgs')[0];
window.Store.sendReactionToMsg = window.mR.findModule('sendReactionToMsg')[0].sendReactionToMsg;
window.Store.createOrUpdateReactionsModule = window.mR.findModule('createOrUpdateReactions')[0];
window.Store.EphemeralFields = window.mR.findModule('getEphemeralFields')[0];
window.Store.MsgActionChecks = window.mR.findModule('canSenderRevokeMsg')[0];
window.Store.QuotedMsg = window.mR.findModule('getQuotedMsgObj')[0];
window.Store.LinkPreview = window.mR.findModule('getLinkPreview')[0];
window.Store.Socket = window.mR.findModule('deprecatedSendIq')[0];
window.Store.SocketWap = window.mR.findModule('wap')[0];
window.Store.SearchContext = window.mR.findModule('getSearchContext')[0].getSearchContext;
window.Store.DrawerManager = window.mR.findModule('DrawerManager')[0].DrawerManager;
window.Store.LidUtils = window.mR.findModule('getCurrentLid')[0];
window.Store.WidToJid = window.mR.findModule('widToUserJid')[0];
window.Store.JidToWid = window.mR.findModule('userJidToUserWid')[0];
window.Store.getMsgInfo = (window.mR.findModule('sendQueryMsgInfo')[0] || {}).sendQueryMsgInfo || window.mR.findModule('queryMsgInfo')[0].queryMsgInfo;
window.Store.pinUnpinMsg = window.mR.findModule('sendPinInChatMsg')[0].sendPinInChatMsg;
/* eslint-disable no-undef, no-cond-assign */
window.Store.QueryExist = ((m = window.mR.findModule('queryExists')[0]) ? m.queryExists : window.mR.findModule('queryExist')[0].queryWidExists);
window.Store.ReplyUtils = (m = window.mR.findModule('canReplyMsg')).length > 0 && m[0];
/* eslint-enable no-undef, no-cond-assign */
window.Store.Settings = {
...window.mR.findModule('ChatlistPanelState')[0],
setPushname: window.mR.findModule((m) => m.setPushname && !m.ChatlistPanelState)[0].setPushname
};
window.Store.StickerTools = {
...window.mR.findModule('toWebpSticker')[0],
...window.mR.findModule('addWebpMetadata')[0]
};
window.Store.GroupUtils = {
...window.mR.findModule('createGroup')[0],
...window.mR.findModule('setGroupDescription')[0],
...window.mR.findModule('sendExitGroup')[0],
...window.mR.findModule('sendSetPicture')[0]
};
window.Store.GroupParticipants = {
...window.mR.findModule('promoteParticipants')[0],
...window.mR.findModule('sendAddParticipantsRPC')[0]
};
window.Store.GroupInvite = {
...window.mR.findModule('resetGroupInviteCode')[0],
...window.mR.findModule('queryGroupInvite')[0]
};
window.Store.GroupInviteV4 = {
...window.mR.findModule('queryGroupInviteV4')[0],
...window.mR.findModule('sendGroupInviteMessage')[0]
};
window.Store.MembershipRequestUtils = {
...window.mR.findModule('getMembershipApprovalRequests')[0],
...window.mR.findModule('sendMembershipRequestsActionRPC')[0]
};
if (!window.Store.Chat._find) {
window.Store.Chat._find = e => {
const target = window.Store.Chat.get(e);
return target ? Promise.resolve(target) : Promise.resolve({
id: e
});
};
}
// eslint-disable-next-line no-undef
if ((m = window.mR.findModule('ChatCollection')[0]) && m.ChatCollection && typeof m.ChatCollection.findImpl === 'undefined' && typeof m.ChatCollection._find !== 'undefined') m.ChatCollection.findImpl = m.ChatCollection._find;
const _isMDBackend = window.mR.findModule('isMDBackend');
if(_isMDBackend && _isMDBackend[0] && _isMDBackend[0].isMDBackend) {
window.Store.MDBackend = _isMDBackend[0].isMDBackend();
} else {
window.Store.MDBackend = true;
}
const _features = window.mR.findModule('FEATURE_CHANGE_EVENT')[0];
if(_features) {
window.Store.Features = _features.LegacyPhoneFeatures;
}
/**
* Target options object description
* @typedef {Object} TargetOptions
* @property {string|number} module The name or a key of the target module to search
* @property {number} index The index value of the target module
* @property {string} function The function name to get from a module
*/
/**
* Function to modify functions
* @param {TargetOptions} target Options specifying the target function to search for modifying
* @param {Function} callback Modified function
*/
window.injectToFunction = (target, callback) => {
const module = typeof target.module === 'string'
? window.mR.findModule(target.module)
: window.mR.modules[target.module];
const originalFunction = module[target.index][target.function];
const modifiedFunction = (...args) => callback(originalFunction, ...args);
module[target.index][target.function] = modifiedFunction;
};
window.injectToFunction({ module: 'mediaTypeFromProtobuf', index: 0, function: 'mediaTypeFromProtobuf' }, (func, ...args) => { const [proto] = args; return proto.locationMessage ? null : func(...args); });
window.injectToFunction({ module: 'typeAttributeFromProtobuf', index: 0, function: 'typeAttributeFromProtobuf' }, (func, ...args) => { const [proto] = args; return proto.locationMessage || proto.groupInviteMessage ? 'text' : func(...args); });
};

166
src/util/Injected/Store.js Normal file
View file

@ -0,0 +1,166 @@
'use strict';
exports.ExposeStore = () => {
/**
* Helper function that compares between two WWeb versions. Its purpose is to help the developer to choose the correct code implementation depending on the comparison value and the WWeb version.
* @param {string} lOperand The left operand for the WWeb version string to compare with
* @param {string} operator The comparison operator
* @param {string} rOperand The right operand for the WWeb version string to compare with
* @returns {boolean} Boolean value that indicates the result of the comparison
*/
window.compareWwebVersions = (lOperand, operator, rOperand) => {
if (!['>', '>=', '<', '<=', '='].includes(operator)) {
throw new class _ extends Error {
constructor(m) { super(m); this.name = 'CompareWwebVersionsError'; }
}('Invalid comparison operator is provided');
}
if (typeof lOperand !== 'string' || typeof rOperand !== 'string') {
throw new class _ extends Error {
constructor(m) { super(m); this.name = 'CompareWwebVersionsError'; }
}('A non-string WWeb version type is provided');
}
lOperand = lOperand.replace(/-beta$/, '');
rOperand = rOperand.replace(/-beta$/, '');
while (lOperand.length !== rOperand.length) {
lOperand.length > rOperand.length
? rOperand = rOperand.concat('0')
: lOperand = lOperand.concat('0');
}
lOperand = Number(lOperand.replace(/\./g, ''));
rOperand = Number(rOperand.replace(/\./g, ''));
return (
operator === '>' ? lOperand > rOperand :
operator === '>=' ? lOperand >= rOperand :
operator === '<' ? lOperand < rOperand :
operator === '<=' ? lOperand <= rOperand :
operator === '=' ? lOperand === rOperand :
false
);
};
window.Store = Object.assign({}, window.require('WAWebCollections'));
window.Store.AppState = window.require('WAWebSocketModel').Socket;
window.Store.BlockContact = window.require('WAWebBlockContactAction');
window.Store.Conn = window.require('WAWebConnModel').Conn;
window.Store.Cmd = window.require('WAWebCmd').Cmd;
window.Store.DownloadManager = window.require('WAWebDownloadManager').downloadManager;
window.Store.GroupQueryAndUpdate = window.require('WAWebGroupQueryJob').queryAndUpdateGroupMetadataById;
window.Store.MediaPrep = window.require('WAWebPrepRawMedia');
window.Store.MediaObject = window.require('WAWebMediaStorage');
window.Store.MediaTypes = window.require('WAWebMmsMediaTypes');
window.Store.MediaUpload = window.require('WAWebMediaMmsV4Upload');
window.Store.MsgKey = window.require('WAWebMsgKey');
window.Store.NumberInfo = window.require('WAPhoneUtils');
window.Store.OpaqueData = window.require('WAWebMediaOpaqueData');
window.Store.QueryProduct = window.require('WAWebBizProductCatalogBridge');
window.Store.QueryOrder = window.require('WAWebBizOrderBridge');
window.Store.SendClear = window.require('WAWebChatClearBridge');
window.Store.SendDelete = window.require('WAWebDeleteChatAction');
window.Store.SendMessage = window.require('WAWebSendMsgChatAction');
window.Store.EditMessage = window.require('WAWebSendMessageEditAction');
window.Store.SendSeen = window.require('WAWebUpdateUnreadChatAction');
window.Store.User = window.require('WAWebUserPrefsMeUser');
window.Store.ContactMethods = window.require('WAWebContactGetters');
window.Store.UploadUtils = window.require('WAWebUploadManager');
window.Store.UserConstructor = window.require('WAWebWid');
window.Store.Validators = window.require('WALinkify');
window.Store.VCard = window.require('WAWebFrontendVcardUtils');
window.Store.WidFactory = window.require('WAWebWidFactory');
window.Store.ProfilePic = window.require('WAWebContactProfilePicThumbBridge');
window.Store.PresenceUtils = window.require('WAWebPresenceChatAction');
window.Store.ChatState = window.require('WAWebChatStateBridge');
window.Store.findCommonGroups = window.require('WAWebFindCommonGroupsContactAction').findCommonGroups;
window.Store.StatusUtils = window.require('WAWebContactStatusBridge');
window.Store.ConversationMsgs = window.require('WAWebChatLoadMessages');
window.Store.sendReactionToMsg = window.require('WAWebSendReactionMsgAction').sendReactionToMsg;
window.Store.createOrUpdateReactionsModule = window.require('WAWebDBCreateOrUpdateReactions');
window.Store.EphemeralFields = window.require('WAWebGetEphemeralFieldsMsgActionsUtils');
window.Store.MsgActionChecks = window.require('WAWebMsgActionCapability');
window.Store.QuotedMsg = window.require('WAWebQuotedMsgModelUtils');
window.Store.LinkPreview = window.require('WAWebLinkPreviewChatAction');
window.Store.Socket = window.require('WADeprecatedSendIq');
window.Store.SocketWap = window.require('WAWap');
window.Store.SearchContext = window.require('WAWebChatMessageSearch').getSearchContext;
window.Store.DrawerManager = window.require('WAWebDrawerManager').DrawerManager;
window.Store.LidUtils = window.require('WAWebApiContact');
window.Store.WidToJid = window.require('WAWebWidToJid');
window.Store.JidToWid = window.require('WAWebJidToWid');
window.Store.getMsgInfo = window.require('WAWebApiMessageInfoStore').queryMsgInfo;
window.Store.pinUnpinMsg = window.require('WAWebSendPinMessageAction').sendPinInChatMsg;
window.Store.QueryExist = window.require('WAWebQueryExistsJob').queryWidExists;
window.Store.ReplyUtils = window.require('WAWebMsgReply');
window.Store.Settings = window.require('WAWebUserPrefsGeneral');
window.Store.BotSecret = window.require('WAWebBotMessageSecret');
window.Store.BotProfiles = window.require('WAWebBotProfileCollection');
if (window.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.1014111620'))
window.Store.AddonReactionTable = window.require('WAWebAddonReactionTableMode').reactionTableMode;
window.Store.ForwardUtils = {
...window.require('WAWebForwardMessagesToChat')
};
window.Store.StickerTools = {
...window.require('WAWebImageUtils'),
...window.require('WAWebAddWebpMetadata')
};
window.Store.GroupUtils = {
...window.require('WAWebGroupCreateJob'),
...window.require('WAWebGroupModifyInfoJob'),
...window.require('WAWebExitGroupAction'),
...window.require('WAWebContactProfilePicThumbBridge')
};
window.Store.GroupParticipants = {
...window.require('WAWebModifyParticipantsGroupAction'),
...window.require('WASmaxGroupsAddParticipantsRPC')
};
window.Store.GroupInvite = {
...window.require('WAWebGroupInviteJob'),
...window.require('WAWebGroupQueryJob')
};
window.Store.GroupInviteV4 = {
...window.require('WAWebGroupInviteV4Job'),
...window.require('WAWebChatSendMessages')
};
window.Store.MembershipRequestUtils = {
...window.require('WAWebApiMembershipApprovalRequestStore'),
...window.require('WASmaxGroupsMembershipRequestsActionRPC')
};
if (!window.Store.Chat._find || !window.Store.Chat.findImpl) {
window.Store.Chat._find = e => {
const target = window.Store.Chat.get(e);
return target ? Promise.resolve(target) : Promise.resolve({
id: e
});
};
window.Store.Chat.findImpl = window.Store.Chat._find;
}
/**
* Target options object description
* @typedef {Object} TargetOptions
* @property {string|number} module The target module
* @property {string} function The function name to get from a module
*/
/**
* Function to modify functions
* @param {TargetOptions} target Options specifying the target function to search for modifying
* @param {Function} callback Modified function
*/
window.injectToFunction = (target, callback) => {
const module = window.require(target.module);
const originalFunction = module[target.function];
const modifiedFunction = (...args) => callback(originalFunction, ...args);
module[target.function] = modifiedFunction;
};
window.injectToFunction({ module: 'WAWebBackendJobsCommon', function: 'mediaTypeFromProtobuf' }, (func, ...args) => { const [proto] = args; return proto.locationMessage ? null : func(...args); });
window.injectToFunction({ module: 'WAWebE2EProtoUtils', function: 'typeAttributeFromProtobuf' }, (func, ...args) => { const [proto] = args; return proto.locationMessage || proto.groupInviteMessage ? 'text' : func(...args); });
};

View file

@ -1,156 +1,19 @@
'use strict';
// Exposes the internal Store to the WhatsApp Web client
exports.ExposeStore = (moduleRaidStr) => {
eval('var moduleRaid = ' + moduleRaidStr);
// eslint-disable-next-line no-undef
window.mR = moduleRaid();
window.Store = Object.assign({}, window.mR.findModule(m => m.default && m.default.Chat)[0].default);
window.Store.AppState = window.mR.findModule('Socket')[0].Socket;
window.Store.Conn = window.mR.findModule('Conn')[0].Conn;
window.Store.BlockContact = window.mR.findModule('blockContact')[0];
window.Store.Call = window.mR.findModule((module) => module.default && module.default.Call)[0].default.Call;
window.Store.Cmd = window.mR.findModule('Cmd')[0].Cmd;
window.Store.CryptoLib = window.mR.findModule('decryptE2EMedia')[0];
window.Store.DownloadManager = window.mR.findModule('downloadManager')[0].downloadManager;
window.Store.GroupMetadata = window.mR.findModule('GroupMetadata')[0].default.GroupMetadata;
window.Store.GroupMetadata.queryAndUpdate = window.mR.findModule('queryAndUpdateGroupMetadataById')[0].queryAndUpdateGroupMetadataById;
window.Store.Label = window.mR.findModule('LabelCollection')[0].LabelCollection;
window.Store.ContactCollection = window.mR.findModule('ContactCollection')[0].ContactCollection;
window.Store.MediaPrep = window.mR.findModule('prepRawMedia')[0];
window.Store.MediaObject = window.mR.findModule('getOrCreateMediaObject')[0];
window.Store.NumberInfo = window.mR.findModule('formattedPhoneNumber')[0];
window.Store.MediaTypes = window.mR.findModule('msgToMediaType')[0];
window.Store.MediaUpload = window.mR.findModule('uploadMedia')[0];
window.Store.MsgKey = window.mR.findModule((module) => module.default && module.default.fromString)[0].default;
window.Store.OpaqueData = window.mR.findModule(module => module.default && module.default.createFromData)[0].default;
window.Store.QueryProduct = window.mR.findModule('queryProduct')[0];
window.Store.QueryOrder = window.mR.findModule('queryOrder')[0];
window.Store.SendClear = window.mR.findModule('sendClear')[0];
window.Store.SendDelete = window.mR.findModule('sendDelete')[0];
window.Store.SendMessage = window.mR.findModule('addAndSendMsgToChat')[0];
window.Store.EditMessage = window.mR.findModule('addAndSendMessageEdit')[0];
window.Store.SendSeen = window.mR.findModule('sendSeen')[0];
window.Store.User = window.mR.findModule('getMaybeMeUser')[0];
window.Store.ContactMethods = window.mR.findModule('getUserid')[0];
window.Store.BusinessProfileCollection = window.mR.findModule('BusinessProfileCollection')[0].BusinessProfileCollection;
window.Store.UploadUtils = window.mR.findModule((module) => (module.default && module.default.encryptAndUpload) ? module.default : null)[0].default;
window.Store.UserConstructor = window.mR.findModule((module) => (module.default && module.default.prototype && module.default.prototype.isServer && module.default.prototype.isUser) ? module.default : null)[0].default;
window.Store.Validators = window.mR.findModule('findLinks')[0];
window.Store.VCard = window.mR.findModule('vcardFromContactModel')[0];
window.Store.WidFactory = window.mR.findModule('createWid')[0];
window.Store.ProfilePic = window.mR.findModule('profilePicResync')[0];
window.Store.PresenceUtils = window.mR.findModule('sendPresenceAvailable')[0];
window.Store.ChatState = window.mR.findModule('sendChatStateComposing')[0];
window.Store.findCommonGroups = window.mR.findModule('findCommonGroups')[0].findCommonGroups;
window.Store.StatusUtils = window.mR.findModule('setMyStatus')[0];
window.Store.ConversationMsgs = window.mR.findModule('loadEarlierMsgs')[0];
window.Store.sendReactionToMsg = window.mR.findModule('sendReactionToMsg')[0].sendReactionToMsg;
window.Store.createOrUpdateReactionsModule = window.mR.findModule('createOrUpdateReactions')[0];
window.Store.EphemeralFields = window.mR.findModule('getEphemeralFields')[0];
window.Store.MsgActionChecks = window.mR.findModule('canSenderRevokeMsg')[0];
window.Store.QuotedMsg = window.mR.findModule('getQuotedMsgObj')[0];
window.Store.LinkPreview = window.mR.findModule('getLinkPreview')[0];
window.Store.Socket = window.mR.findModule('deprecatedSendIq')[0];
window.Store.SocketWap = window.mR.findModule('wap')[0];
window.Store.SearchContext = window.mR.findModule('getSearchContext')[0].getSearchContext;
window.Store.DrawerManager = window.mR.findModule('DrawerManager')[0].DrawerManager;
window.Store.LidUtils = window.mR.findModule('getCurrentLid')[0];
window.Store.WidToJid = window.mR.findModule('widToUserJid')[0];
window.Store.JidToWid = window.mR.findModule('userJidToUserWid')[0];
window.Store.getMsgInfo = (window.mR.findModule('sendQueryMsgInfo')[0] || {}).sendQueryMsgInfo || window.mR.findModule('queryMsgInfo')[0].queryMsgInfo;
window.Store.pinUnpinMsg = window.mR.findModule('sendPinInChatMsg')[0].sendPinInChatMsg;
/* eslint-disable no-undef, no-cond-assign */
window.Store.QueryExist = ((m = window.mR.findModule('queryExists')[0]) ? m.queryExists : window.mR.findModule('queryExist')[0].queryWidExists);
window.Store.ReplyUtils = (m = window.mR.findModule('canReplyMsg')).length > 0 && m[0];
/* eslint-enable no-undef, no-cond-assign */
window.Store.Settings = {
...window.mR.findModule('ChatlistPanelState')[0],
setPushname: window.mR.findModule((m) => m.setPushname && !m.ChatlistPanelState)[0].setPushname
};
window.Store.StickerTools = {
...window.mR.findModule('toWebpSticker')[0],
...window.mR.findModule('addWebpMetadata')[0]
};
window.Store.GroupUtils = {
...window.mR.findModule('createGroup')[0],
...window.mR.findModule('setGroupDescription')[0],
...window.mR.findModule('sendExitGroup')[0],
...window.mR.findModule('sendSetPicture')[0]
};
window.Store.GroupParticipants = {
...window.mR.findModule('promoteParticipants')[0],
...window.mR.findModule('sendAddParticipantsRPC')[0]
};
window.Store.GroupInvite = {
...window.mR.findModule('resetGroupInviteCode')[0],
...window.mR.findModule('queryGroupInvite')[0]
};
window.Store.GroupInviteV4 = {
...window.mR.findModule('queryGroupInviteV4')[0],
...window.mR.findModule('sendGroupInviteMessage')[0]
};
window.Store.MembershipRequestUtils = {
...window.mR.findModule('getMembershipApprovalRequests')[0],
...window.mR.findModule('sendMembershipRequestsActionRPC')[0]
};
if (!window.Store.Chat._find) {
window.Store.Chat._find = e => {
const target = window.Store.Chat.get(e);
return target ? Promise.resolve(target) : Promise.resolve({
id: e
});
};
}
// eslint-disable-next-line no-undef
if ((m = window.mR.findModule('ChatCollection')[0]) && m.ChatCollection && typeof m.ChatCollection.findImpl === 'undefined' && typeof m.ChatCollection._find !== 'undefined') m.ChatCollection.findImpl = m.ChatCollection._find;
const _isMDBackend = window.mR.findModule('isMDBackend');
if(_isMDBackend && _isMDBackend[0] && _isMDBackend[0].isMDBackend) {
window.Store.MDBackend = _isMDBackend[0].isMDBackend();
} else {
window.Store.MDBackend = true;
}
const _features = window.mR.findModule('FEATURE_CHANGE_EVENT')[0];
if(_features) {
window.Store.Features = _features.LegacyPhoneFeatures;
}
/**
* Target options object description
* @typedef {Object} TargetOptions
* @property {string|number} module The name or a key of the target module to search
* @property {number} index The index value of the target module
* @property {string} function The function name to get from a module
*/
/**
* Function to modify functions
* @param {TargetOptions} target Options specifying the target function to search for modifying
* @param {Function} callback Modified function
*/
window.injectToFunction = (target, callback) => {
const module = typeof target.module === 'string'
? window.mR.findModule(target.module)
: window.mR.modules[target.module];
const originalFunction = module[target.index][target.function];
const modifiedFunction = (...args) => callback(originalFunction, ...args);
module[target.index][target.function] = modifiedFunction;
};
window.injectToFunction({ module: 'mediaTypeFromProtobuf', index: 0, function: 'mediaTypeFromProtobuf' }, (func, ...args) => { const [proto] = args; return proto.locationMessage ? null : func(...args); });
window.injectToFunction({ module: 'typeAttributeFromProtobuf', index: 0, function: 'typeAttributeFromProtobuf' }, (func, ...args) => { const [proto] = args; return proto.locationMessage || proto.groupInviteMessage ? 'text' : func(...args); });
};
exports.LoadUtils = () => {
window.WWebJS = {};
window.WWebJS.forwardMessage = async (chatId, msgId) => {
let msg = window.Store.Msg.get(msgId);
let chat = window.Store.Chat.get(chatId);
if (window.compareWwebVersions(window.Debug.VERSION, '>', '2.3000.0')) {
return window.Store.ForwardUtils.forwardMessagesToChats([msg], [chat], false);
} else {
return chat.forwardMessages([msg]);
}
};
window.WWebJS.sendSeen = async (chatId) => {
let chat = window.Store.Chat.get(chatId);
if (chat !== undefined) {
@ -332,15 +195,23 @@ exports.LoadUtils = () => {
delete listOptions.list.footer;
}
const botOptions = {};
if (options.invokedBotWid) {
botOptions.messageSecret = window.crypto.getRandomValues(new Uint8Array(32));
botOptions.botMessageSecret = await window.Store.BotSecret.genBotMsgSecretFromMsgSecret(botOptions.messageSecret);
botOptions.invokedBotWid = window.Store.WidFactory.createWid(options.invokedBotWid);
botOptions.botPersonaId = window.Store.BotProfiles.BotProfileCollection.get(options.invokedBotWid).personaId;
delete options.invokedBotWid;
}
const meUser = window.Store.User.getMaybeMeUser();
const isMD = window.Store.MDBackend;
const newId = await window.Store.MsgKey.newId();
const newMsgId = new window.Store.MsgKey({
from: meUser,
to: chat.id,
id: newId,
participant: isMD && chat.id.isGroup() ? meUser : undefined,
participant: chat.id.isGroup() ? meUser : undefined,
selfDir: 'out',
});
@ -370,9 +241,15 @@ exports.LoadUtils = () => {
...vcardOptions,
...buttonOptions,
...listOptions,
...botOptions,
...extraOptions
};
// Bot's won't reply if canonicalUrl is set (linking)
if (botOptions) {
delete message.canonicalUrl;
}
await window.Store.SendMessage.addAndSendMsgToChat(chat, message);
return window.Store.Msg.get(newMsgId._serialized);
};
@ -666,7 +543,7 @@ exports.LoadUtils = () => {
window.WWebJS.getContact = async contactId => {
const wid = window.Store.WidFactory.createWid(contactId);
const contact = await window.Store.Contact.find(wid);
const bizProfile = await window.Store.BusinessProfileCollection.fetchBizProfile(wid);
const bizProfile = await window.Store.BusinessProfile.fetchBizProfile(wid);
bizProfile.profileOptions && (contact.businessProfile = bizProfile);
return window.WWebJS.getContactModel(contact);
};
@ -787,9 +664,8 @@ exports.LoadUtils = () => {
};
window.WWebJS.sendChatstate = async (state, chatId) => {
if (window.Store.MDBackend) {
chatId = window.Store.WidFactory.createWid(chatId);
}
switch (state) {
case 'typing':
await window.Store.ChatState.sendChatStateComposing(chatId);
@ -1037,7 +913,7 @@ exports.LoadUtils = () => {
let response;
let result = [];
await window.Store.GroupMetadata.queryAndUpdate(groupWid);
await window.Store.GroupQueryAndUpdate(groupWid);
if (!requesterIds?.length) {
membershipRequests = group.groupMetadata.membershipApprovalRequests._models.map(({ id }) => id);
@ -1131,4 +1007,5 @@ exports.LoadUtils = () => {
if (response.messageSendResult === 'OK') return true;
return false;
};
};

View file

@ -29,11 +29,8 @@ class LocalWebCache extends WebCache {
}
}
async persist(indexHtml) {
// extract version from index (e.g. manifest-2.2206.9.json -> 2.2206.9)
const version = indexHtml.match(/manifest-([\d\\.]+)\.json/)[1];
if(!version) return;
async persist(indexHtml, version) {
// version = (version+'').replace(/[^0-9.]/g,'');
const filePath = path.join(this.path, `${version}.html`);
fs.mkdirSync(this.path, { recursive: true });
fs.writeFileSync(filePath, indexHtml);