feat: Switch between QR or phone number pairing method (#3180)

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

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

* chore(webpack-exodus): eslint

* fix(webpack-exodus): Fixed Store

* fix(webpack-exodus): slight store fix

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

* chore: eslint fix

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

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

* fix(webpack-exodus): Fix order queries

* fix(webpack-exodus): Bad export name

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

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

* fix(webpack-exodus): 2.24 compatibility

* revert testing options

* chore(webpack-exodus): ESLint

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

* style: fix broken link in readme file

* fix(webpack-exodus): Fix forwarding messages

* fix(webpack-exodus): finish forwarding function

* Making the ESLint god happy

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

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

* Prevent breaking v2.2x

* refactor: make it more readable

---------

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

* Sanitize, improves and fixes

* make ESList god happy

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

* fix getInviteCode for group (#3007)

* move changes to proper getInviteCode method.

* style fix

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

* refactor: handle promise properly

---------

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

* fixes

* Update LocalWebCache.js

* fix delete on 2.3000 (#3048)

* fix delete on 2.3000

* fix compareWwebVersions

* fix: use proper comparison operator

---------

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

* Implement bot invoking capabilities (#3009)

* Implement bot invoking capabilities

* Update index.d.ts

* Eslint

* Webpack exodus fix message_reaction event (#3099)

* exposed WAWebAddonReactionTableMode

* capture reaction using WAWebAddonReactionTableMode

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

* message_reaction on example.js (#3102)

* handle message_reaction event on example.js

* log wweb version on ready

* fix reactions on pvt chats (#3104)

* New group function

* new group settings

* Update Client.js

* Switch pairing method between QR and phone number

* Remove existing pairing code interval

* Modify example.js

* Update docs

* Small improvements in inject function

* Fix ESLint

* example.js correction

* docs: fix docs for 'requestPairingCode' method

Co-authored-by: Leandro Matilla <30844484+wis-dev@users.noreply.github.com>

* Update Utils.js getChatModel because Meta chat object affected Group Chats

* Ajust to set isGroup as false as default

* Remove the need for custom interval

* Fix typo

* Fix intermittent response bug #9864

Co-authored-by: Mob Code 100 <66469454+MobCode100@users.noreply.github.com>

* fix: eslint fix

* Revert "Remove the need for custom interval"

This reverts commit c5d5e5626684742cadbf58f28739c1c2c9dcc582.

* Add wait for PairingCodeLinkUtils on requestPairingCode

* Remove unused vars WhatsWebStaticURL

Co-authored-by: Mob Code 100 <66469454+MobCode100@users.noreply.github.com>

* style: format code

---------

Co-authored-by: Rajeh Taher <rajeh@reforward.dev>
Co-authored-by: alechkos <93551621+alechkos@users.noreply.github.com>
Co-authored-by: tuyuribr <45042245+tuyuribr@users.noreply.github.com>
Co-authored-by: Seow Zhen Jun <56989187+seowzhenjun0126@users.noreply.github.com>
Co-authored-by: Sven <sven2neumann@gmail.com>
Co-authored-by: Sven Neumann <s.neumann@websix.de>
Co-authored-by: Jorge Rocha Gualtieri <jorge@jrg.com.br>
Co-authored-by: Mateus Mercer <accounts@matbm.net>
Co-authored-by: Leandro Matilla <30844484+wis-dev@users.noreply.github.com>
Co-authored-by: BenyFilho <168232825+BenyFilho@users.noreply.github.com>
Co-authored-by: Shir Serlui <70711723+shirser121@users.noreply.github.com>
This commit is contained in:
Mob Code 100 2025-08-09 05:37:25 +08:00 • committed by GitHub
commit b5adf47664
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 103 additions and 50 deletions

View file

@ -17,7 +17,12 @@ 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: <COUNTRY_CODE><PHONE_NUMBER>)
// showNotification: true,
// intervalMs: 180000 // Time to renew pairing code in milliseconds, defaults to 3 minutes
// }
});
// client initialize does not finish at ready now.
@ -27,19 +32,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', () => {

21
index.d.ts vendored
View file

@ -160,10 +160,11 @@ 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`
* @param intervalMs - The interval in milliseconds on how frequent to generate pairing code (WhatsApp default to 3 minutes). Defaults to `180000`
* @returns {Promise<string>} - Returns a pairing code in format "ABCDEFGH"
*/
requestPairingCode(phoneNumber: string, showNotification?: boolean): Promise<string>
requestPairingCode(phoneNumber: string, showNotification?: boolean, intervalMs?: number): Promise<string>
/** Force reset of connection state for the client */
resetState(): Promise<void>
@ -460,6 +461,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 */
@ -569,6 +577,15 @@ declare namespace WAWebJS {
browserName?: 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,
* intervalMs: 180000,
* }
*/
pairWithPhoneNumber?: {phoneNumber: string, showNotification?: boolean, intervalMs?: number}
}
export interface LocalWebCacheOptions {

View file

@ -96,8 +96,8 @@ class Client extends EventEmitter {
*/
async inject() {
await this.pupPage.waitForFunction('window.Debug?.VERSION != undefined', {timeout: this.options.authTimeoutMs});
await this.setDeviceName(this.options.deviceName, this.options.browserName);
const pairWithPhoneNumber = this.options.pairWithPhoneNumber;
const version = await this.getWWebVersion();
const isCometOrAbove = parseInt(version.split('.')?.[1]) >= 3000;
@ -143,41 +143,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, pairWithPhoneNumber.intervalMs);
} 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;
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
});
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();
}
@ -346,15 +360,32 @@ 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
* @param {number} [intervalMs = 180000] - The interval in milliseconds on how frequent to generate pairing code (WhatsApp default to 3 minutes)
* @returns {Promise<string>} - 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);
async requestPairingCode(phoneNumber, showNotification = true, intervalMs = 180000) {
return await this.pupPage.evaluate(async (phoneNumber, showNotification, intervalMs) => {
const getCode = async () => {
while (!window.AuthStore.PairingCodeLinkUtils) {
await new Promise(resolve => setTimeout(resolve, 250));
}
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());
}, intervalMs);
return window.onCodeReceivedEvent(await getCode());
}, phoneNumber, showNotification, intervalMs);
}
/**

View file

@ -18,7 +18,12 @@ 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,
intervalMs: 180000,
},
};
/**
@ -60,6 +65,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',