diff --git a/example.js b/example.js index a3ade0e0aa..74aaf012e5 100644 --- a/example.js +++ b/example.js @@ -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); diff --git a/index.d.ts b/index.d.ts index c192b4aa83..93b4a4e836 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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 @@ -111,6 +115,14 @@ declare namespace WAWebJS { */ muteChat(chatId: string, unmuteDate?: Date): Promise + /** + * 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} - Returns a pairing code in format "ABCDEFGH" + */ + requestPairingCode(phoneNumber: string, showNotification = true): Promise + /** Force reset of connection state for the client */ resetState(): Promise @@ -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 */ diff --git a/index.js b/index.js index 9b7bf85bac..dd991f585c 100644 --- a/index.js +++ b/index.js @@ -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 }; diff --git a/src/Client.js b/src/Client.js index a0ac1a6d20..4825638f13 100644 --- a/src/Client.js +++ b/src/Client.js @@ -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(); - } + 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,549 +338,355 @@ 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; - }`); - - let lastPercent = null, - lastPercentMessage = null; + await this.inject(); - 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 - ); + /** + * 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} - Returns a pairing code in format "ABCDEFGH" + */ + 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); + } - if (progressBar) { - window.loadingScreen( - progressBar.value, - progressMessage.innerText - ); + /** + * Attach event listeners to WA Web + * Private function + * @property {boolean} reinject is this a reinject? + */ + 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)) { + /** + * Emitted when a user joins the chat via invite link or is added by an admin. + * @event Client#group_join + * @param {GroupNotification} notification GroupNotification with more information about the action + */ + this.emit(Events.GROUP_JOIN, notification); + } else if (msg.subtype === 'remove' || msg.subtype === 'leave') { + /** + * Emitted when a user leaves the chat or is removed by an admin. + * @event Client#group_leave + * @param {GroupNotification} notification GroupNotification with more information about the action + */ + this.emit(Events.GROUP_LEAVE, notification); + } else if (msg.subtype === 'promote' || msg.subtype === 'demote') { + /** + * Emitted when a current user is promoted to an admin or demoted to a regular user. + * @event Client#group_admin_changed + * @param {GroupNotification} notification GroupNotification with more information about the action + */ + this.emit(Events.GROUP_ADMIN_CHANGED, notification); + } else if (msg.subtype === 'membership_approval_request') { + /** + * Emitted when some user requested to join the group + * that has the membership approval mode turned on + * @event Client#group_membership_request + * @param {GroupNotification} notification GroupNotification with more information about the action + * @param {string} notification.chatId The group ID the request was made for + * @param {string} notification.author The user ID that made a request + * @param {number} notification.timestamp The timestamp the request was made at + */ + this.emit(Events.GROUP_MEMBERSHIP_REQUEST, notification); + } else { + /** + * Emitted when group settings are updated, such as subject, description or picture. + * @event Client#group_update + * @param {GroupNotification} notification GroupNotification with more information about the action + */ + this.emit(Events.GROUP_UPDATE, notification); } - }); - - 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; } - 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) => { + const message = new Message(this, msg); + /** - * 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(); - } - } - }); + * Emitted when a new message is created, which may include the current user's own messages. + * @event Client#message_create + * @param {Message} message The message that was created + */ + this.emit(Events.MESSAGE_CREATE, message); - await page.evaluate(function (selectors) { - const qr_container = document.querySelector(selectors.QR_CONTAINER); - window.qrChanged(qr_container.dataset.ref); + if (msg.id.fromMe) return; - 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 + /** + * Emitted when a new message is received. + * @event Client#message + * @param {Message} message The message that was received + */ + this.emit(Events.MESSAGE_RECEIVED, message); }); - // 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; - } - - } + let last_message; - 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'); + await this.pupPage.exposeFunction('onChangeMessageTypeEvent', (msg) => { - } - 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$/, ''); + if (msg.type === 'revoked') { + const message = new Message(this, msg); + let revoked_msg; + if (last_message && msg.id.id === last_message.id.id) { + revoked_msg = new Message(this, last_message); + } - while (lOperand.length !== rOperand.length) { - lOperand.length > rOperand.length - ? rOperand = rOperand.concat('0') - : lOperand = lOperand.concat('0'); + /** + * Emitted when a message is deleted for everyone in the chat. + * @event Client#message_revoke_everyone + * @param {Message} message The message that was revoked, in its current state. It will not contain the original message's data. + * @param {?Message} revoked_msg The message that was revoked, before it was revoked. It will contain the message's original data. + * Note that due to the way this data is captured, it may be possible that this param will be undefined. + */ + this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg); } - 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 this.pupPage.exposeFunction('onChangeMessageEvent', (msg) => { - await page.evaluate(ExposeStore, moduleRaid.toString()); - const authEventPayload = await this.authStrategy.getAuthEventPayload(); + if (msg.type !== 'revoked') { + last_message = msg; + } - /** - * Emitted when authentication is successful - * @event Client#authenticated - */ - this.emit(Events.AUTHENTICATED, authEventPayload); + /** + * The event notification that is received when one of + * the group participants changes their phone number. + */ + const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify'; - // Check window.Store Injection - await page.waitForFunction('window.Store != undefined'); + /** + * The event notification that is received when one of + * the contacts changes their phone number. + */ + const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number'; - await page.evaluate(async () => { - // safely unregister service workers - const registrations = await navigator.serviceWorker.getRegistrations(); - for (let registration of registrations) { - registration.unregister(); - } - }); + if (isParticipant || isContact) { + /** @type {GroupNotification} object does not provide enough information about this event, so a @type {Message} object is used. */ + const message = new Message(this, msg); - //Load util functions (serializers, helper functions) - await page.evaluate(LoadUtils); + const newId = isParticipant ? msg.recipients[0] : msg.to; + const oldId = isParticipant ? msg.author : msg.templateParams.find(id => id !== newId); - // Expose client info - /** - * Current connection information - * @type {ClientInfo} - */ - 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 => { - if (msg.type === 'gp2') { - const notification = new GroupNotification(this, msg); - if (['add', 'invite', 'linked_group_join'].includes(msg.subtype)) { /** - * Emitted when a user joins the chat via invite link or is added by an admin. - * @event Client#group_join - * @param {GroupNotification} notification GroupNotification with more information about the action + * Emitted when a contact or a group participant changes their phone number. + * @event Client#contact_changed + * @param {Message} message Message with more information about the event. + * @param {String} oldId The user's id (an old one) who changed their phone number + * and who triggered the notification. + * @param {String} newId The user's new id after the change. + * @param {Boolean} isContact Indicates if a contact or a group participant changed their phone number. */ - this.emit(Events.GROUP_JOIN, notification); - } else if (msg.subtype === 'remove' || msg.subtype === 'leave') { - /** - * Emitted when a user leaves the chat or is removed by an admin. - * @event Client#group_leave - * @param {GroupNotification} notification GroupNotification with more information about the action - */ - this.emit(Events.GROUP_LEAVE, notification); - } else if (msg.subtype === 'promote' || msg.subtype === 'demote') { - /** - * Emitted when a current user is promoted to an admin or demoted to a regular user. - * @event Client#group_admin_changed - * @param {GroupNotification} notification GroupNotification with more information about the action - */ - this.emit(Events.GROUP_ADMIN_CHANGED, notification); - } else if (msg.subtype === 'membership_approval_request') { - /** - * Emitted when some user requested to join the group - * that has the membership approval mode turned on - * @event Client#group_membership_request - * @param {GroupNotification} notification GroupNotification with more information about the action - * @param {string} notification.chatId The group ID the request was made for - * @param {string} notification.author The user ID that made a request - * @param {number} notification.timestamp The timestamp the request was made at - */ - this.emit(Events.GROUP_MEMBERSHIP_REQUEST, notification); - } else { - /** - * Emitted when group settings are updated, such as subject, description or picture. - * @event Client#group_update - * @param {GroupNotification} notification GroupNotification with more information about the action - */ - this.emit(Events.GROUP_UPDATE, notification); + this.emit(Events.CONTACT_CHANGED, message, oldId, newId, isContact); } - return; - } - - const message = new Message(this, msg); - - /** - * Emitted when a new message is created, which may include the current user's own messages. - * @event Client#message_create - * @param {Message} message The message that was created - */ - this.emit(Events.MESSAGE_CREATE, message); - - if (msg.id.fromMe) return; - - /** - * Emitted when a new message is received. - * @event Client#message - * @param {Message} message The message that was received - */ - this.emit(Events.MESSAGE_RECEIVED, message); - }); + }); - let last_message; + await this.pupPage.exposeFunction('onRemoveMessageEvent', (msg) => { - await page.exposeFunction('onChangeMessageTypeEvent', (msg) => { + if (!msg.isNewMsg) return; - if (msg.type === 'revoked') { const message = new Message(this, msg); - let revoked_msg; - if (last_message && msg.id.id === last_message.id.id) { - revoked_msg = new Message(this, last_message); - } /** - * Emitted when a message is deleted for everyone in the chat. - * @event Client#message_revoke_everyone - * @param {Message} message The message that was revoked, in its current state. It will not contain the original message's data. - * @param {?Message} revoked_msg The message that was revoked, before it was revoked. It will contain the message's original data. - * Note that due to the way this data is captured, it may be possible that this param will be undefined. + * Emitted when a message is deleted by the current user. + * @event Client#message_revoke_me + * @param {Message} message The message that was revoked */ - this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg); - } - - }); + this.emit(Events.MESSAGE_REVOKED_ME, message); - await page.exposeFunction('onChangeMessageEvent', (msg) => { - - if (msg.type !== 'revoked') { - last_message = msg; - } - - /** - * The event notification that is received when one of - * the group participants changes their phone number. - */ - const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify'; + }); - /** - * The event notification that is received when one of - * the contacts changes their phone number. - */ - const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number'; + await this.pupPage.exposeFunction('onMessageAckEvent', (msg, ack) => { - if (isParticipant || isContact) { - /** @type {GroupNotification} object does not provide enough information about this event, so a @type {Message} object is used. */ const message = new Message(this, msg); - const newId = isParticipant ? msg.recipients[0] : msg.to; - const oldId = isParticipant ? msg.author : msg.templateParams.find(id => id !== newId); - /** - * Emitted when a contact or a group participant changes their phone number. - * @event Client#contact_changed - * @param {Message} message Message with more information about the event. - * @param {String} oldId The user's id (an old one) who changed their phone number - * and who triggered the notification. - * @param {String} newId The user's new id after the change. - * @param {Boolean} isContact Indicates if a contact or a group participant changed their phone number. + * Emitted when an ack event occurrs on message type. + * @event Client#message_ack + * @param {Message} message The message that was affected + * @param {MessageAck} ack The new ACK value */ - this.emit(Events.CONTACT_CHANGED, message, oldId, newId, isContact); - } - }); - - await page.exposeFunction('onRemoveMessageEvent', (msg) => { - - if (!msg.isNewMsg) return; - - const message = new Message(this, msg); + this.emit(Events.MESSAGE_ACK, message, ack); - /** - * Emitted when a message is deleted by the current user. - * @event Client#message_revoke_me - * @param {Message} message The message that was revoked - */ - this.emit(Events.MESSAGE_REVOKED_ME, message); - - }); - - await page.exposeFunction('onMessageAckEvent', (msg, ack) => { - - const message = new Message(this, msg); + }); - /** - * Emitted when an ack event occurrs on message type. - * @event Client#message_ack - * @param {Message} message The message that was affected - * @param {MessageAck} ack The new ACK value - */ - this.emit(Events.MESSAGE_ACK, message, ack); + await this.pupPage.exposeFunction('onChatUnreadCountEvent', async (data) =>{ + const chat = await this.getChatById(data.id); + + /** + * Emitted when the chat unread count changes + */ + this.emit(Events.UNREAD_COUNT, chat); + }); - }); + await this.pupPage.exposeFunction('onMessageMediaUploadedEvent', (msg) => { - await page.exposeFunction('onChatUnreadCountEvent', async (data) =>{ - const chat = await this.getChatById(data.id); - - /** - * Emitted when the chat unread count changes - */ - this.emit(Events.UNREAD_COUNT, chat); - }); + const message = new Message(this, msg); - await page.exposeFunction('onMessageMediaUploadedEvent', (msg) => { + /** + * Emitted when media has been uploaded for a message sent by the client. + * @event Client#media_uploaded + * @param {Message} message The message with media that was uploaded + */ + this.emit(Events.MEDIA_UPLOADED, message); + }); - const message = new Message(this, msg); + await this.pupPage.exposeFunction('onAppStateChangedEvent', async (state) => { + /** + * Emitted when the connection state changes + * @event Client#change_state + * @param {WAState} state the new connection state + */ + this.emit(Events.STATE_CHANGED, state); - /** - * Emitted when media has been uploaded for a message sent by the client. - * @event Client#media_uploaded - * @param {Message} message The message with media that was uploaded - */ - this.emit(Events.MEDIA_UPLOADED, message); - }); + const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT]; - await page.exposeFunction('onAppStateChangedEvent', async (state) => { + if (this.options.takeoverOnConflict) { + ACCEPTED_STATES.push(WAState.CONFLICT); - /** - * Emitted when the connection state changes - * @event Client#change_state - * @param {WAState} state the new connection state - */ - this.emit(Events.STATE_CHANGED, state); + if (state === WAState.CONFLICT) { + setTimeout(() => { + this.pupPage.evaluate(() => window.Store.AppState.takeover()); + }, this.options.takeoverTimeoutMs); + } + } - const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT]; + if (!ACCEPTED_STATES.includes(state)) { + /** + * Emitted when the client has been disconnected + * @event Client#disconnected + * @param {WAState|"LOGOUT"} reason reason that caused the disconnect + */ + await this.authStrategy.disconnect(); + this.emit(Events.DISCONNECTED, state); + this.destroy(); + } + }); - if (this.options.takeoverOnConflict) { - ACCEPTED_STATES.push(WAState.CONFLICT); + await this.pupPage.exposeFunction('onBatteryStateChangedEvent', (state) => { + const { battery, plugged } = state; - if (state === WAState.CONFLICT) { - setTimeout(() => { - this.pupPage.evaluate(() => window.Store.AppState.takeover()); - }, this.options.takeoverTimeoutMs); - } - } + if (battery === undefined) return; - if (!ACCEPTED_STATES.includes(state)) { /** - * Emitted when the client has been disconnected - * @event Client#disconnected - * @param {WAState|"NAVIGATION"} reason reason that caused the disconnect + * Emitted when the battery percentage for the attached device changes. Will not be sent if using multi-device. + * @event Client#change_battery + * @param {object} batteryInfo + * @param {number} batteryInfo.battery - The current battery percentage + * @param {boolean} batteryInfo.plugged - Indicates if the phone is plugged in (true) or not (false) + * @deprecated */ - await this.authStrategy.disconnect(); - this.emit(Events.DISCONNECTED, state); - this.destroy(); - } - }); + this.emit(Events.BATTERY_CHANGED, { battery, plugged }); + }); - await page.exposeFunction('onBatteryStateChangedEvent', (state) => { - const { battery, plugged } = state; + await this.pupPage.exposeFunction('onIncomingCall', (call) => { + /** + * Emitted when a call is received + * @event Client#incoming_call + * @param {object} call + * @param {number} call.id - Call id + * @param {string} call.peerJid - Who called + * @param {boolean} call.isVideo - if is video + * @param {boolean} call.isGroup - if is group + * @param {boolean} call.canHandleLocally - if we can handle in waweb + * @param {boolean} call.outgoing - if is outgoing + * @param {boolean} call.webClientShouldHandle - If Waweb should handle + * @param {object} call.participants - Participants + */ + const cll = new Call(this, call); + this.emit(Events.INCOMING_CALL, cll); + }); - if (battery === undefined) return; + await this.pupPage.exposeFunction('onReaction', (reactions) => { + for (const reaction of reactions) { + /** + * Emitted when a reaction is sent, received, updated or removed + * @event Client#message_reaction + * @param {object} reaction + * @param {object} reaction.id - Reaction id + * @param {number} reaction.orphan - Orphan + * @param {?string} reaction.orphanReason - Orphan reason + * @param {number} reaction.timestamp - Timestamp + * @param {string} reaction.reaction - Reaction + * @param {boolean} reaction.read - Read + * @param {object} reaction.msgId - Parent message id + * @param {string} reaction.senderId - Sender id + * @param {?number} reaction.ack - Ack + */ - /** - * Emitted when the battery percentage for the attached device changes. Will not be sent if using multi-device. - * @event Client#change_battery - * @param {object} batteryInfo - * @param {number} batteryInfo.battery - The current battery percentage - * @param {boolean} batteryInfo.plugged - Indicates if the phone is plugged in (true) or not (false) - * @deprecated - */ - this.emit(Events.BATTERY_CHANGED, { battery, plugged }); - }); + this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction)); + } + }); - await page.exposeFunction('onIncomingCall', (call) => { - /** - * Emitted when a call is received - * @event Client#incoming_call - * @param {object} call - * @param {number} call.id - Call id - * @param {string} call.peerJid - Who called - * @param {boolean} call.isVideo - if is video - * @param {boolean} call.isGroup - if is group - * @param {boolean} call.canHandleLocally - if we can handle in waweb - * @param {boolean} call.outgoing - if is outgoing - * @param {boolean} call.webClientShouldHandle - If Waweb should handle - * @param {object} call.participants - Participants - */ - const cll = new Call(this, call); - this.emit(Events.INCOMING_CALL, cll); - }); + await this.pupPage.exposeFunction('onRemoveChatEvent', async (chat) => { + const _chat = await this.getChatById(chat.id); - await page.exposeFunction('onReaction', (reactions) => { - for (const reaction of reactions) { /** - * Emitted when a reaction is sent, received, updated or removed - * @event Client#message_reaction - * @param {object} reaction - * @param {object} reaction.id - Reaction id - * @param {number} reaction.orphan - Orphan - * @param {?string} reaction.orphanReason - Orphan reason - * @param {number} reaction.timestamp - Timestamp - * @param {string} reaction.reaction - Reaction - * @param {boolean} reaction.read - Read - * @param {object} reaction.msgId - Parent message id - * @param {string} reaction.senderId - Sender id - * @param {?number} reaction.ack - Ack + * Emitted when a chat is removed + * @event Client#chat_removed + * @param {Chat} chat */ - - this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction)); - } - }); - - await page.exposeFunction('onRemoveChatEvent', async (chat) => { - const _chat = await this.getChatById(chat.id); - - /** - * Emitted when a chat is removed - * @event Client#chat_removed - * @param {Chat} chat - */ - this.emit(Events.CHAT_REMOVED, _chat); - }); - - await page.exposeFunction('onArchiveChatEvent', async (chat, currState, prevState) => { - const _chat = await this.getChatById(chat.id); + this.emit(Events.CHAT_REMOVED, _chat); + }); - /** - * Emitted when a chat is archived/unarchived - * @event Client#chat_archived - * @param {Chat} chat - * @param {boolean} currState - * @param {boolean} prevState - */ - this.emit(Events.CHAT_ARCHIVED, _chat, currState, prevState); - }); + await this.pupPage.exposeFunction('onArchiveChatEvent', async (chat, currState, prevState) => { + const _chat = await this.getChatById(chat.id); + + /** + * Emitted when a chat is archived/unarchived + * @event Client#chat_archived + * @param {Chat} chat + * @param {boolean} currState + * @param {boolean} prevState + */ + this.emit(Events.CHAT_ARCHIVED, _chat, currState, prevState); + }); - await page.exposeFunction('onEditMessageEvent', (msg, newBody, prevBody) => { - - if(msg.type === 'revoked'){ - return; - } - /** - * Emitted when messages are edited - * @event Client#message_edit - * @param {Message} message - * @param {string} newBody - * @param {string} prevBody - */ - this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody); - }); - - await page.exposeFunction('onAddMessageCiphertextEvent', msg => { + await this.pupPage.exposeFunction('onEditMessageEvent', (msg, newBody, prevBody) => { + + if(msg.type === 'revoked'){ + return; + } + /** + * Emitted when messages are edited + * @event Client#message_edit + * @param {Message} message + * @param {string} newBody + * @param {string} prevBody + */ + this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody); + }); - /** - * Emitted when messages are edited - * @event Client#message_ciphertext - * @param {Message} message - */ - this.emit(Events.MESSAGE_CIPHERTEXT, new Message(this, msg)); - }); + await this.pupPage.exposeFunction('onAddMessageCiphertextEvent', msg => { + + /** + * Emitted when messages are edited + * @event Client#message_ciphertext + * @param {Message} message + */ + 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,24 +757,7 @@ 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() { const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache; @@ -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; - } + await window.Store.Settings.setPushname(displayName); + return true; }, 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, diff --git a/src/authStrategies/LegacySessionAuth.js b/src/authStrategies/LegacySessionAuth.js deleted file mode 100644 index dd09f7b5e6..0000000000 --- a/src/authStrategies/LegacySessionAuth.js +++ /dev/null @@ -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; diff --git a/src/authStrategies/LocalAuth.js b/src/authStrategies/LocalAuth.js index 8309c3d0d5..543a6b9ba1 100644 --- a/src/authStrategies/LocalAuth.js +++ b/src/authStrategies/LocalAuth.js @@ -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); + }); } } diff --git a/src/structures/GroupChat.js b/src/structures/GroupChat.js index 4ae5b19559..04de9bdf0b 100644 --- a/src/structures/GroupChat.js +++ b/src/structures/GroupChat.js @@ -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; } /** diff --git a/src/structures/Message.js b/src/structures/Message.js index b61563af68..ea633126f1 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -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,7 +505,11 @@ class Message extends Base { const canRevoke = window.Store.MsgActionChecks.canSenderRevokeMsg(msg) || window.Store.MsgActionChecks.canAdminRevokeMsg(msg); if (everyone && canRevoke) { - return window.Store.Cmd.sendRevokeMsgs(chat, [msg], { clearMedia: true, type: msg.id.fromMe ? 'Sender' : 'Admin' }); + 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); diff --git a/src/util/Injected/AuthStore/AuthStore.js b/src/util/Injected/AuthStore/AuthStore.js new file mode 100644 index 0000000000..c797aa0544 --- /dev/null +++ b/src/util/Injected/AuthStore/AuthStore.js @@ -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'), + }; +}; \ No newline at end of file diff --git a/src/util/Injected/AuthStore/LegacyAuthStore.js b/src/util/Injected/AuthStore/LegacyAuthStore.js new file mode 100644 index 0000000000..c3016f7d17 --- /dev/null +++ b/src/util/Injected/AuthStore/LegacyAuthStore.js @@ -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], + }; +}; \ No newline at end of file diff --git a/src/util/Injected/LegacyStore.js b/src/util/Injected/LegacyStore.js new file mode 100644 index 0000000000..e9584d9b7a --- /dev/null +++ b/src/util/Injected/LegacyStore.js @@ -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); }); +}; \ No newline at end of file diff --git a/src/util/Injected/Store.js b/src/util/Injected/Store.js new file mode 100644 index 0000000000..873f09c2ab --- /dev/null +++ b/src/util/Injected/Store.js @@ -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); }); + +}; diff --git a/src/util/Injected.js b/src/util/Injected/Utils.js similarity index 79% rename from src/util/Injected.js rename to src/util/Injected/Utils.js index c0df187fc0..40d243d671 100644 --- a/src/util/Injected.js +++ b/src/util/Injected/Utils.js @@ -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; - } +exports.LoadUtils = () => { + window.WWebJS = {}; - /** - * 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 - */ + window.WWebJS.forwardMessage = async (chatId, msgId) => { + let msg = window.Store.Msg.get(msgId); + let chat = window.Store.Chat.get(chatId); - /** - * 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; + if (window.compareWwebVersions(window.Debug.VERSION, '>', '2.3000.0')) { + return window.Store.ForwardUtils.forwardMessagesToChats([msg], [chat], false); + } else { + return chat.forwardMessages([msg]); + } }; - 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.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,8 +241,14 @@ 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); - } + 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; }; + }; diff --git a/src/webCache/LocalWebCache.js b/src/webCache/LocalWebCache.js index a377f559b5..9c429683f8 100644 --- a/src/webCache/LocalWebCache.js +++ b/src/webCache/LocalWebCache.js @@ -29,15 +29,12 @@ 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); } } -module.exports = LocalWebCache; \ No newline at end of file +module.exports = LocalWebCache;