diff --git a/example.js b/example.js index 3334e5b6a2..402ce5140b 100644 --- a/example.js +++ b/example.js @@ -6,7 +6,10 @@ const client = new Client({ puppeteer: { // args: ['--proxy-server=proxy-server-that-requires-authentication.example.com'], headless: false, - } + }, + // pairWithPhoneNumber: { + // phoneNumber: '96170100100' // Pair with phone number (format: ) + // } }); // client initialize does not finish at ready now. @@ -16,19 +19,13 @@ client.on('loading_screen', (percent, message) => { console.log('LOADING SCREEN', percent, message); }); -// 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('code', (code) => { + console.log('Pairing code:',code); }); client.on('authenticated', () => { diff --git a/index.d.ts b/index.d.ts index bd75dbf96b..8f72069eb6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -121,10 +121,10 @@ declare namespace WAWebJS { /** * 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 + * @param showNotification - Show notification to pair on phone number. Defaults to `true` * @returns {Promise} - Returns a pairing code in format "ABCDEFGH" */ - requestPairingCode(phoneNumber: string, showNotification = true): Promise + requestPairingCode(phoneNumber: string, showNotification?: boolean): Promise /** Force reset of connection state for the client */ resetState(): Promise @@ -386,6 +386,13 @@ declare namespace WAWebJS { qr: string ) => void): this + /** Emitted when the phone number pairing code is received */ + on(event: 'code', listener: ( + /** pairing code string + * @example `8W2WZ3TS` */ + code: string + ) => void): this + /** Emitted when a call is received */ on(event: 'call', listener: ( /** The call that started */ @@ -485,6 +492,14 @@ declare namespace WAWebJS { ffmpegPath?: string, /** Object with proxy autentication requirements @default: undefined */ proxyAuthentication?: {username: string, password: string} | undefined + /** Phone number pairing configuration. Refer the requestPairingCode function of Client. + * @default + * { + * phoneNumber: "", + * showNotification: true, + * } + */ + pairWithPhoneNumber?: {phoneNumber: string, showNotification?: boolean} } export interface LocalWebCacheOptions { diff --git a/src/Client.js b/src/Client.js index c5a6c3bfd3..fc44e306a8 100644 --- a/src/Client.js +++ b/src/Client.js @@ -87,6 +87,11 @@ class Client extends EventEmitter { this.lastLoggedOut = false; Util.setFfmpegPath(this.options.ffmpegPath); + + /** + * @type {number} - The time in seconds before pairing code expires + */ + this.pairingCodeTTL = null; } /** * Injection logic @@ -94,7 +99,7 @@ class Client extends EventEmitter { */ async inject() { await this.pupPage.waitForFunction('window.Debug?.VERSION != undefined', {timeout: this.options.authTimeoutMs}); - + const pairWithPhoneNumber = this.options.pairWithPhoneNumber; const version = await this.getWWebVersion(); const isCometOrAbove = parseInt(version.split('.')?.[1]) >= 3000; @@ -140,41 +145,55 @@ class Client extends EventEmitter { return; } - // Register qr events - let qrRetries = 0; - await exposeFunctionIfAbsent(this.pupPage, '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(); + // Register qr/code events + if(pairWithPhoneNumber.phoneNumber){ + await exposeFunctionIfAbsent(this.pupPage, 'onCodeReceivedEvent', async (code) => { + /** + * Emitted when a pairing code is received + * @event Client#code + * @param {string} code Code + * @returns {string} Code that was just received + */ + this.emit(Events.CODE_RECEIVED, code); + return code; + }); + this.requestPairingCode(pairWithPhoneNumber.phoneNumber,pairWithPhoneNumber.showNotification); + } else { + let qrRetries = 0; + await exposeFunctionIfAbsent(this.pupPage, '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 - }); + 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 + }); + } } await exposeFunctionIfAbsent(this.pupPage, 'onAuthAppStateChangedEvent', async (state) => { - if (state == 'UNPAIRED_IDLE') { + if (state == 'UNPAIRED_IDLE' && !pairWithPhoneNumber.phoneNumber) { // refresh qr code window.Store.Cmd.refreshQR(); } @@ -343,15 +362,28 @@ class Client extends EventEmitter { /** * 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 + * @param {boolean} [showNotification = true] - 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); + return await this.pupPage.evaluate(async (phoneNumber, showNotification, pairingCodeTTL) => { + const getCode = async () => { + window.AuthStore.PairingCodeLinkUtils.setPairingType('ALT_DEVICE_LINKING'); + await window.AuthStore.PairingCodeLinkUtils.initializeAltDeviceLinking(); + return window.AuthStore.PairingCodeLinkUtils.startAltLinkingFlow(phoneNumber, showNotification); + }; + if (window.codeInterval) { + clearInterval(window.codeInterval); // remove existing interval + } + window.codeInterval = setInterval(async () => { + if (window.AuthStore.AppState.state != 'UNPAIRED' && window.AuthStore.AppState.state != 'UNPAIRED_IDLE') { + clearInterval(window.codeInterval); + return; + } + window.onCodeReceivedEvent(await getCode()); + }, pairingCodeTTL * 1000); + return window.onCodeReceivedEvent(await getCode()); + }, phoneNumber, showNotification, this.pairingCodeTTL); } /** @@ -767,9 +799,23 @@ class Client extends EventEmitter { }); } else { this.pupPage.on('response', async (res) => { + const textContent = await res.text(); + // Get pairing code expiration time in seconds + if(this.pairingCodeTTL == null && this.options.pairWithPhoneNumber.phoneNumber){ + const index = textContent.indexOf('("WAWebAltDeviceLinkingApi",['); + if(index > -1){ + const execRegex = (reg) => { + reg.lastIndex = index; + return reg.exec(textContent); + } + const captureVarName = execRegex(/.codeGenerationTs>(.+?)\)/g); + // Find last occurrence of the variable definition + const captureValue = execRegex(new RegExp(`${captureVarName[1]}=(\\d+)(?!.*${captureVarName[1]}=.+?codeGenerationTs>)`,"g")); + this.pairingCodeTTL = Number(captureValue[1]); + } + } if(res.ok() && res.url() === WhatsWebURL) { - const indexHtml = await res.text(); - this.currentIndexHtml = indexHtml; + this.currentIndexHtml = textContent; } }); } diff --git a/src/util/Constants.js b/src/util/Constants.js index d7badc5791..7556336f6e 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -18,7 +18,11 @@ exports.DefaultOptions = { userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36', ffmpegPath: 'ffmpeg', bypassCSP: false, - proxyAuthentication: undefined + proxyAuthentication: undefined, + pairWithPhoneNumber: { + phoneNumber: '', + showNotification: true, + }, }; /** @@ -60,6 +64,7 @@ exports.Events = { GROUP_MEMBERSHIP_REQUEST: 'group_membership_request', GROUP_UPDATE: 'group_update', QR_RECEIVED: 'qr', + CODE_RECEIVED: 'code', LOADING_SCREEN: 'loading_screen', DISCONNECTED: 'disconnected', STATE_CHANGED: 'change_state',