whatsapp-web.js 1.24.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Client.js CHANGED
@@ -7,12 +7,15 @@ const moduleRaid = require('@pedroslopez/moduleraid/moduleraid');
7
7
  const Util = require('./util/Util');
8
8
  const InterfaceController = require('./util/InterfaceController');
9
9
  const { WhatsWebURL, DefaultOptions, Events, WAState } = require('./util/Constants');
10
- const { ExposeStore, LoadUtils } = require('./util/Injected');
10
+ const { ExposeAuthStore } = require('./util/Injected/AuthStore/AuthStore');
11
+ const { ExposeStore } = require('./util/Injected/Store');
12
+ const { ExposeLegacyAuthStore } = require('./util/Injected/AuthStore/LegacyAuthStore');
13
+ const { ExposeLegacyStore } = require('./util/Injected/LegacyStore');
14
+ const { LoadUtils } = require('./util/Injected/Utils');
11
15
  const ChatFactory = require('./factories/ChatFactory');
12
16
  const ContactFactory = require('./factories/ContactFactory');
13
17
  const WebCacheFactory = require('./webCache/WebCacheFactory');
14
18
  const { ClientInfo, Message, MessageMedia, Contact, Location, Poll, PollVote, GroupNotification, Label, Call, Buttons, List, Reaction } = require('./structures');
15
- const LegacySessionAuth = require('./authStrategies/LegacySessionAuth');
16
19
  const NoAuth = require('./authStrategies/NoAuth');
17
20
 
18
21
  /**
@@ -63,37 +66,227 @@ class Client extends EventEmitter {
63
66
  this.options = Util.mergeDefault(DefaultOptions, options);
64
67
 
65
68
  if(!this.options.authStrategy) {
66
- if(Object.prototype.hasOwnProperty.call(this.options, 'session')) {
67
- process.emitWarning(
68
- 'options.session is deprecated and will be removed in a future release due to incompatibility with multi-device. ' +
69
- '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).',
70
- 'DeprecationWarning'
71
- );
72
-
73
- this.authStrategy = new LegacySessionAuth({
74
- session: this.options.session,
75
- restartOnAuthFail: this.options.restartOnAuthFail
76
- });
77
- } else {
78
- this.authStrategy = new NoAuth();
79
- }
69
+ this.authStrategy = new NoAuth();
80
70
  } else {
81
71
  this.authStrategy = this.options.authStrategy;
82
72
  }
83
73
 
84
74
  this.authStrategy.setup(this);
85
75
 
76
+ /**
77
+ * @type {puppeteer.Browser}
78
+ */
86
79
  this.pupBrowser = null;
80
+ /**
81
+ * @type {puppeteer.Page}
82
+ */
87
83
  this.pupPage = null;
88
84
 
85
+ this.currentIndexHtml = null;
86
+ this.lastLoggedOut = false;
87
+
89
88
  Util.setFfmpegPath(this.options.ffmpegPath);
90
89
  }
90
+ /**
91
+ * Injection logic
92
+ * Private function
93
+ * @property {boolean} reinject is this a reinject?
94
+ */
95
+ async inject(reinject = false) {
96
+ await this.pupPage.waitForFunction('window.Debug?.VERSION != undefined', {timeout: this.options.authTimeoutMs});
97
+
98
+ const version = await this.getWWebVersion();
99
+ const isCometOrAbove = parseInt(version.split('.')?.[1]) >= 3000;
100
+
101
+ if (isCometOrAbove) {
102
+ await this.pupPage.evaluate(ExposeAuthStore);
103
+ } else {
104
+ await this.pupPage.evaluate(ExposeLegacyAuthStore, moduleRaid.toString());
105
+ }
106
+
107
+ const needAuthentication = await this.pupPage.evaluate(async () => {
108
+ let state = window.AuthStore.AppState.state;
109
+
110
+ if (state === 'OPENING' || state === 'UNLAUNCHED' || state === 'PAIRING') {
111
+ // wait till state changes
112
+ await new Promise(r => {
113
+ window.AuthStore.AppState.on('change:state', function waitTillInit(_AppState, state) {
114
+ if (state !== 'OPENING' && state !== 'UNLAUNCHED' && state !== 'PAIRING') {
115
+ window.AuthStore.AppState.off('change:state', waitTillInit);
116
+ r();
117
+ }
118
+ });
119
+ });
120
+ }
121
+ state = window.AuthStore.AppState.state;
122
+ return state == 'UNPAIRED' || state == 'UNPAIRED_IDLE';
123
+ });
124
+
125
+ if (needAuthentication) {
126
+ const { failed, failureEventPayload, restart } = await this.authStrategy.onAuthenticationNeeded();
127
+
128
+ if(failed) {
129
+ /**
130
+ * Emitted when there has been an error while trying to restore an existing session
131
+ * @event Client#auth_failure
132
+ * @param {string} message
133
+ */
134
+ this.emit(Events.AUTHENTICATION_FAILURE, failureEventPayload);
135
+ await this.destroy();
136
+ if (restart) {
137
+ // session restore failed so try again but without session to force new authentication
138
+ return this.initialize();
139
+ }
140
+ return;
141
+ }
142
+
143
+ // Register qr events
144
+ let qrRetries = 0;
145
+ const injected = await this.pupPage.evaluate(() => {
146
+ return typeof window.onQRChangedEvent !== 'undefined';
147
+ });
148
+ if (!injected) {
149
+ await this.pupPage.exposeFunction('onQRChangedEvent', async (qr) => {
150
+ /**
151
+ * Emitted when a QR code is received
152
+ * @event Client#qr
153
+ * @param {string} qr QR Code
154
+ */
155
+ this.emit(Events.QR_RECEIVED, qr);
156
+ if (this.options.qrMaxRetries > 0) {
157
+ qrRetries++;
158
+ if (qrRetries > this.options.qrMaxRetries) {
159
+ this.emit(Events.DISCONNECTED, 'Max qrcode retries reached');
160
+ await this.destroy();
161
+ }
162
+ }
163
+ });
164
+ }
165
+
166
+
167
+ await this.pupPage.evaluate(async () => {
168
+ const registrationInfo = await window.AuthStore.RegistrationUtils.waSignalStore.getRegistrationInfo();
169
+ const noiseKeyPair = await window.AuthStore.RegistrationUtils.waNoiseInfo.get();
170
+ const staticKeyB64 = window.AuthStore.Base64Tools.encodeB64(noiseKeyPair.staticKeyPair.pubKey);
171
+ const identityKeyB64 = window.AuthStore.Base64Tools.encodeB64(registrationInfo.identityKeyPair.pubKey);
172
+ const advSecretKey = await window.AuthStore.RegistrationUtils.getADVSecretKey();
173
+ const platform = window.AuthStore.RegistrationUtils.DEVICE_PLATFORM;
174
+ const getQR = (ref) => ref + ',' + staticKeyB64 + ',' + identityKeyB64 + ',' + advSecretKey + ',' + platform;
175
+
176
+ window.onQRChangedEvent(getQR(window.AuthStore.Conn.ref)); // initial qr
177
+ window.AuthStore.Conn.on('change:ref', (_, ref) => { window.onQRChangedEvent(getQR(ref)); }); // future QR changes
178
+ });
179
+ }
180
+
181
+ if (!reinject) {
182
+ await this.pupPage.exposeFunction('onAuthAppStateChangedEvent', async (state) => {
183
+ if (state == 'UNPAIRED_IDLE') {
184
+ // refresh qr code
185
+ window.Store.Cmd.refreshQR();
186
+ }
187
+ });
188
+
189
+ await this.pupPage.exposeFunction('onAppStateHasSyncedEvent', async () => {
190
+ const authEventPayload = await this.authStrategy.getAuthEventPayload();
191
+ /**
192
+ * Emitted when authentication is successful
193
+ * @event Client#authenticated
194
+ */
195
+ this.emit(Events.AUTHENTICATED, authEventPayload);
196
+
197
+ const injected = await this.pupPage.evaluate(async () => {
198
+ return typeof window.Store !== 'undefined' && typeof window.WWebJS !== 'undefined';
199
+ });
200
+
201
+ if (!injected) {
202
+ if (this.options.webVersionCache.type === 'local' && this.currentIndexHtml) {
203
+ const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache;
204
+ const webCache = WebCacheFactory.createWebCache(webCacheType, webCacheOptions);
205
+
206
+ await webCache.persist(this.currentIndexHtml, version);
207
+ }
208
+
209
+ if (isCometOrAbove) {
210
+ await this.pupPage.evaluate(ExposeStore);
211
+ } else {
212
+ // make sure all modules are ready before injection
213
+ // 2 second delay after authentication makes sense and does not need to be made dyanmic or removed
214
+ await new Promise(r => setTimeout(r, 2000));
215
+ await this.pupPage.evaluate(ExposeLegacyStore);
216
+ }
217
+
218
+ // Check window.Store Injection
219
+ await this.pupPage.waitForFunction('window.Store != undefined');
220
+
221
+ /**
222
+ * Current connection information
223
+ * @type {ClientInfo}
224
+ */
225
+ this.info = new ClientInfo(this, await this.pupPage.evaluate(() => {
226
+ return { ...window.Store.Conn.serialize(), wid: window.Store.User.getMeUser() };
227
+ }));
228
+
229
+ this.interface = new InterfaceController(this);
230
+
231
+ //Load util functions (serializers, helper functions)
232
+ await this.pupPage.evaluate(LoadUtils);
233
+
234
+ await this.attachEventListeners(reinject);
235
+ reinject = true;
236
+ }
237
+ /**
238
+ * Emitted when the client has initialized and is ready to receive messages.
239
+ * @event Client#ready
240
+ */
241
+ this.emit(Events.READY);
242
+ this.authStrategy.afterAuthReady();
243
+ });
244
+ let lastPercent = null;
245
+ await this.pupPage.exposeFunction('onOfflineProgressUpdateEvent', async (percent) => {
246
+ if (lastPercent !== percent) {
247
+ lastPercent = percent;
248
+ this.emit(Events.LOADING_SCREEN, percent, 'WhatsApp'); // Message is hardcoded as "WhatsApp" for now
249
+ }
250
+ });
251
+ }
252
+ const logoutCatchInjected = await this.pupPage.evaluate(() => {
253
+ return typeof window.onLogoutEvent !== 'undefined';
254
+ });
255
+ if (!logoutCatchInjected) {
256
+ await this.pupPage.exposeFunction('onLogoutEvent', async () => {
257
+ this.lastLoggedOut = true;
258
+ await this.pupPage.waitForNavigation({waitUntil: 'load', timeout: 5000}).catch((_) => _);
259
+ });
260
+ }
261
+ await this.pupPage.evaluate(() => {
262
+ window.AuthStore.AppState.on('change:state', (_AppState, state) => { window.onAuthAppStateChangedEvent(state); });
263
+ window.AuthStore.AppState.on('change:hasSynced', () => { window.onAppStateHasSyncedEvent(); });
264
+ window.AuthStore.Cmd.on('offline_progress_update', () => {
265
+ window.onOfflineProgressUpdateEvent(window.AuthStore.OfflineMessageHandler.getOfflineDeliveryProgress());
266
+ });
267
+ window.AuthStore.Cmd.on('logout', async () => {
268
+ await window.onLogoutEvent();
269
+ });
270
+ });
271
+ }
91
272
 
92
273
  /**
93
274
  * Sets up events and requirements, kicks off authentication request
94
275
  */
95
276
  async initialize() {
96
- let [browser, page] = [null, null];
277
+
278
+ let
279
+ /**
280
+ * @type {puppeteer.Browser}
281
+ */
282
+ browser,
283
+ /**
284
+ * @type {puppeteer.Page}
285
+ */
286
+ page;
287
+
288
+ browser = null;
289
+ page = null;
97
290
 
98
291
  await this.authStrategy.beforeBrowserInitialized();
99
292
 
@@ -126,9 +319,11 @@ class Client extends EventEmitter {
126
319
  await this.authStrategy.afterBrowserInitialized();
127
320
  await this.initWebVersionCache();
128
321
 
129
- // ocVesion (isOfficialClient patch)
322
+ // ocVersion (isOfficialClient patch)
323
+ // remove after 2.3000.x hard release
130
324
  await page.evaluateOnNewDocument(() => {
131
325
  const originalError = Error;
326
+ window.originalError = originalError;
132
327
  //eslint-disable-next-line no-global-assign
133
328
  Error = function (message) {
134
329
  const error = new originalError(message);
@@ -144,549 +339,355 @@ class Client extends EventEmitter {
144
339
  referer: 'https://whatsapp.com/'
145
340
  });
146
341
 
147
- await page.evaluate(`function getElementByXpath(path) {
148
- return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
149
- }`);
342
+ await this.inject();
150
343
 
151
- let lastPercent = null,
152
- lastPercentMessage = null;
153
-
154
- await page.exposeFunction('loadingScreen', async (percent, message) => {
155
- if (lastPercent !== percent || lastPercentMessage !== message) {
156
- this.emit(Events.LOADING_SCREEN, percent, message);
157
- lastPercent = percent;
158
- lastPercentMessage = message;
344
+ this.pupPage.on('framenavigated', async (frame) => {
345
+ if(frame.url().includes('post_logout=1') || this.lastLoggedOut) {
346
+ this.emit(Events.DISCONNECTED, 'LOGOUT');
347
+ await this.authStrategy.logout();
348
+ await this.authStrategy.beforeBrowserInitialized();
349
+ await this.authStrategy.afterBrowserInitialized();
350
+ this.lastLoggedOut = false;
159
351
  }
352
+ await this.inject(true);
160
353
  });
354
+ }
161
355
 
162
- await page.evaluate(
163
- async function (selectors) {
164
- var observer = new MutationObserver(function () {
165
- let progressBar = window.getElementByXpath(
166
- selectors.PROGRESS
167
- );
168
- let progressMessage = window.getElementByXpath(
169
- selectors.PROGRESS_MESSAGE
170
- );
356
+ /**
357
+ * Request authentication via pairing code instead of QR code
358
+ * @param {string} phoneNumber - Phone number in international, symbol-free format (e.g. 12025550108 for US, 551155501234 for Brazil)
359
+ * @param {boolean} showNotification - Show notification to pair on phone number
360
+ * @returns {Promise<string>} - Returns a pairing code in format "ABCDEFGH"
361
+ */
362
+ async requestPairingCode(phoneNumber, showNotification = true) {
363
+ return await this.pupPage.evaluate(async (phoneNumber, showNotification) => {
364
+ window.AuthStore.PairingCodeLinkUtils.setPairingType('ALT_DEVICE_LINKING');
365
+ await window.AuthStore.PairingCodeLinkUtils.initializeAltDeviceLinking();
366
+ return window.AuthStore.PairingCodeLinkUtils.startAltLinkingFlow(phoneNumber, showNotification);
367
+ }, phoneNumber, showNotification);
368
+ }
171
369
 
172
- if (progressBar) {
173
- window.loadingScreen(
174
- progressBar.value,
175
- progressMessage.innerText
176
- );
370
+ /**
371
+ * Attach event listeners to WA Web
372
+ * Private function
373
+ * @property {boolean} reinject is this a reinject?
374
+ */
375
+ async attachEventListeners(reinject = false) {
376
+ if (!reinject) {
377
+ await this.pupPage.exposeFunction('onAddMessageEvent', msg => {
378
+ if (msg.type === 'gp2') {
379
+ const notification = new GroupNotification(this, msg);
380
+ if (['add', 'invite', 'linked_group_join'].includes(msg.subtype)) {
381
+ /**
382
+ * Emitted when a user joins the chat via invite link or is added by an admin.
383
+ * @event Client#group_join
384
+ * @param {GroupNotification} notification GroupNotification with more information about the action
385
+ */
386
+ this.emit(Events.GROUP_JOIN, notification);
387
+ } else if (msg.subtype === 'remove' || msg.subtype === 'leave') {
388
+ /**
389
+ * Emitted when a user leaves the chat or is removed by an admin.
390
+ * @event Client#group_leave
391
+ * @param {GroupNotification} notification GroupNotification with more information about the action
392
+ */
393
+ this.emit(Events.GROUP_LEAVE, notification);
394
+ } else if (msg.subtype === 'promote' || msg.subtype === 'demote') {
395
+ /**
396
+ * Emitted when a current user is promoted to an admin or demoted to a regular user.
397
+ * @event Client#group_admin_changed
398
+ * @param {GroupNotification} notification GroupNotification with more information about the action
399
+ */
400
+ this.emit(Events.GROUP_ADMIN_CHANGED, notification);
401
+ } else if (msg.subtype === 'membership_approval_request') {
402
+ /**
403
+ * Emitted when some user requested to join the group
404
+ * that has the membership approval mode turned on
405
+ * @event Client#group_membership_request
406
+ * @param {GroupNotification} notification GroupNotification with more information about the action
407
+ * @param {string} notification.chatId The group ID the request was made for
408
+ * @param {string} notification.author The user ID that made a request
409
+ * @param {number} notification.timestamp The timestamp the request was made at
410
+ */
411
+ this.emit(Events.GROUP_MEMBERSHIP_REQUEST, notification);
412
+ } else {
413
+ /**
414
+ * Emitted when group settings are updated, such as subject, description or picture.
415
+ * @event Client#group_update
416
+ * @param {GroupNotification} notification GroupNotification with more information about the action
417
+ */
418
+ this.emit(Events.GROUP_UPDATE, notification);
177
419
  }
178
- });
179
-
180
- observer.observe(document, {
181
- attributes: true,
182
- childList: true,
183
- characterData: true,
184
- subtree: true,
185
- });
186
- },
187
- {
188
- PROGRESS: '//*[@id=\'app\']/div/div/div[2]/progress',
189
- PROGRESS_MESSAGE: '//*[@id=\'app\']/div/div/div[3]',
190
- }
191
- );
192
-
193
- const INTRO_IMG_SELECTOR = '[data-icon=\'search\']';
194
- const INTRO_QRCODE_SELECTOR = 'div[data-ref] canvas';
195
-
196
- // Checks which selector appears first
197
- const needAuthentication = await Promise.race([
198
- new Promise(resolve => {
199
- page.waitForSelector(INTRO_IMG_SELECTOR, { timeout: this.options.authTimeoutMs })
200
- .then(() => resolve(false))
201
- .catch((err) => resolve(err));
202
- }),
203
- new Promise(resolve => {
204
- page.waitForSelector(INTRO_QRCODE_SELECTOR, { timeout: this.options.authTimeoutMs })
205
- .then(() => resolve(true))
206
- .catch((err) => resolve(err));
207
- })
208
- ]);
209
-
210
- // Checks if an error occurred on the first found selector. The second will be discarded and ignored by .race;
211
- if (needAuthentication instanceof Error) throw needAuthentication;
212
-
213
- // Scan-qrcode selector was found. Needs authentication
214
- if (needAuthentication) {
215
- const { failed, failureEventPayload, restart } = await this.authStrategy.onAuthenticationNeeded();
216
- if(failed) {
217
- /**
218
- * Emitted when there has been an error while trying to restore an existing session
219
- * @event Client#auth_failure
220
- * @param {string} message
221
- */
222
- this.emit(Events.AUTHENTICATION_FAILURE, failureEventPayload);
223
- await this.destroy();
224
- if (restart) {
225
- // session restore failed so try again but without session to force new authentication
226
- return this.initialize();
420
+ return;
227
421
  }
228
- return;
229
- }
230
422
 
231
- const QR_CONTAINER = 'div[data-ref]';
232
- const QR_RETRY_BUTTON = 'div[data-ref] > span > button';
233
- let qrRetries = 0;
234
- await page.exposeFunction('qrChanged', async (qr) => {
423
+ const message = new Message(this, msg);
424
+
235
425
  /**
236
- * Emitted when a QR code is received
237
- * @event Client#qr
238
- * @param {string} qr QR Code
239
- */
240
- this.emit(Events.QR_RECEIVED, qr);
241
- if (this.options.qrMaxRetries > 0) {
242
- qrRetries++;
243
- if (qrRetries > this.options.qrMaxRetries) {
244
- this.emit(Events.DISCONNECTED, 'Max qrcode retries reached');
245
- await this.destroy();
246
- }
247
- }
248
- });
426
+ * Emitted when a new message is created, which may include the current user's own messages.
427
+ * @event Client#message_create
428
+ * @param {Message} message The message that was created
429
+ */
430
+ this.emit(Events.MESSAGE_CREATE, message);
249
431
 
250
- await page.evaluate(function (selectors) {
251
- const qr_container = document.querySelector(selectors.QR_CONTAINER);
252
- window.qrChanged(qr_container.dataset.ref);
432
+ if (msg.id.fromMe) return;
253
433
 
254
- const obs = new MutationObserver((muts) => {
255
- muts.forEach(mut => {
256
- // Listens to qr token change
257
- if (mut.type === 'attributes' && mut.attributeName === 'data-ref') {
258
- window.qrChanged(mut.target.dataset.ref);
259
- }
260
- // Listens to retry button, when found, click it
261
- else if (mut.type === 'childList') {
262
- const retry_button = document.querySelector(selectors.QR_RETRY_BUTTON);
263
- if (retry_button) retry_button.click();
264
- }
265
- });
266
- });
267
- obs.observe(qr_container.parentElement, {
268
- subtree: true,
269
- childList: true,
270
- attributes: true,
271
- attributeFilter: ['data-ref'],
272
- });
273
- }, {
274
- QR_CONTAINER,
275
- QR_RETRY_BUTTON
434
+ /**
435
+ * Emitted when a new message is received.
436
+ * @event Client#message
437
+ * @param {Message} message The message that was received
438
+ */
439
+ this.emit(Events.MESSAGE_RECEIVED, message);
276
440
  });
277
441
 
278
- // Wait for code scan
279
- try {
280
- await page.waitForSelector(INTRO_IMG_SELECTOR, { timeout: 0 });
281
- } catch(error) {
282
- if (
283
- error.name === 'ProtocolError' &&
284
- error.message &&
285
- error.message.match(/Target closed/)
286
- ) {
287
- // something has called .destroy() while waiting
288
- return;
289
- }
290
-
291
- throw error;
292
- }
293
-
294
- }
295
-
296
- await page.evaluate(() => {
297
- /**
298
- * 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.
299
- * @param {string} lOperand The left operand for the WWeb version string to compare with
300
- * @param {string} operator The comparison operator
301
- * @param {string} rOperand The right operand for the WWeb version string to compare with
302
- * @returns {boolean} Boolean value that indicates the result of the comparison
303
- */
304
- window.compareWwebVersions = (lOperand, operator, rOperand) => {
305
- if (!['>', '>=', '<', '<=', '='].includes(operator)) {
306
- throw new class _ extends Error {
307
- constructor(m) { super(m); this.name = 'CompareWwebVersionsError'; }
308
- }('Invalid comparison operator is provided');
442
+ let last_message;
309
443
 
310
- }
311
- if (typeof lOperand !== 'string' || typeof rOperand !== 'string') {
312
- throw new class _ extends Error {
313
- constructor(m) { super(m); this.name = 'CompareWwebVersionsError'; }
314
- }('A non-string WWeb version type is provided');
315
- }
444
+ await this.pupPage.exposeFunction('onChangeMessageTypeEvent', (msg) => {
316
445
 
317
- lOperand = lOperand.replace(/-beta$/, '');
318
- rOperand = rOperand.replace(/-beta$/, '');
446
+ if (msg.type === 'revoked') {
447
+ const message = new Message(this, msg);
448
+ let revoked_msg;
449
+ if (last_message && msg.id.id === last_message.id.id) {
450
+ revoked_msg = new Message(this, last_message);
451
+ }
319
452
 
320
- while (lOperand.length !== rOperand.length) {
321
- lOperand.length > rOperand.length
322
- ? rOperand = rOperand.concat('0')
323
- : lOperand = lOperand.concat('0');
453
+ /**
454
+ * Emitted when a message is deleted for everyone in the chat.
455
+ * @event Client#message_revoke_everyone
456
+ * @param {Message} message The message that was revoked, in its current state. It will not contain the original message's data.
457
+ * @param {?Message} revoked_msg The message that was revoked, before it was revoked. It will contain the message's original data.
458
+ * Note that due to the way this data is captured, it may be possible that this param will be undefined.
459
+ */
460
+ this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg);
324
461
  }
325
462
 
326
- lOperand = Number(lOperand.replace(/\./g, ''));
327
- rOperand = Number(rOperand.replace(/\./g, ''));
463
+ });
328
464
 
329
- return (
330
- operator === '>' ? lOperand > rOperand :
331
- operator === '>=' ? lOperand >= rOperand :
332
- operator === '<' ? lOperand < rOperand :
333
- operator === '<=' ? lOperand <= rOperand :
334
- operator === '=' ? lOperand === rOperand :
335
- false
336
- );
337
- };
338
- });
465
+ await this.pupPage.exposeFunction('onChangeMessageEvent', (msg) => {
339
466
 
340
- await page.evaluate(ExposeStore, moduleRaid.toString());
341
- const authEventPayload = await this.authStrategy.getAuthEventPayload();
467
+ if (msg.type !== 'revoked') {
468
+ last_message = msg;
469
+ }
342
470
 
343
- /**
344
- * Emitted when authentication is successful
345
- * @event Client#authenticated
346
- */
347
- this.emit(Events.AUTHENTICATED, authEventPayload);
471
+ /**
472
+ * The event notification that is received when one of
473
+ * the group participants changes their phone number.
474
+ */
475
+ const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify';
348
476
 
349
- // Check window.Store Injection
350
- await page.waitForFunction('window.Store != undefined');
477
+ /**
478
+ * The event notification that is received when one of
479
+ * the contacts changes their phone number.
480
+ */
481
+ const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number';
351
482
 
352
- await page.evaluate(async () => {
353
- // safely unregister service workers
354
- const registrations = await navigator.serviceWorker.getRegistrations();
355
- for (let registration of registrations) {
356
- registration.unregister();
357
- }
358
- });
483
+ if (isParticipant || isContact) {
484
+ /** @type {GroupNotification} object does not provide enough information about this event, so a @type {Message} object is used. */
485
+ const message = new Message(this, msg);
359
486
 
360
- //Load util functions (serializers, helper functions)
361
- await page.evaluate(LoadUtils);
487
+ const newId = isParticipant ? msg.recipients[0] : msg.to;
488
+ const oldId = isParticipant ? msg.author : msg.templateParams.find(id => id !== newId);
362
489
 
363
- // Expose client info
364
- /**
365
- * Current connection information
366
- * @type {ClientInfo}
367
- */
368
- this.info = new ClientInfo(this, await page.evaluate(() => {
369
- return { ...window.Store.Conn.serialize(), wid: window.Store.User.getMeUser() };
370
- }));
371
-
372
- // Add InterfaceController
373
- this.interface = new InterfaceController(this);
374
-
375
- // Register events
376
- await page.exposeFunction('onAddMessageEvent', msg => {
377
- if (msg.type === 'gp2') {
378
- const notification = new GroupNotification(this, msg);
379
- if (['add', 'invite', 'linked_group_join'].includes(msg.subtype)) {
380
- /**
381
- * Emitted when a user joins the chat via invite link or is added by an admin.
382
- * @event Client#group_join
383
- * @param {GroupNotification} notification GroupNotification with more information about the action
384
- */
385
- this.emit(Events.GROUP_JOIN, notification);
386
- } else if (msg.subtype === 'remove' || msg.subtype === 'leave') {
387
- /**
388
- * Emitted when a user leaves the chat or is removed by an admin.
389
- * @event Client#group_leave
390
- * @param {GroupNotification} notification GroupNotification with more information about the action
391
- */
392
- this.emit(Events.GROUP_LEAVE, notification);
393
- } else if (msg.subtype === 'promote' || msg.subtype === 'demote') {
394
- /**
395
- * Emitted when a current user is promoted to an admin or demoted to a regular user.
396
- * @event Client#group_admin_changed
397
- * @param {GroupNotification} notification GroupNotification with more information about the action
398
- */
399
- this.emit(Events.GROUP_ADMIN_CHANGED, notification);
400
- } else if (msg.subtype === 'membership_approval_request') {
401
- /**
402
- * Emitted when some user requested to join the group
403
- * that has the membership approval mode turned on
404
- * @event Client#group_membership_request
405
- * @param {GroupNotification} notification GroupNotification with more information about the action
406
- * @param {string} notification.chatId The group ID the request was made for
407
- * @param {string} notification.author The user ID that made a request
408
- * @param {number} notification.timestamp The timestamp the request was made at
409
- */
410
- this.emit(Events.GROUP_MEMBERSHIP_REQUEST, notification);
411
- } else {
412
490
  /**
413
- * Emitted when group settings are updated, such as subject, description or picture.
414
- * @event Client#group_update
415
- * @param {GroupNotification} notification GroupNotification with more information about the action
491
+ * Emitted when a contact or a group participant changes their phone number.
492
+ * @event Client#contact_changed
493
+ * @param {Message} message Message with more information about the event.
494
+ * @param {String} oldId The user's id (an old one) who changed their phone number
495
+ * and who triggered the notification.
496
+ * @param {String} newId The user's new id after the change.
497
+ * @param {Boolean} isContact Indicates if a contact or a group participant changed their phone number.
416
498
  */
417
- this.emit(Events.GROUP_UPDATE, notification);
499
+ this.emit(Events.CONTACT_CHANGED, message, oldId, newId, isContact);
418
500
  }
419
- return;
420
- }
421
-
422
- const message = new Message(this, msg);
423
-
424
- /**
425
- * Emitted when a new message is created, which may include the current user's own messages.
426
- * @event Client#message_create
427
- * @param {Message} message The message that was created
428
- */
429
- this.emit(Events.MESSAGE_CREATE, message);
430
-
431
- if (msg.id.fromMe) return;
432
-
433
- /**
434
- * Emitted when a new message is received.
435
- * @event Client#message
436
- * @param {Message} message The message that was received
437
- */
438
- this.emit(Events.MESSAGE_RECEIVED, message);
439
- });
501
+ });
440
502
 
441
- let last_message;
503
+ await this.pupPage.exposeFunction('onRemoveMessageEvent', (msg) => {
442
504
 
443
- await page.exposeFunction('onChangeMessageTypeEvent', (msg) => {
505
+ if (!msg.isNewMsg) return;
444
506
 
445
- if (msg.type === 'revoked') {
446
507
  const message = new Message(this, msg);
447
- let revoked_msg;
448
- if (last_message && msg.id.id === last_message.id.id) {
449
- revoked_msg = new Message(this, last_message);
450
- }
451
508
 
452
509
  /**
453
- * Emitted when a message is deleted for everyone in the chat.
454
- * @event Client#message_revoke_everyone
455
- * @param {Message} message The message that was revoked, in its current state. It will not contain the original message's data.
456
- * @param {?Message} revoked_msg The message that was revoked, before it was revoked. It will contain the message's original data.
457
- * Note that due to the way this data is captured, it may be possible that this param will be undefined.
510
+ * Emitted when a message is deleted by the current user.
511
+ * @event Client#message_revoke_me
512
+ * @param {Message} message The message that was revoked
458
513
  */
459
- this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg);
460
- }
514
+ this.emit(Events.MESSAGE_REVOKED_ME, message);
461
515
 
462
- });
463
-
464
- await page.exposeFunction('onChangeMessageEvent', (msg) => {
465
-
466
- if (msg.type !== 'revoked') {
467
- last_message = msg;
468
- }
469
-
470
- /**
471
- * The event notification that is received when one of
472
- * the group participants changes their phone number.
473
- */
474
- const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify';
516
+ });
475
517
 
476
- /**
477
- * The event notification that is received when one of
478
- * the contacts changes their phone number.
479
- */
480
- const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number';
518
+ await this.pupPage.exposeFunction('onMessageAckEvent', (msg, ack) => {
481
519
 
482
- if (isParticipant || isContact) {
483
- /** @type {GroupNotification} object does not provide enough information about this event, so a @type {Message} object is used. */
484
520
  const message = new Message(this, msg);
485
521
 
486
- const newId = isParticipant ? msg.recipients[0] : msg.to;
487
- const oldId = isParticipant ? msg.author : msg.templateParams.find(id => id !== newId);
488
-
489
522
  /**
490
- * Emitted when a contact or a group participant changes their phone number.
491
- * @event Client#contact_changed
492
- * @param {Message} message Message with more information about the event.
493
- * @param {String} oldId The user's id (an old one) who changed their phone number
494
- * and who triggered the notification.
495
- * @param {String} newId The user's new id after the change.
496
- * @param {Boolean} isContact Indicates if a contact or a group participant changed their phone number.
523
+ * Emitted when an ack event occurrs on message type.
524
+ * @event Client#message_ack
525
+ * @param {Message} message The message that was affected
526
+ * @param {MessageAck} ack The new ACK value
497
527
  */
498
- this.emit(Events.CONTACT_CHANGED, message, oldId, newId, isContact);
499
- }
500
- });
501
-
502
- await page.exposeFunction('onRemoveMessageEvent', (msg) => {
503
-
504
- if (!msg.isNewMsg) return;
505
-
506
- const message = new Message(this, msg);
507
-
508
- /**
509
- * Emitted when a message is deleted by the current user.
510
- * @event Client#message_revoke_me
511
- * @param {Message} message The message that was revoked
512
- */
513
- this.emit(Events.MESSAGE_REVOKED_ME, message);
514
-
515
- });
516
-
517
- await page.exposeFunction('onMessageAckEvent', (msg, ack) => {
528
+ this.emit(Events.MESSAGE_ACK, message, ack);
518
529
 
519
- const message = new Message(this, msg);
530
+ });
520
531
 
521
- /**
522
- * Emitted when an ack event occurrs on message type.
523
- * @event Client#message_ack
524
- * @param {Message} message The message that was affected
525
- * @param {MessageAck} ack The new ACK value
526
- */
527
- this.emit(Events.MESSAGE_ACK, message, ack);
532
+ await this.pupPage.exposeFunction('onChatUnreadCountEvent', async (data) =>{
533
+ const chat = await this.getChatById(data.id);
534
+
535
+ /**
536
+ * Emitted when the chat unread count changes
537
+ */
538
+ this.emit(Events.UNREAD_COUNT, chat);
539
+ });
528
540
 
529
- });
541
+ await this.pupPage.exposeFunction('onMessageMediaUploadedEvent', (msg) => {
530
542
 
531
- await page.exposeFunction('onChatUnreadCountEvent', async (data) =>{
532
- const chat = await this.getChatById(data.id);
533
-
534
- /**
535
- * Emitted when the chat unread count changes
536
- */
537
- this.emit(Events.UNREAD_COUNT, chat);
538
- });
543
+ const message = new Message(this, msg);
539
544
 
540
- await page.exposeFunction('onMessageMediaUploadedEvent', (msg) => {
545
+ /**
546
+ * Emitted when media has been uploaded for a message sent by the client.
547
+ * @event Client#media_uploaded
548
+ * @param {Message} message The message with media that was uploaded
549
+ */
550
+ this.emit(Events.MEDIA_UPLOADED, message);
551
+ });
541
552
 
542
- const message = new Message(this, msg);
553
+ await this.pupPage.exposeFunction('onAppStateChangedEvent', async (state) => {
554
+ /**
555
+ * Emitted when the connection state changes
556
+ * @event Client#change_state
557
+ * @param {WAState} state the new connection state
558
+ */
559
+ this.emit(Events.STATE_CHANGED, state);
543
560
 
544
- /**
545
- * Emitted when media has been uploaded for a message sent by the client.
546
- * @event Client#media_uploaded
547
- * @param {Message} message The message with media that was uploaded
548
- */
549
- this.emit(Events.MEDIA_UPLOADED, message);
550
- });
561
+ const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT];
551
562
 
552
- await page.exposeFunction('onAppStateChangedEvent', async (state) => {
563
+ if (this.options.takeoverOnConflict) {
564
+ ACCEPTED_STATES.push(WAState.CONFLICT);
553
565
 
554
- /**
555
- * Emitted when the connection state changes
556
- * @event Client#change_state
557
- * @param {WAState} state the new connection state
558
- */
559
- this.emit(Events.STATE_CHANGED, state);
566
+ if (state === WAState.CONFLICT) {
567
+ setTimeout(() => {
568
+ this.pupPage.evaluate(() => window.Store.AppState.takeover());
569
+ }, this.options.takeoverTimeoutMs);
570
+ }
571
+ }
560
572
 
561
- const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT];
573
+ if (!ACCEPTED_STATES.includes(state)) {
574
+ /**
575
+ * Emitted when the client has been disconnected
576
+ * @event Client#disconnected
577
+ * @param {WAState|"LOGOUT"} reason reason that caused the disconnect
578
+ */
579
+ await this.authStrategy.disconnect();
580
+ this.emit(Events.DISCONNECTED, state);
581
+ this.destroy();
582
+ }
583
+ });
562
584
 
563
- if (this.options.takeoverOnConflict) {
564
- ACCEPTED_STATES.push(WAState.CONFLICT);
585
+ await this.pupPage.exposeFunction('onBatteryStateChangedEvent', (state) => {
586
+ const { battery, plugged } = state;
565
587
 
566
- if (state === WAState.CONFLICT) {
567
- setTimeout(() => {
568
- this.pupPage.evaluate(() => window.Store.AppState.takeover());
569
- }, this.options.takeoverTimeoutMs);
570
- }
571
- }
588
+ if (battery === undefined) return;
572
589
 
573
- if (!ACCEPTED_STATES.includes(state)) {
574
590
  /**
575
- * Emitted when the client has been disconnected
576
- * @event Client#disconnected
577
- * @param {WAState|"NAVIGATION"} reason reason that caused the disconnect
591
+ * Emitted when the battery percentage for the attached device changes. Will not be sent if using multi-device.
592
+ * @event Client#change_battery
593
+ * @param {object} batteryInfo
594
+ * @param {number} batteryInfo.battery - The current battery percentage
595
+ * @param {boolean} batteryInfo.plugged - Indicates if the phone is plugged in (true) or not (false)
596
+ * @deprecated
578
597
  */
579
- await this.authStrategy.disconnect();
580
- this.emit(Events.DISCONNECTED, state);
581
- this.destroy();
582
- }
583
- });
598
+ this.emit(Events.BATTERY_CHANGED, { battery, plugged });
599
+ });
584
600
 
585
- await page.exposeFunction('onBatteryStateChangedEvent', (state) => {
586
- const { battery, plugged } = state;
601
+ await this.pupPage.exposeFunction('onIncomingCall', (call) => {
602
+ /**
603
+ * Emitted when a call is received
604
+ * @event Client#incoming_call
605
+ * @param {object} call
606
+ * @param {number} call.id - Call id
607
+ * @param {string} call.peerJid - Who called
608
+ * @param {boolean} call.isVideo - if is video
609
+ * @param {boolean} call.isGroup - if is group
610
+ * @param {boolean} call.canHandleLocally - if we can handle in waweb
611
+ * @param {boolean} call.outgoing - if is outgoing
612
+ * @param {boolean} call.webClientShouldHandle - If Waweb should handle
613
+ * @param {object} call.participants - Participants
614
+ */
615
+ const cll = new Call(this, call);
616
+ this.emit(Events.INCOMING_CALL, cll);
617
+ });
587
618
 
588
- if (battery === undefined) return;
619
+ await this.pupPage.exposeFunction('onReaction', (reactions) => {
620
+ for (const reaction of reactions) {
621
+ /**
622
+ * Emitted when a reaction is sent, received, updated or removed
623
+ * @event Client#message_reaction
624
+ * @param {object} reaction
625
+ * @param {object} reaction.id - Reaction id
626
+ * @param {number} reaction.orphan - Orphan
627
+ * @param {?string} reaction.orphanReason - Orphan reason
628
+ * @param {number} reaction.timestamp - Timestamp
629
+ * @param {string} reaction.reaction - Reaction
630
+ * @param {boolean} reaction.read - Read
631
+ * @param {object} reaction.msgId - Parent message id
632
+ * @param {string} reaction.senderId - Sender id
633
+ * @param {?number} reaction.ack - Ack
634
+ */
589
635
 
590
- /**
591
- * Emitted when the battery percentage for the attached device changes. Will not be sent if using multi-device.
592
- * @event Client#change_battery
593
- * @param {object} batteryInfo
594
- * @param {number} batteryInfo.battery - The current battery percentage
595
- * @param {boolean} batteryInfo.plugged - Indicates if the phone is plugged in (true) or not (false)
596
- * @deprecated
597
- */
598
- this.emit(Events.BATTERY_CHANGED, { battery, plugged });
599
- });
636
+ this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction));
637
+ }
638
+ });
600
639
 
601
- await page.exposeFunction('onIncomingCall', (call) => {
602
- /**
603
- * Emitted when a call is received
604
- * @event Client#incoming_call
605
- * @param {object} call
606
- * @param {number} call.id - Call id
607
- * @param {string} call.peerJid - Who called
608
- * @param {boolean} call.isVideo - if is video
609
- * @param {boolean} call.isGroup - if is group
610
- * @param {boolean} call.canHandleLocally - if we can handle in waweb
611
- * @param {boolean} call.outgoing - if is outgoing
612
- * @param {boolean} call.webClientShouldHandle - If Waweb should handle
613
- * @param {object} call.participants - Participants
614
- */
615
- const cll = new Call(this, call);
616
- this.emit(Events.INCOMING_CALL, cll);
617
- });
640
+ await this.pupPage.exposeFunction('onRemoveChatEvent', async (chat) => {
641
+ const _chat = await this.getChatById(chat.id);
618
642
 
619
- await page.exposeFunction('onReaction', (reactions) => {
620
- for (const reaction of reactions) {
621
643
  /**
622
- * Emitted when a reaction is sent, received, updated or removed
623
- * @event Client#message_reaction
624
- * @param {object} reaction
625
- * @param {object} reaction.id - Reaction id
626
- * @param {number} reaction.orphan - Orphan
627
- * @param {?string} reaction.orphanReason - Orphan reason
628
- * @param {number} reaction.timestamp - Timestamp
629
- * @param {string} reaction.reaction - Reaction
630
- * @param {boolean} reaction.read - Read
631
- * @param {object} reaction.msgId - Parent message id
632
- * @param {string} reaction.senderId - Sender id
633
- * @param {?number} reaction.ack - Ack
644
+ * Emitted when a chat is removed
645
+ * @event Client#chat_removed
646
+ * @param {Chat} chat
634
647
  */
635
-
636
- this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction));
637
- }
638
- });
639
-
640
- await page.exposeFunction('onRemoveChatEvent', async (chat) => {
641
- const _chat = await this.getChatById(chat.id);
642
-
643
- /**
644
- * Emitted when a chat is removed
645
- * @event Client#chat_removed
646
- * @param {Chat} chat
647
- */
648
- this.emit(Events.CHAT_REMOVED, _chat);
649
- });
650
-
651
- await page.exposeFunction('onArchiveChatEvent', async (chat, currState, prevState) => {
652
- const _chat = await this.getChatById(chat.id);
648
+ this.emit(Events.CHAT_REMOVED, _chat);
649
+ });
653
650
 
654
- /**
655
- * Emitted when a chat is archived/unarchived
656
- * @event Client#chat_archived
657
- * @param {Chat} chat
658
- * @param {boolean} currState
659
- * @param {boolean} prevState
660
- */
661
- this.emit(Events.CHAT_ARCHIVED, _chat, currState, prevState);
662
- });
651
+ await this.pupPage.exposeFunction('onArchiveChatEvent', async (chat, currState, prevState) => {
652
+ const _chat = await this.getChatById(chat.id);
653
+
654
+ /**
655
+ * Emitted when a chat is archived/unarchived
656
+ * @event Client#chat_archived
657
+ * @param {Chat} chat
658
+ * @param {boolean} currState
659
+ * @param {boolean} prevState
660
+ */
661
+ this.emit(Events.CHAT_ARCHIVED, _chat, currState, prevState);
662
+ });
663
663
 
664
- await page.exposeFunction('onEditMessageEvent', (msg, newBody, prevBody) => {
665
-
666
- if(msg.type === 'revoked'){
667
- return;
668
- }
669
- /**
670
- * Emitted when messages are edited
671
- * @event Client#message_edit
672
- * @param {Message} message
673
- * @param {string} newBody
674
- * @param {string} prevBody
675
- */
676
- this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody);
677
- });
678
-
679
- await page.exposeFunction('onAddMessageCiphertextEvent', msg => {
664
+ await this.pupPage.exposeFunction('onEditMessageEvent', (msg, newBody, prevBody) => {
665
+
666
+ if(msg.type === 'revoked'){
667
+ return;
668
+ }
669
+ /**
670
+ * Emitted when messages are edited
671
+ * @event Client#message_edit
672
+ * @param {Message} message
673
+ * @param {string} newBody
674
+ * @param {string} prevBody
675
+ */
676
+ this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody);
677
+ });
680
678
 
681
- /**
682
- * Emitted when messages are edited
683
- * @event Client#message_ciphertext
684
- * @param {Message} message
685
- */
686
- this.emit(Events.MESSAGE_CIPHERTEXT, new Message(this, msg));
687
- });
679
+ await this.pupPage.exposeFunction('onAddMessageCiphertextEvent', msg => {
680
+
681
+ /**
682
+ * Emitted when messages are edited
683
+ * @event Client#message_ciphertext
684
+ * @param {Message} message
685
+ */
686
+ this.emit(Events.MESSAGE_CIPHERTEXT, new Message(this, msg));
687
+ });
688
+ }
688
689
 
689
- await page.exposeFunction('onPollVoteEvent', (vote) => {
690
+ await this.pupPage.exposeFunction('onPollVoteEvent', (vote) => {
690
691
  const _vote = new PollVote(this, vote);
691
692
  /**
692
693
  * Emitted when some poll option is selected or deselected,
@@ -696,7 +697,7 @@ class Client extends EventEmitter {
696
697
  this.emit(Events.VOTE_UPDATE, _vote);
697
698
  });
698
699
 
699
- await page.evaluate(() => {
700
+ await this.pupPage.evaluate(() => {
700
701
  window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(window.WWebJS.getMessageModel(msg)); });
701
702
  window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(window.WWebJS.getMessageModel(msg)); });
702
703
  window.Store.Msg.on('change:ack', (msg, ack) => { window.onMessageAckEvent(window.WWebJS.getMessageModel(msg), ack); });
@@ -720,12 +721,28 @@ class Client extends EventEmitter {
720
721
  }
721
722
  });
722
723
  window.Store.Chat.on('change:unreadCount', (chat) => {window.onChatUnreadCountEvent(chat);});
723
- window.Store.PollVote.on('add', (vote) => {
724
- const pollVoteModel = window.WWebJS.getPollVoteModel(vote);
724
+ window.Store.PollVote.on('add', async (vote) => {
725
+ const pollVoteModel = await window.WWebJS.getPollVoteModel(vote);
725
726
  pollVoteModel && window.onPollVoteEvent(pollVoteModel);
726
727
  });
727
728
 
728
- {
729
+ if (window.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.1014111620')) {
730
+ const module = window.Store.AddonReactionTable;
731
+ const ogMethod = module.bulkUpsert;
732
+ module.bulkUpsert = ((...args) => {
733
+ window.onReaction(args[0].map(reaction => {
734
+ const msgKey = reaction.id;
735
+ const parentMsgKey = reaction.reactionParentKey;
736
+ const timestamp = reaction.reactionTimestamp / 1000;
737
+ const sender = reaction.author ?? reaction.from;
738
+ const senderUserJid = sender._serialized;
739
+
740
+ return {...reaction, msgKey, parentMsgKey, senderUserJid, timestamp };
741
+ }));
742
+
743
+ return ogMethod(...args);
744
+ }).bind(module);
745
+ } else {
729
746
  const module = window.Store.createOrUpdateReactionsModule;
730
747
  const ogMethod = module.createOrUpdateReactions;
731
748
  module.createOrUpdateReactions = ((...args) => {
@@ -741,24 +758,7 @@ class Client extends EventEmitter {
741
758
  }).bind(module);
742
759
  }
743
760
  });
744
-
745
- /**
746
- * Emitted when the client has initialized and is ready to receive messages.
747
- * @event Client#ready
748
- */
749
- this.emit(Events.READY);
750
- this.authStrategy.afterAuthReady();
751
-
752
- // Disconnect when navigating away when in PAIRING state (detect logout)
753
- this.pupPage.on('framenavigated', async () => {
754
- const appState = await this.getState();
755
- if(!appState || appState === WAState.PAIRING) {
756
- await this.authStrategy.disconnect();
757
- this.emit(Events.DISCONNECTED, 'NAVIGATION');
758
- await this.destroy();
759
- }
760
- });
761
- }
761
+ }
762
762
 
763
763
  async initWebVersionCache() {
764
764
  const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache;
@@ -783,7 +783,8 @@ class Client extends EventEmitter {
783
783
  } else {
784
784
  this.pupPage.on('response', async (res) => {
785
785
  if(res.ok() && res.url() === WhatsWebURL) {
786
- await webCache.persist(await res.text());
786
+ const indexHtml = await res.text();
787
+ this.currentIndexHtml = indexHtml;
787
788
  }
788
789
  });
789
790
  }
@@ -802,7 +803,9 @@ class Client extends EventEmitter {
802
803
  */
803
804
  async logout() {
804
805
  await this.pupPage.evaluate(() => {
805
- return window.Store.AppState.logout();
806
+ if (window.Store && window.Store.AppState && typeof window.Store.AppState.logout === 'function') {
807
+ return window.Store.AppState.logout();
808
+ }
806
809
  });
807
810
  await this.pupBrowser.close();
808
811
 
@@ -861,6 +864,7 @@ class Client extends EventEmitter {
861
864
  * @property {GroupMention[]} [groupMentions] - An array of object that handle group mentions
862
865
  * @property {string[]} [mentions] - User IDs to mention in the message
863
866
  * @property {boolean} [sendSeen=true] - Mark the conversation as seen after sending the message
867
+ * @property {string} [invokedBotWid=undefined] - Bot Wid when doing a bot mention like @Meta AI
864
868
  * @property {string} [stickerAuthor=undefined] - Sets the author of the sticker, (if sendMediaAsSticker is true).
865
869
  * @property {string} [stickerName=undefined] - Sets the name of the sticker, (if sendMediaAsSticker is true).
866
870
  * @property {string[]} [stickerCategories=undefined] - Sets the categories of the sticker, (if sendMediaAsSticker is true). Provide emoji char array, can be null.
@@ -894,9 +898,10 @@ class Client extends EventEmitter {
894
898
  sendMediaAsDocument: options.sendMediaAsDocument,
895
899
  caption: options.caption,
896
900
  quotedMessageId: options.quotedMessageId,
897
- parseVCards: options.parseVCards === false ? false : true,
901
+ parseVCards: options.parseVCards !== false,
898
902
  mentionedJidList: options.mentions || [],
899
903
  groupMentions: options.groupMentions,
904
+ invokedBotWid: options.invokedBotWid,
900
905
  extraOptions: options.extra
901
906
  };
902
907
 
@@ -1032,7 +1037,7 @@ class Client extends EventEmitter {
1032
1037
  if(msg) return window.WWebJS.getMessageModel(msg);
1033
1038
 
1034
1039
  const params = messageId.split('_');
1035
- if(params.length !== 3) throw new Error('Invalid serialized message id specified');
1040
+ if (params.length !== 3 && params.length !== 4) throw new Error('Invalid serialized message id specified');
1036
1041
 
1037
1042
  let messagesObject = await window.Store.Msg.getMessagesById([messageId]);
1038
1043
  if (messagesObject && messagesObject.messages.length) msg = messagesObject.messages[0];
@@ -1102,14 +1107,8 @@ class Client extends EventEmitter {
1102
1107
  async setDisplayName(displayName) {
1103
1108
  const couldSet = await this.pupPage.evaluate(async displayName => {
1104
1109
  if(!window.Store.Conn.canSetMyPushname()) return false;
1105
-
1106
- if(window.Store.MDBackend) {
1107
- await window.Store.Settings.setPushname(displayName);
1108
- return true;
1109
- } else {
1110
- const res = await window.Store.Wap.setPushname(displayName);
1111
- return !res.status || res.status === 200;
1112
- }
1110
+ await window.Store.Settings.setPushname(displayName);
1111
+ return true;
1113
1112
  }, displayName);
1114
1113
 
1115
1114
  return couldSet;
@@ -1250,7 +1249,9 @@ class Client extends EventEmitter {
1250
1249
  const profilePic = await this.pupPage.evaluate(async contactId => {
1251
1250
  try {
1252
1251
  const chatWid = window.Store.WidFactory.createWid(contactId);
1253
- return await window.Store.ProfilePic.profilePicFind(chatWid);
1252
+ return window.compareWwebVersions(window.Debug.VERSION, '<', '2.3000.0')
1253
+ ? await window.Store.ProfilePic.profilePicFind(chatWid)
1254
+ : await window.Store.ProfilePic.requestProfilePicFromServer(chatWid);
1254
1255
  } catch (err) {
1255
1256
  if(err.name === 'ServerStatusCodeError') return undefined;
1256
1257
  throw err;
@@ -1416,10 +1417,18 @@ class Client extends EventEmitter {
1416
1417
 
1417
1418
  try {
1418
1419
  createGroupResult = await window.Store.GroupUtils.createGroup(
1419
- title,
1420
- participantWids,
1421
- messageTimer,
1422
- parentGroupWid
1420
+ {
1421
+ 'memberAddMode': options.memberAddMode === undefined ? true : options.memberAddMode,
1422
+ 'membershipApprovalMode': options.membershipApprovalMode === undefined ? false : options.membershipApprovalMode,
1423
+ 'announce': options.announce === undefined ? true : options.announce,
1424
+ 'ephemeralDuration': messageTimer,
1425
+ 'full': undefined,
1426
+ 'parentGroupId': parentGroupWid,
1427
+ 'restrict': options.restrict === undefined ? true : options.restrict,
1428
+ 'thumb': undefined,
1429
+ 'title': title,
1430
+ },
1431
+ participantWids
1423
1432
  );
1424
1433
  } catch (err) {
1425
1434
  return 'CreateGroupError: An unknown error occupied while creating a group';
@@ -1431,7 +1440,7 @@ class Client extends EventEmitter {
1431
1440
  const statusCode = participant.error ?? 200;
1432
1441
 
1433
1442
  if (autoSendInviteV4 && statusCode === 403) {
1434
- window.Store.ContactCollection.gadd(participant.wid, { silent: true });
1443
+ window.Store.Contact.gadd(participant.wid, { silent: true });
1435
1444
  const addParticipantResult = await window.Store.GroupInviteV4.sendGroupInviteMessage(
1436
1445
  await window.Store.Chat.find(participant.wid),
1437
1446
  createGroupResult.wid._serialized,
@@ -1608,8 +1617,8 @@ class Client extends EventEmitter {
1608
1617
  * @returns {Promise<Array<GroupMembershipRequest>>} An array of membership requests
1609
1618
  */
1610
1619
  async getGroupMembershipRequests(groupId) {
1611
- return await this.pupPage.evaluate(async (gropId) => {
1612
- const groupWid = window.Store.WidFactory.createWid(gropId);
1620
+ return await this.pupPage.evaluate(async (groupId) => {
1621
+ const groupWid = window.Store.WidFactory.createWid(groupId);
1613
1622
  return await window.Store.MembershipRequestUtils.getMembershipApprovalRequests(groupWid);
1614
1623
  }, groupId);
1615
1624
  }
@@ -1715,6 +1724,21 @@ class Client extends EventEmitter {
1715
1724
  return flag;
1716
1725
  }, flag);
1717
1726
  }
1727
+
1728
+ /**
1729
+ * Get user device count by ID
1730
+ * Each WaWeb Connection counts as one device, and the phone (if exists) counts as one
1731
+ * So for a non-enterprise user with one WaWeb connection it should return "2"
1732
+ * @param {string} contactId
1733
+ * @returns {number}
1734
+ */
1735
+ async getContactDeviceCount(contactId) {
1736
+ let devices = await window.Store.DeviceList.getDeviceIds([window.Store.WidFactory.createWid(contactId)]);
1737
+ if(devices && devices.length && devices[0] != null && typeof devices[0].devices == 'object'){
1738
+ return devices[0].devices.length;
1739
+ }
1740
+ return 0;
1741
+ }
1718
1742
  }
1719
1743
 
1720
- module.exports = Client;
1744
+ module.exports = Client;