whatsapp-web-sj.js 1.26.0 → 1.31.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
@@ -15,8 +15,9 @@ const { LoadUtils } = require('./util/Injected/Utils');
15
15
  const ChatFactory = require('./factories/ChatFactory');
16
16
  const ContactFactory = require('./factories/ContactFactory');
17
17
  const WebCacheFactory = require('./webCache/WebCacheFactory');
18
- const { ClientInfo, Message, MessageMedia, Contact, Location, Poll, PollVote, GroupNotification, Label, Call, Buttons, List, Reaction } = require('./structures');
18
+ const { Broadcast, Buttons, Call, ClientInfo, Contact, GroupNotification, Label, List, Location, Message, MessageMedia, Poll, PollVote, Reaction } = require('./structures');
19
19
  const NoAuth = require('./authStrategies/NoAuth');
20
+ const {exposeFunctionIfAbsent} = require('./util/Puppeteer');
20
21
  const pie = require('puppeteer-in-electron');
21
22
 
22
23
  /**
@@ -91,9 +92,8 @@ class Client extends EventEmitter {
91
92
  /**
92
93
  * Injection logic
93
94
  * Private function
94
- * @property {boolean} reinject is this a reinject?
95
95
  */
96
- async inject(reinject = false) {
96
+ async inject() {
97
97
  await this.pupPage.waitForFunction('window.Debug?.VERSION != undefined', {timeout: this.options.authTimeoutMs});
98
98
 
99
99
  const version = await this.getWWebVersion();
@@ -143,26 +143,21 @@ class Client extends EventEmitter {
143
143
 
144
144
  // Register qr events
145
145
  let qrRetries = 0;
146
- const injected = await this.pupPage.evaluate(() => {
147
- return typeof window.onQRChangedEvent !== 'undefined';
148
- });
149
- if (!injected) {
150
- await this.pupPage.exposeFunction('onQRChangedEvent', async (qr) => {
151
- /**
152
- * Emitted when a QR code is received
153
- * @event Client#qr
154
- * @param {string} qr QR Code
155
- */
156
- this.emit(Events.QR_RECEIVED, qr);
157
- if (this.options.qrMaxRetries > 0) {
158
- qrRetries++;
159
- if (qrRetries > this.options.qrMaxRetries) {
160
- this.emit(Events.DISCONNECTED, 'Max qrcode retries reached');
161
- await this.destroy();
162
- }
146
+ await exposeFunctionIfAbsent(this.pupPage, 'onQRChangedEvent', async (qr) => {
147
+ /**
148
+ * Emitted when a QR code is received
149
+ * @event Client#qr
150
+ * @param {string} qr QR Code
151
+ */
152
+ this.emit(Events.QR_RECEIVED, qr);
153
+ if (this.options.qrMaxRetries > 0) {
154
+ qrRetries++;
155
+ if (qrRetries > this.options.qrMaxRetries) {
156
+ this.emit(Events.DISCONNECTED, 'Max qrcode retries reached');
157
+ await this.destroy();
163
158
  }
164
- });
165
- }
159
+ }
160
+ });
166
161
 
167
162
 
168
163
  await this.pupPage.evaluate(async () => {
@@ -179,86 +174,78 @@ class Client extends EventEmitter {
179
174
  });
180
175
  }
181
176
 
182
- if (!reinject) {
183
- await this.pupPage.exposeFunction('onAuthAppStateChangedEvent', async (state) => {
184
- if (state == 'UNPAIRED_IDLE') {
185
- // refresh qr code
186
- window.Store.Cmd.refreshQR();
187
- }
188
- });
177
+ await exposeFunctionIfAbsent(this.pupPage, 'onAuthAppStateChangedEvent', async (state) => {
178
+ if (state == 'UNPAIRED_IDLE') {
179
+ // refresh qr code
180
+ window.Store.Cmd.refreshQR();
181
+ }
182
+ });
189
183
 
190
- await this.pupPage.exposeFunction('onAppStateHasSyncedEvent', async () => {
191
- const authEventPayload = await this.authStrategy.getAuthEventPayload();
192
- /**
184
+ await exposeFunctionIfAbsent(this.pupPage, 'onAppStateHasSyncedEvent', async () => {
185
+ const authEventPayload = await this.authStrategy.getAuthEventPayload();
186
+ /**
193
187
  * Emitted when authentication is successful
194
188
  * @event Client#authenticated
195
189
  */
196
- this.emit(Events.AUTHENTICATED, authEventPayload);
190
+ this.emit(Events.AUTHENTICATED, authEventPayload);
197
191
 
198
- const injected = await this.pupPage.evaluate(async () => {
199
- return typeof window.Store !== 'undefined' && typeof window.WWebJS !== 'undefined';
200
- });
192
+ const injected = await this.pupPage.evaluate(async () => {
193
+ return typeof window.Store !== 'undefined' && typeof window.WWebJS !== 'undefined';
194
+ });
201
195
 
202
- if (!injected) {
203
- if (this.options.webVersionCache.type === 'local' && this.currentIndexHtml) {
204
- const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache;
205
- const webCache = WebCacheFactory.createWebCache(webCacheType, webCacheOptions);
196
+ if (!injected) {
197
+ if (this.options.webVersionCache.type === 'local' && this.currentIndexHtml) {
198
+ const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache;
199
+ const webCache = WebCacheFactory.createWebCache(webCacheType, webCacheOptions);
206
200
 
207
- await webCache.persist(this.currentIndexHtml, version);
208
- }
201
+ await webCache.persist(this.currentIndexHtml, version);
202
+ }
209
203
 
210
- if (isCometOrAbove) {
211
- await this.pupPage.evaluate(ExposeStore);
212
- } else {
213
- // make sure all modules are ready before injection
214
- // 2 second delay after authentication makes sense and does not need to be made dyanmic or removed
215
- await new Promise(r => setTimeout(r, 2000));
216
- await this.pupPage.evaluate(ExposeLegacyStore);
217
- }
204
+ if (isCometOrAbove) {
205
+ await this.pupPage.evaluate(ExposeStore);
206
+ } else {
207
+ // make sure all modules are ready before injection
208
+ // 2 second delay after authentication makes sense and does not need to be made dyanmic or removed
209
+ await new Promise(r => setTimeout(r, 2000));
210
+ await this.pupPage.evaluate(ExposeLegacyStore);
211
+ }
218
212
 
219
- // Check window.Store Injection
220
- await this.pupPage.waitForFunction('window.Store != undefined');
213
+ // Check window.Store Injection
214
+ await this.pupPage.waitForFunction('window.Store != undefined');
221
215
 
222
- /**
216
+ /**
223
217
  * Current connection information
224
218
  * @type {ClientInfo}
225
219
  */
226
- this.info = new ClientInfo(this, await this.pupPage.evaluate(() => {
227
- return { ...window.Store.Conn.serialize(), wid: window.Store.User.getMeUser() };
228
- }));
220
+ this.info = new ClientInfo(this, await this.pupPage.evaluate(() => {
221
+ return { ...window.Store.Conn.serialize(), wid: window.Store.User.getMeUser() };
222
+ }));
229
223
 
230
- this.interface = new InterfaceController(this);
224
+ this.interface = new InterfaceController(this);
231
225
 
232
- //Load util functions (serializers, helper functions)
233
- await this.pupPage.evaluate(LoadUtils);
226
+ //Load util functions (serializers, helper functions)
227
+ await this.pupPage.evaluate(LoadUtils);
234
228
 
235
- await this.attachEventListeners(reinject);
236
- reinject = true;
237
- }
238
- /**
229
+ await this.attachEventListeners();
230
+ }
231
+ /**
239
232
  * Emitted when the client has initialized and is ready to receive messages.
240
233
  * @event Client#ready
241
234
  */
242
- this.emit(Events.READY);
243
- this.authStrategy.afterAuthReady();
244
- });
245
- let lastPercent = null;
246
- await this.pupPage.exposeFunction('onOfflineProgressUpdateEvent', async (percent) => {
247
- if (lastPercent !== percent) {
248
- lastPercent = percent;
249
- this.emit(Events.LOADING_SCREEN, percent, 'WhatsApp'); // Message is hardcoded as "WhatsApp" for now
250
- }
251
- });
252
- }
253
- const logoutCatchInjected = await this.pupPage.evaluate(() => {
254
- return typeof window.onLogoutEvent !== 'undefined';
235
+ this.emit(Events.READY);
236
+ this.authStrategy.afterAuthReady();
237
+ });
238
+ let lastPercent = null;
239
+ await exposeFunctionIfAbsent(this.pupPage, 'onOfflineProgressUpdateEvent', async (percent) => {
240
+ if (lastPercent !== percent) {
241
+ lastPercent = percent;
242
+ this.emit(Events.LOADING_SCREEN, percent, 'WhatsApp'); // Message is hardcoded as "WhatsApp" for now
243
+ }
244
+ });
245
+ await exposeFunctionIfAbsent(this.pupPage, 'onLogoutEvent', async () => {
246
+ this.lastLoggedOut = true;
247
+ await this.pupPage.waitForNavigation({waitUntil: 'load', timeout: 5000}).catch((_) => _);
255
248
  });
256
- if (!logoutCatchInjected) {
257
- await this.pupPage.exposeFunction('onLogoutEvent', async () => {
258
- this.lastLoggedOut = true;
259
- await this.pupPage.waitForNavigation({waitUntil: 'load', timeout: 5000}).catch((_) => _);
260
- });
261
- }
262
249
  await this.pupPage.evaluate(() => {
263
250
  window.AuthStore.AppState.on('change:state', (_AppState, state) => { window.onAuthAppStateChangedEvent(state); });
264
251
  window.AuthStore.AppState.on('change:hasSynced', () => { window.onAppStateHasSyncedEvent(); });
@@ -292,7 +279,7 @@ class Client extends EventEmitter {
292
279
  // await this.authStrategy.beforeBrowserInitialized();
293
280
 
294
281
  // const puppeteerOpts = this.options.puppeteer;
295
- // if (puppeteerOpts && puppeteerOpts.browserWSEndpoint) {
282
+ // if (puppeteerOpts && (puppeteerOpts.browserWSEndpoint || puppeteerOpts.browserURL)) {
296
283
  // browser = await puppeteer.connect(puppeteerOpts);
297
284
  // page = await browser.newPage();
298
285
  // } else {
@@ -352,7 +339,7 @@ class Client extends EventEmitter {
352
339
  await this.authStrategy.afterBrowserInitialized();
353
340
  this.lastLoggedOut = false;
354
341
  }
355
- await this.inject(true);
342
+ await this.inject();
356
343
  });
357
344
  }
358
345
 
@@ -375,34 +362,33 @@ class Client extends EventEmitter {
375
362
  * Private function
376
363
  * @property {boolean} reinject is this a reinject?
377
364
  */
378
- async attachEventListeners(reinject = false) {
379
- if (!reinject) {
380
- await this.pupPage.exposeFunction('onAddMessageEvent', msg => {
381
- if (msg.type === 'gp2') {
382
- const notification = new GroupNotification(this, msg);
383
- if (['add', 'invite', 'linked_group_join'].includes(msg.subtype)) {
384
- /**
365
+ async attachEventListeners() {
366
+ await exposeFunctionIfAbsent(this.pupPage, 'onAddMessageEvent', msg => {
367
+ if (msg.type === 'gp2') {
368
+ const notification = new GroupNotification(this, msg);
369
+ if (['add', 'invite', 'linked_group_join'].includes(msg.subtype)) {
370
+ /**
385
371
  * Emitted when a user joins the chat via invite link or is added by an admin.
386
372
  * @event Client#group_join
387
373
  * @param {GroupNotification} notification GroupNotification with more information about the action
388
374
  */
389
- this.emit(Events.GROUP_JOIN, notification);
390
- } else if (msg.subtype === 'remove' || msg.subtype === 'leave') {
391
- /**
375
+ this.emit(Events.GROUP_JOIN, notification);
376
+ } else if (msg.subtype === 'remove' || msg.subtype === 'leave') {
377
+ /**
392
378
  * Emitted when a user leaves the chat or is removed by an admin.
393
379
  * @event Client#group_leave
394
380
  * @param {GroupNotification} notification GroupNotification with more information about the action
395
381
  */
396
- this.emit(Events.GROUP_LEAVE, notification);
397
- } else if (msg.subtype === 'promote' || msg.subtype === 'demote') {
398
- /**
382
+ this.emit(Events.GROUP_LEAVE, notification);
383
+ } else if (msg.subtype === 'promote' || msg.subtype === 'demote') {
384
+ /**
399
385
  * Emitted when a current user is promoted to an admin or demoted to a regular user.
400
386
  * @event Client#group_admin_changed
401
387
  * @param {GroupNotification} notification GroupNotification with more information about the action
402
388
  */
403
- this.emit(Events.GROUP_ADMIN_CHANGED, notification);
404
- } else if (msg.subtype === 'membership_approval_request') {
405
- /**
389
+ this.emit(Events.GROUP_ADMIN_CHANGED, notification);
390
+ } else if (msg.subtype === 'membership_approval_request') {
391
+ /**
406
392
  * Emitted when some user requested to join the group
407
393
  * that has the membership approval mode turned on
408
394
  * @event Client#group_membership_request
@@ -411,86 +397,86 @@ class Client extends EventEmitter {
411
397
  * @param {string} notification.author The user ID that made a request
412
398
  * @param {number} notification.timestamp The timestamp the request was made at
413
399
  */
414
- this.emit(Events.GROUP_MEMBERSHIP_REQUEST, notification);
415
- } else {
416
- /**
400
+ this.emit(Events.GROUP_MEMBERSHIP_REQUEST, notification);
401
+ } else {
402
+ /**
417
403
  * Emitted when group settings are updated, such as subject, description or picture.
418
404
  * @event Client#group_update
419
405
  * @param {GroupNotification} notification GroupNotification with more information about the action
420
406
  */
421
- this.emit(Events.GROUP_UPDATE, notification);
422
- }
423
- return;
407
+ this.emit(Events.GROUP_UPDATE, notification);
424
408
  }
409
+ return;
410
+ }
425
411
 
426
- const message = new Message(this, msg);
412
+ const message = new Message(this, msg);
427
413
 
428
- /**
414
+ /**
429
415
  * Emitted when a new message is created, which may include the current user's own messages.
430
416
  * @event Client#message_create
431
417
  * @param {Message} message The message that was created
432
418
  */
433
- this.emit(Events.MESSAGE_CREATE, message);
419
+ this.emit(Events.MESSAGE_CREATE, message);
434
420
 
435
- if (msg.id.fromMe) return;
421
+ if (msg.id.fromMe) return;
436
422
 
437
- /**
423
+ /**
438
424
  * Emitted when a new message is received.
439
425
  * @event Client#message
440
426
  * @param {Message} message The message that was received
441
427
  */
442
- this.emit(Events.MESSAGE_RECEIVED, message);
443
- });
428
+ this.emit(Events.MESSAGE_RECEIVED, message);
429
+ });
444
430
 
445
- let last_message;
431
+ let last_message;
446
432
 
447
- await this.pupPage.exposeFunction('onChangeMessageTypeEvent', (msg) => {
433
+ await exposeFunctionIfAbsent(this.pupPage, 'onChangeMessageTypeEvent', (msg) => {
448
434
 
449
- if (msg.type === 'revoked') {
450
- const message = new Message(this, msg);
451
- let revoked_msg;
452
- if (last_message && msg.id.id === last_message.id.id) {
453
- revoked_msg = new Message(this, last_message);
454
- }
435
+ if (msg.type === 'revoked') {
436
+ const message = new Message(this, msg);
437
+ let revoked_msg;
438
+ if (last_message && msg.id.id === last_message.id.id) {
439
+ revoked_msg = new Message(this, last_message);
440
+ }
455
441
 
456
- /**
442
+ /**
457
443
  * Emitted when a message is deleted for everyone in the chat.
458
444
  * @event Client#message_revoke_everyone
459
445
  * @param {Message} message The message that was revoked, in its current state. It will not contain the original message's data.
460
446
  * @param {?Message} revoked_msg The message that was revoked, before it was revoked. It will contain the message's original data.
461
447
  * Note that due to the way this data is captured, it may be possible that this param will be undefined.
462
448
  */
463
- this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg);
464
- }
449
+ this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg);
450
+ }
465
451
 
466
- });
452
+ });
467
453
 
468
- await this.pupPage.exposeFunction('onChangeMessageEvent', (msg) => {
454
+ await exposeFunctionIfAbsent(this.pupPage, 'onChangeMessageEvent', (msg) => {
469
455
 
470
- if (msg.type !== 'revoked') {
471
- last_message = msg;
472
- }
456
+ if (msg.type !== 'revoked') {
457
+ last_message = msg;
458
+ }
473
459
 
474
- /**
460
+ /**
475
461
  * The event notification that is received when one of
476
462
  * the group participants changes their phone number.
477
463
  */
478
- const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify';
464
+ const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify';
479
465
 
480
- /**
466
+ /**
481
467
  * The event notification that is received when one of
482
468
  * the contacts changes their phone number.
483
469
  */
484
- const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number';
470
+ const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number';
485
471
 
486
- if (isParticipant || isContact) {
487
- /** @type {GroupNotification} object does not provide enough information about this event, so a @type {Message} object is used. */
488
- const message = new Message(this, msg);
472
+ if (isParticipant || isContact) {
473
+ /** @type {GroupNotification} object does not provide enough information about this event, so a @type {Message} object is used. */
474
+ const message = new Message(this, msg);
489
475
 
490
- const newId = isParticipant ? msg.recipients[0] : msg.to;
491
- const oldId = isParticipant ? msg.author : msg.templateParams.find(id => id !== newId);
476
+ const newId = isParticipant ? msg.recipients[0] : msg.to;
477
+ const oldId = isParticipant ? msg.author : msg.templateParams.find(id => id !== newId);
492
478
 
493
- /**
479
+ /**
494
480
  * Emitted when a contact or a group participant changes their phone number.
495
481
  * @event Client#contact_changed
496
482
  * @param {Message} message Message with more information about the event.
@@ -499,98 +485,98 @@ class Client extends EventEmitter {
499
485
  * @param {String} newId The user's new id after the change.
500
486
  * @param {Boolean} isContact Indicates if a contact or a group participant changed their phone number.
501
487
  */
502
- this.emit(Events.CONTACT_CHANGED, message, oldId, newId, isContact);
503
- }
504
- });
488
+ this.emit(Events.CONTACT_CHANGED, message, oldId, newId, isContact);
489
+ }
490
+ });
505
491
 
506
- await this.pupPage.exposeFunction('onRemoveMessageEvent', (msg) => {
492
+ await exposeFunctionIfAbsent(this.pupPage, 'onRemoveMessageEvent', (msg) => {
507
493
 
508
- if (!msg.isNewMsg) return;
494
+ if (!msg.isNewMsg) return;
509
495
 
510
- const message = new Message(this, msg);
496
+ const message = new Message(this, msg);
511
497
 
512
- /**
498
+ /**
513
499
  * Emitted when a message is deleted by the current user.
514
500
  * @event Client#message_revoke_me
515
501
  * @param {Message} message The message that was revoked
516
502
  */
517
- this.emit(Events.MESSAGE_REVOKED_ME, message);
503
+ this.emit(Events.MESSAGE_REVOKED_ME, message);
518
504
 
519
- });
505
+ });
520
506
 
521
- await this.pupPage.exposeFunction('onMessageAckEvent', (msg, ack) => {
507
+ await exposeFunctionIfAbsent(this.pupPage, 'onMessageAckEvent', (msg, ack) => {
522
508
 
523
- const message = new Message(this, msg);
509
+ const message = new Message(this, msg);
524
510
 
525
- /**
511
+ /**
526
512
  * Emitted when an ack event occurrs on message type.
527
513
  * @event Client#message_ack
528
514
  * @param {Message} message The message that was affected
529
515
  * @param {MessageAck} ack The new ACK value
530
516
  */
531
- this.emit(Events.MESSAGE_ACK, message, ack);
517
+ this.emit(Events.MESSAGE_ACK, message, ack);
532
518
 
533
- });
519
+ });
534
520
 
535
- await this.pupPage.exposeFunction('onChatUnreadCountEvent', async (data) =>{
536
- const chat = await this.getChatById(data.id);
521
+ await exposeFunctionIfAbsent(this.pupPage, 'onChatUnreadCountEvent', async (data) =>{
522
+ const chat = await this.getChatById(data.id);
537
523
 
538
- /**
524
+ /**
539
525
  * Emitted when the chat unread count changes
540
526
  */
541
- this.emit(Events.UNREAD_COUNT, chat);
542
- });
527
+ this.emit(Events.UNREAD_COUNT, chat);
528
+ });
543
529
 
544
- await this.pupPage.exposeFunction('onMessageMediaUploadedEvent', (msg) => {
530
+ await exposeFunctionIfAbsent(this.pupPage, 'onMessageMediaUploadedEvent', (msg) => {
545
531
 
546
- const message = new Message(this, msg);
532
+ const message = new Message(this, msg);
547
533
 
548
- /**
534
+ /**
549
535
  * Emitted when media has been uploaded for a message sent by the client.
550
536
  * @event Client#media_uploaded
551
537
  * @param {Message} message The message with media that was uploaded
552
538
  */
553
- this.emit(Events.MEDIA_UPLOADED, message);
554
- });
539
+ this.emit(Events.MEDIA_UPLOADED, message);
540
+ });
555
541
 
556
- await this.pupPage.exposeFunction('onAppStateChangedEvent', async (state) => {
557
- /**
542
+ await exposeFunctionIfAbsent(this.pupPage, 'onAppStateChangedEvent', async (state) => {
543
+ /**
558
544
  * Emitted when the connection state changes
559
545
  * @event Client#change_state
560
546
  * @param {WAState} state the new connection state
561
547
  */
562
- this.emit(Events.STATE_CHANGED, state);
548
+ this.emit(Events.STATE_CHANGED, state);
563
549
 
564
- const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT];
550
+ const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT];
565
551
 
566
- if (this.options.takeoverOnConflict) {
567
- ACCEPTED_STATES.push(WAState.CONFLICT);
552
+ if (this.options.takeoverOnConflict) {
553
+ ACCEPTED_STATES.push(WAState.CONFLICT);
568
554
 
569
- if (state === WAState.CONFLICT) {
570
- setTimeout(() => {
571
- this.pupPage.evaluate(() => window.Store.AppState.takeover());
572
- }, this.options.takeoverTimeoutMs);
573
- }
555
+ if (state === WAState.CONFLICT) {
556
+ setTimeout(() => {
557
+ this.pupPage.evaluate(() => window.Store.AppState.takeover());
558
+ }, this.options.takeoverTimeoutMs);
574
559
  }
560
+ }
575
561
 
576
- if (!ACCEPTED_STATES.includes(state)) {
577
- /**
562
+ if (!ACCEPTED_STATES.includes(state)) {
563
+ /**
578
564
  * Emitted when the client has been disconnected
579
565
  * @event Client#disconnected
580
566
  * @param {WAState|"LOGOUT"} reason reason that caused the disconnect
581
567
  */
582
- await this.authStrategy.disconnect();
583
- this.emit(Events.DISCONNECTED, state);
584
- this.destroy();
585
- }
586
- });
568
+ await this.authStrategy.disconnect();
569
+ this.emit(Events.DISCONNECTED, state);
570
+ this.destroy();
571
+ }
572
+ });
587
573
 
588
- await this.pupPage.exposeFunction('onBatteryStateChangedEvent', (state) => {
589
- const { battery, plugged } = state;
574
+ await exposeFunctionIfAbsent(this.pupPage, 'onBatteryStateChangedEvent', (state) => {
575
+ const { battery, plugged } = state;
590
576
 
591
- if (battery === undefined) return;
577
+ if (battery === undefined) return;
592
578
 
593
- /**
579
+ /**
594
580
  * Emitted when the battery percentage for the attached device changes. Will not be sent if using multi-device.
595
581
  * @event Client#change_battery
596
582
  * @param {object} batteryInfo
@@ -598,11 +584,11 @@ class Client extends EventEmitter {
598
584
  * @param {boolean} batteryInfo.plugged - Indicates if the phone is plugged in (true) or not (false)
599
585
  * @deprecated
600
586
  */
601
- this.emit(Events.BATTERY_CHANGED, { battery, plugged });
602
- });
587
+ this.emit(Events.BATTERY_CHANGED, { battery, plugged });
588
+ });
603
589
 
604
- await this.pupPage.exposeFunction('onIncomingCall', (call) => {
605
- /**
590
+ await exposeFunctionIfAbsent(this.pupPage, 'onIncomingCall', (call) => {
591
+ /**
606
592
  * Emitted when a call is received
607
593
  * @event Client#incoming_call
608
594
  * @param {object} call
@@ -615,13 +601,13 @@ class Client extends EventEmitter {
615
601
  * @param {boolean} call.webClientShouldHandle - If Waweb should handle
616
602
  * @param {object} call.participants - Participants
617
603
  */
618
- const cll = new Call(this, call);
619
- this.emit(Events.INCOMING_CALL, cll);
620
- });
604
+ const cll = new Call(this, call);
605
+ this.emit(Events.INCOMING_CALL, cll);
606
+ });
621
607
 
622
- await this.pupPage.exposeFunction('onReaction', (reactions) => {
623
- for (const reaction of reactions) {
624
- /**
608
+ await exposeFunctionIfAbsent(this.pupPage, 'onReaction', (reactions) => {
609
+ for (const reaction of reactions) {
610
+ /**
625
611
  * Emitted when a reaction is sent, received, updated or removed
626
612
  * @event Client#message_reaction
627
613
  * @param {object} reaction
@@ -636,61 +622,60 @@ class Client extends EventEmitter {
636
622
  * @param {?number} reaction.ack - Ack
637
623
  */
638
624
 
639
- this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction));
640
- }
641
- });
625
+ this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction));
626
+ }
627
+ });
642
628
 
643
- await this.pupPage.exposeFunction('onRemoveChatEvent', async (chat) => {
644
- const _chat = await this.getChatById(chat.id);
629
+ await exposeFunctionIfAbsent(this.pupPage, 'onRemoveChatEvent', async (chat) => {
630
+ const _chat = await this.getChatById(chat.id);
645
631
 
646
- /**
632
+ /**
647
633
  * Emitted when a chat is removed
648
634
  * @event Client#chat_removed
649
635
  * @param {Chat} chat
650
636
  */
651
- this.emit(Events.CHAT_REMOVED, _chat);
652
- });
637
+ this.emit(Events.CHAT_REMOVED, _chat);
638
+ });
653
639
 
654
- await this.pupPage.exposeFunction('onArchiveChatEvent', async (chat, currState, prevState) => {
655
- const _chat = await this.getChatById(chat.id);
640
+ await exposeFunctionIfAbsent(this.pupPage, 'onArchiveChatEvent', async (chat, currState, prevState) => {
641
+ const _chat = await this.getChatById(chat.id);
656
642
 
657
- /**
643
+ /**
658
644
  * Emitted when a chat is archived/unarchived
659
645
  * @event Client#chat_archived
660
646
  * @param {Chat} chat
661
647
  * @param {boolean} currState
662
648
  * @param {boolean} prevState
663
649
  */
664
- this.emit(Events.CHAT_ARCHIVED, _chat, currState, prevState);
665
- });
650
+ this.emit(Events.CHAT_ARCHIVED, _chat, currState, prevState);
651
+ });
666
652
 
667
- await this.pupPage.exposeFunction('onEditMessageEvent', (msg, newBody, prevBody) => {
653
+ await exposeFunctionIfAbsent(this.pupPage, 'onEditMessageEvent', (msg, newBody, prevBody) => {
668
654
 
669
- if(msg.type === 'revoked'){
670
- return;
671
- }
672
- /**
655
+ if(msg.type === 'revoked'){
656
+ return;
657
+ }
658
+ /**
673
659
  * Emitted when messages are edited
674
660
  * @event Client#message_edit
675
661
  * @param {Message} message
676
662
  * @param {string} newBody
677
663
  * @param {string} prevBody
678
664
  */
679
- this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody);
680
- });
665
+ this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody);
666
+ });
681
667
 
682
- await this.pupPage.exposeFunction('onAddMessageCiphertextEvent', msg => {
668
+ await exposeFunctionIfAbsent(this.pupPage, 'onAddMessageCiphertextEvent', msg => {
683
669
 
684
- /**
670
+ /**
685
671
  * Emitted when messages are edited
686
672
  * @event Client#message_ciphertext
687
673
  * @param {Message} message
688
674
  */
689
- this.emit(Events.MESSAGE_CIPHERTEXT, new Message(this, msg));
690
- });
691
- }
675
+ this.emit(Events.MESSAGE_CIPHERTEXT, new Message(this, msg));
676
+ });
692
677
 
693
- await this.pupPage.exposeFunction('onPollVoteEvent', (vote) => {
678
+ await exposeFunctionIfAbsent(this.pupPage, 'onPollVoteEvent', (vote) => {
694
679
  const _vote = new PollVote(this, vote);
695
680
  /**
696
681
  * Emitted when some poll option is selected or deselected,
@@ -810,15 +795,15 @@ class Client extends EventEmitter {
810
795
  return window.Store.AppState.logout();
811
796
  }
812
797
  });
813
- // await this.pupBrowser.close();
798
+ await this.pupBrowser.close();
814
799
 
815
- // let maxDelay = 0;
816
- // while (this.pupBrowser.isConnected() && (maxDelay < 10)) { // waits a maximum of 1 second before calling the AuthStrategy
817
- // await new Promise(resolve => setTimeout(resolve, 100));
818
- // maxDelay++;
819
- // }
800
+ let maxDelay = 0;
801
+ while (this.pupBrowser.isConnected() && (maxDelay < 10)) { // waits a maximum of 1 second before calling the AuthStrategy
802
+ await new Promise(resolve => setTimeout(resolve, 100));
803
+ maxDelay++;
804
+ }
820
805
 
821
- // await this.authStrategy.logout();
806
+ await this.authStrategy.logout();
822
807
  }
823
808
 
824
809
  /**
@@ -838,11 +823,9 @@ class Client extends EventEmitter {
838
823
  *
839
824
  */
840
825
  async sendSeen(chatId) {
841
- const result = await this.pupPage.evaluate(async (chatId) => {
826
+ return await this.pupPage.evaluate(async (chatId) => {
842
827
  return window.WWebJS.sendSeen(chatId);
843
-
844
828
  }, chatId);
845
- return result;
846
829
  }
847
830
 
848
831
  /**
@@ -860,6 +843,7 @@ class Client extends EventEmitter {
860
843
  * @property {boolean} [sendVideoAsGif=false] - Send video as gif
861
844
  * @property {boolean} [sendMediaAsSticker=false] - Send media as a sticker
862
845
  * @property {boolean} [sendMediaAsDocument=false] - Send media as a document
846
+ * @property {boolean} [sendMediaAsHd=false] - Send image as quality HD
863
847
  * @property {boolean} [isViewOnce=false] - Send photo/video as a view once message
864
848
  * @property {boolean} [parseVCards=true] - Automatically parse vCards and send them as contacts
865
849
  * @property {string} [caption] - Image or video caption
@@ -871,7 +855,9 @@ class Client extends EventEmitter {
871
855
  * @property {string} [stickerAuthor=undefined] - Sets the author of the sticker, (if sendMediaAsSticker is true).
872
856
  * @property {string} [stickerName=undefined] - Sets the name of the sticker, (if sendMediaAsSticker is true).
873
857
  * @property {string[]} [stickerCategories=undefined] - Sets the categories of the sticker, (if sendMediaAsSticker is true). Provide emoji char array, can be null.
858
+ * @property {boolean} [ignoreQuoteErrors = true] - Should the bot send a quoted message without the quoted message if it fails to get the quote?
874
859
  * @property {MessageMedia} [media] - Media to be sent
860
+ * @property {any} [extra] - Extra options
875
861
  */
876
862
 
877
863
  /**
@@ -883,6 +869,19 @@ class Client extends EventEmitter {
883
869
  * @returns {Promise<Message>} Message that was just sent
884
870
  */
885
871
  async sendMessage(chatId, content, options = {}) {
872
+ const isChannel = /@\w*newsletter\b/.test(chatId);
873
+
874
+ if (isChannel && [
875
+ options.sendMediaAsDocument, options.quotedMessageId,
876
+ options.parseVCards, options.isViewOnce,
877
+ content instanceof Location, content instanceof Contact,
878
+ content instanceof Buttons, content instanceof List,
879
+ Array.isArray(content) && content.length > 0 && content[0] instanceof Contact
880
+ ].includes(true)) {
881
+ console.warn('The message type is currently not supported for sending in channels,\nthe supported message types are: text, image, sticker, gif, video, voice and poll.');
882
+ return null;
883
+ }
884
+
886
885
  if (options.mentions) {
887
886
  !Array.isArray(options.mentions) && (options.mentions = [options.mentions]);
888
887
  if (options.mentions.some((possiblyContact) => possiblyContact instanceof Contact)) {
@@ -892,30 +891,32 @@ class Client extends EventEmitter {
892
891
  }
893
892
 
894
893
  options.groupMentions && !Array.isArray(options.groupMentions) && (options.groupMentions = [options.groupMentions]);
895
-
894
+
896
895
  let internalOptions = {
897
896
  linkPreview: options.linkPreview === false ? undefined : true,
898
897
  sendAudioAsVoice: options.sendAudioAsVoice,
899
898
  sendVideoAsGif: options.sendVideoAsGif,
900
899
  sendMediaAsSticker: options.sendMediaAsSticker,
901
900
  sendMediaAsDocument: options.sendMediaAsDocument,
901
+ sendMediaAsHd: options.sendMediaAsHd,
902
902
  caption: options.caption,
903
903
  quotedMessageId: options.quotedMessageId,
904
904
  parseVCards: options.parseVCards !== false,
905
905
  mentionedJidList: options.mentions || [],
906
906
  groupMentions: options.groupMentions,
907
907
  invokedBotWid: options.invokedBotWid,
908
+ ignoreQuoteErrors: options.ignoreQuoteErrors !== false,
908
909
  extraOptions: options.extra
909
910
  };
910
911
 
911
- const sendSeen = typeof options.sendSeen === 'undefined' ? true : options.sendSeen;
912
+ const sendSeen = options.sendSeen !== false;
912
913
 
913
914
  if (content instanceof MessageMedia) {
914
- internalOptions.attachment = content;
915
+ internalOptions.media = content;
915
916
  internalOptions.isViewOnce = options.isViewOnce,
916
917
  content = '';
917
918
  } else if (options.media instanceof MessageMedia) {
918
- internalOptions.attachment = options.media;
919
+ internalOptions.media = options.media;
919
920
  internalOptions.caption = content;
920
921
  internalOptions.isViewOnce = options.isViewOnce,
921
922
  content = '';
@@ -932,17 +933,19 @@ class Client extends EventEmitter {
932
933
  internalOptions.contactCardList = content.map(contact => contact.id._serialized);
933
934
  content = '';
934
935
  } else if (content instanceof Buttons) {
936
+ console.warn('Buttons are now deprecated. See more at https://www.youtube.com/watch?v=hv1R1rLeVVE.');
935
937
  if (content.type !== 'chat') { internalOptions.attachment = content.body; }
936
938
  internalOptions.buttons = content;
937
939
  content = '';
938
940
  } else if (content instanceof List) {
941
+ console.warn('Lists are now deprecated. See more at https://www.youtube.com/watch?v=hv1R1rLeVVE.');
939
942
  internalOptions.list = content;
940
943
  content = '';
941
944
  }
942
945
 
943
- if (internalOptions.sendMediaAsSticker && internalOptions.attachment) {
944
- internalOptions.attachment = await Util.formatToWebpSticker(
945
- internalOptions.attachment, {
946
+ if (internalOptions.sendMediaAsSticker && internalOptions.media) {
947
+ internalOptions.media = await Util.formatToWebpSticker(
948
+ internalOptions.media, {
946
949
  name: options.stickerName,
947
950
  author: options.stickerAuthor,
948
951
  categories: options.stickerCategories
@@ -950,20 +953,60 @@ class Client extends EventEmitter {
950
953
  );
951
954
  }
952
955
 
953
- const newMessage = await this.pupPage.evaluate(async (chatId, message, options, sendSeen) => {
954
- const chatWid = window.Store.WidFactory.createWid(chatId);
955
- const chat = await window.Store.Chat.find(chatWid);
956
+ const sentMsg = await this.pupPage.evaluate(async (chatId, content, options, sendSeen) => {
957
+ const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
956
958
 
959
+ if (!chat) return null;
957
960
 
958
961
  if (sendSeen) {
959
962
  await window.WWebJS.sendSeen(chatId);
960
963
  }
961
964
 
962
- const msg = await window.WWebJS.sendMessage(chat, message, options, sendSeen);
963
- return window.WWebJS.getMessageModel(msg);
965
+ const msg = await window.WWebJS.sendMessage(chat, content, options);
966
+ return msg
967
+ ? window.WWebJS.getMessageModel(msg)
968
+ : undefined;
964
969
  }, chatId, content, internalOptions, sendSeen);
965
970
 
966
- return new Message(this, newMessage);
971
+ return sentMsg
972
+ ? new Message(this, sentMsg)
973
+ : undefined;
974
+ }
975
+
976
+ /**
977
+ * @typedef {Object} SendChannelAdminInviteOptions
978
+ * @property {?string} comment The comment to be added to an invitation
979
+ */
980
+
981
+ /**
982
+ * Sends a channel admin invitation to a user, allowing them to become an admin of the channel
983
+ * @param {string} chatId The ID of a user to send the channel admin invitation to
984
+ * @param {string} channelId The ID of a channel for which the invitation is being sent
985
+ * @param {SendChannelAdminInviteOptions} options
986
+ * @returns {Promise<boolean>} Returns true if an invitation was sent successfully, false otherwise
987
+ */
988
+ async sendChannelAdminInvite(chatId, channelId, options = {}) {
989
+ const response = await this.pupPage.evaluate(async (chatId, channelId, options) => {
990
+ const channelWid = window.Store.WidFactory.createWid(channelId);
991
+ const chatWid = window.Store.WidFactory.createWid(chatId);
992
+ const chat = window.Store.Chat.get(chatWid) || (await window.Store.Chat.find(chatWid));
993
+
994
+ if (!chatWid.isUser()) {
995
+ return false;
996
+ }
997
+
998
+ return await window.Store.SendChannelMessage.sendNewsletterAdminInviteMessage(
999
+ chat,
1000
+ {
1001
+ newsletterWid: channelWid,
1002
+ invitee: chatWid,
1003
+ inviteMessage: options.comment,
1004
+ base64Thumb: await window.WWebJS.getProfilePicThumbToBase64(channelWid)
1005
+ }
1006
+ );
1007
+ }, chatId, channelId, options);
1008
+
1009
+ return response.messageSendResult === 'OK';
967
1010
  }
968
1011
 
969
1012
  /**
@@ -989,7 +1032,7 @@ class Client extends EventEmitter {
989
1032
  * @returns {Promise<Array<Chat>>}
990
1033
  */
991
1034
  async getChats() {
992
- let chats = await this.pupPage.evaluate(async () => {
1035
+ const chats = await this.pupPage.evaluate(async () => {
993
1036
  return await window.WWebJS.getChats();
994
1037
  });
995
1038
 
@@ -997,16 +1040,51 @@ class Client extends EventEmitter {
997
1040
  }
998
1041
 
999
1042
  /**
1000
- * Get chat instance by ID
1043
+ * Gets all cached {@link Channel} instance
1044
+ * @returns {Promise<Array<Channel>>}
1045
+ */
1046
+ async getChannels() {
1047
+ const channels = await this.pupPage.evaluate(async () => {
1048
+ return await window.WWebJS.getChannels();
1049
+ });
1050
+
1051
+ return channels.map((channel) => ChatFactory.create(this, channel));
1052
+ }
1053
+
1054
+ /**
1055
+ * Gets chat or channel instance by ID
1001
1056
  * @param {string} chatId
1002
- * @returns {Promise<Chat>}
1057
+ * @returns {Promise<Chat|Channel>}
1003
1058
  */
1004
1059
  async getChatById(chatId) {
1005
- let chat = await this.pupPage.evaluate(async chatId => {
1060
+ const chat = await this.pupPage.evaluate(async chatId => {
1006
1061
  return await window.WWebJS.getChat(chatId);
1007
1062
  }, chatId);
1063
+ return chat
1064
+ ? ChatFactory.create(this, chat)
1065
+ : undefined;
1066
+ }
1008
1067
 
1009
- return ChatFactory.create(this, chat);
1068
+ /**
1069
+ * Gets a {@link Channel} instance by invite code
1070
+ * @param {string} inviteCode The code that comes after the 'https://whatsapp.com/channel/'
1071
+ * @returns {Promise<Channel>}
1072
+ */
1073
+ async getChannelByInviteCode(inviteCode) {
1074
+ const channel = await this.pupPage.evaluate(async (inviteCode) => {
1075
+ let channelMetadata;
1076
+ try {
1077
+ channelMetadata = await window.WWebJS.getChannelMetadata(inviteCode);
1078
+ } catch (err) {
1079
+ if (err.name === 'ServerStatusCodeError') return null;
1080
+ throw err;
1081
+ }
1082
+ return await window.WWebJS.getChat(channelMetadata.id);
1083
+ }, inviteCode);
1084
+
1085
+ return channel
1086
+ ? ChatFactory.create(this, channel)
1087
+ : undefined;
1010
1088
  }
1011
1089
 
1012
1090
  /**
@@ -1076,6 +1154,61 @@ class Client extends EventEmitter {
1076
1154
  return res.gid._serialized;
1077
1155
  }
1078
1156
 
1157
+ /**
1158
+ * Accepts a channel admin invitation and promotes the current user to a channel admin
1159
+ * @param {string} channelId The channel ID to accept the admin invitation from
1160
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1161
+ */
1162
+ async acceptChannelAdminInvite(channelId) {
1163
+ return await this.pupPage.evaluate(async (channelId) => {
1164
+ try {
1165
+ await window.Store.ChannelUtils.acceptNewsletterAdminInvite(channelId);
1166
+ return true;
1167
+ } catch (err) {
1168
+ if (err.name === 'ServerStatusCodeError') return false;
1169
+ throw err;
1170
+ }
1171
+ }, channelId);
1172
+ }
1173
+
1174
+ /**
1175
+ * Revokes a channel admin invitation sent to a user by a channel owner
1176
+ * @param {string} channelId The channel ID an invitation belongs to
1177
+ * @param {string} userId The user ID the invitation was sent to
1178
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1179
+ */
1180
+ async revokeChannelAdminInvite(channelId, userId) {
1181
+ return await this.pupPage.evaluate(async (channelId, userId) => {
1182
+ try {
1183
+ const userWid = window.Store.WidFactory.createWid(userId);
1184
+ await window.Store.ChannelUtils.revokeNewsletterAdminInvite(channelId, userWid);
1185
+ return true;
1186
+ } catch (err) {
1187
+ if (err.name === 'ServerStatusCodeError') return false;
1188
+ throw err;
1189
+ }
1190
+ }, channelId, userId);
1191
+ }
1192
+
1193
+ /**
1194
+ * Demotes a channel admin to a regular subscriber (can be used also for self-demotion)
1195
+ * @param {string} channelId The channel ID to demote an admin in
1196
+ * @param {string} userId The user ID to demote
1197
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1198
+ */
1199
+ async demoteChannelAdmin(channelId, userId) {
1200
+ return await this.pupPage.evaluate(async (channelId, userId) => {
1201
+ try {
1202
+ const userWid = window.Store.WidFactory.createWid(userId);
1203
+ await window.Store.ChannelUtils.demoteNewsletterAdmin(channelId, userWid);
1204
+ return true;
1205
+ } catch (err) {
1206
+ if (err.name === 'ServerStatusCodeError') return false;
1207
+ throw err;
1208
+ }
1209
+ }, channelId, userId);
1210
+ }
1211
+
1079
1212
  /**
1080
1213
  * Accepts a private invitation to join a group
1081
1214
  * @param {object} inviteInfo Invite V4 Info
@@ -1152,7 +1285,7 @@ class Client extends EventEmitter {
1152
1285
  */
1153
1286
  async archiveChat(chatId) {
1154
1287
  return await this.pupPage.evaluate(async chatId => {
1155
- let chat = await window.Store.Chat.get(chatId);
1288
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1156
1289
  await window.Store.Cmd.archiveChat(chat, true);
1157
1290
  return true;
1158
1291
  }, chatId);
@@ -1164,7 +1297,7 @@ class Client extends EventEmitter {
1164
1297
  */
1165
1298
  async unarchiveChat(chatId) {
1166
1299
  return await this.pupPage.evaluate(async chatId => {
1167
- let chat = await window.Store.Chat.get(chatId);
1300
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1168
1301
  await window.Store.Cmd.archiveChat(chat, false);
1169
1302
  return false;
1170
1303
  }, chatId);
@@ -1176,7 +1309,7 @@ class Client extends EventEmitter {
1176
1309
  */
1177
1310
  async pinChat(chatId) {
1178
1311
  return this.pupPage.evaluate(async chatId => {
1179
- let chat = window.Store.Chat.get(chatId);
1312
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1180
1313
  if (chat.pin) {
1181
1314
  return true;
1182
1315
  }
@@ -1199,7 +1332,7 @@ class Client extends EventEmitter {
1199
1332
  */
1200
1333
  async unpinChat(chatId) {
1201
1334
  return this.pupPage.evaluate(async chatId => {
1202
- let chat = window.Store.Chat.get(chatId);
1335
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1203
1336
  if (!chat.pin) {
1204
1337
  return false;
1205
1338
  }
@@ -1211,25 +1344,38 @@ class Client extends EventEmitter {
1211
1344
  /**
1212
1345
  * Mutes this chat forever, unless a date is specified
1213
1346
  * @param {string} chatId ID of the chat that will be muted
1214
- * @param {?Date} unmuteDate Date when the chat will be unmuted, leave as is to mute forever
1347
+ * @param {?Date} unmuteDate Date when the chat will be unmuted, don't provide a value to mute forever
1348
+ * @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
1215
1349
  */
1216
1350
  async muteChat(chatId, unmuteDate) {
1217
- unmuteDate = unmuteDate ? unmuteDate.getTime() / 1000 : -1;
1218
- await this.pupPage.evaluate(async (chatId, timestamp) => {
1219
- let chat = await window.Store.Chat.get(chatId);
1220
- await chat.mute.mute({expiration: timestamp, sendDevice:!0});
1221
- }, chatId, unmuteDate || -1);
1351
+ unmuteDate = unmuteDate ? Math.floor(unmuteDate.getTime() / 1000) : -1;
1352
+ return this._muteUnmuteChat(chatId, 'MUTE', unmuteDate);
1222
1353
  }
1223
1354
 
1224
1355
  /**
1225
1356
  * Unmutes the Chat
1226
1357
  * @param {string} chatId ID of the chat that will be unmuted
1358
+ * @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
1227
1359
  */
1228
1360
  async unmuteChat(chatId) {
1229
- await this.pupPage.evaluate(async chatId => {
1230
- let chat = await window.Store.Chat.get(chatId);
1231
- await window.Store.Cmd.muteChat(chat, false);
1232
- }, chatId);
1361
+ return this._muteUnmuteChat(chatId, 'UNMUTE');
1362
+ }
1363
+
1364
+ /**
1365
+ * Internal method to mute or unmute the chat
1366
+ * @param {string} chatId ID of the chat that will be muted/unmuted
1367
+ * @param {string} action The action: 'MUTE' or 'UNMUTE'
1368
+ * @param {number} unmuteDateTs Timestamp at which the chat will be unmuted
1369
+ * @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
1370
+ */
1371
+ async _muteUnmuteChat (chatId, action, unmuteDateTs) {
1372
+ return this.pupPage.evaluate(async (chatId, action, unmuteDateTs) => {
1373
+ const chat = window.Store.Chat.get(chatId) ?? await window.Store.Chat.find(chatId);
1374
+ action === 'MUTE'
1375
+ ? await chat.mute.mute({ expiration: unmuteDateTs, sendDevice: true })
1376
+ : await chat.mute.unmute({ sendDevice: true });
1377
+ return { isMuted: chat.mute.expiration !== 0, muteExpiration: chat.mute.expiration };
1378
+ }, chatId, action, unmuteDateTs || -1);
1233
1379
  }
1234
1380
 
1235
1381
  /**
@@ -1238,7 +1384,7 @@ class Client extends EventEmitter {
1238
1384
  */
1239
1385
  async markChatUnread(chatId) {
1240
1386
  await this.pupPage.evaluate(async chatId => {
1241
- let chat = await window.Store.Chat.get(chatId);
1387
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1242
1388
  await window.Store.Cmd.markChatUnread(chat, true);
1243
1389
  }, chatId);
1244
1390
  }
@@ -1299,7 +1445,7 @@ class Client extends EventEmitter {
1299
1445
  */
1300
1446
  async resetState() {
1301
1447
  await this.pupPage.evaluate(() => {
1302
- window.Store.AppState.phoneWatchdog.shiftTimer.forceRunNow();
1448
+ window.Store.AppState.reconnect();
1303
1449
  });
1304
1450
  }
1305
1451
 
@@ -1440,7 +1586,7 @@ class Client extends EventEmitter {
1440
1586
  for (const participant of createGroupResult.participants) {
1441
1587
  let isInviteV4Sent = false;
1442
1588
  const participantId = participant.wid._serialized;
1443
- const statusCode = participant.error ?? 200;
1589
+ const statusCode = participant.error || 200;
1444
1590
 
1445
1591
  if (autoSendInviteV4 && statusCode === 403) {
1446
1592
  window.Store.Contact.gadd(participant.wid, { silent: true });
@@ -1479,6 +1625,219 @@ class Client extends EventEmitter {
1479
1625
  }, title, participants, options);
1480
1626
  }
1481
1627
 
1628
+ /**
1629
+ * An object that handles the result for {@link createChannel} method
1630
+ * @typedef {Object} CreateChannelResult
1631
+ * @property {string} title A channel title
1632
+ * @property {ChatId} nid An object that handels the newly created channel ID
1633
+ * @property {string} nid.server 'newsletter'
1634
+ * @property {string} nid.user 'XXXXXXXXXX'
1635
+ * @property {string} nid._serialized 'XXXXXXXXXX@newsletter'
1636
+ * @property {string} inviteLink The channel invite link, starts with 'https://whatsapp.com/channel/'
1637
+ * @property {number} createdAtTs The timestamp the channel was created at
1638
+ */
1639
+
1640
+ /**
1641
+ * Options for the channel creation
1642
+ * @typedef {Object} CreateChannelOptions
1643
+ * @property {?string} description The channel description
1644
+ * @property {?MessageMedia} picture The channel profile picture
1645
+ */
1646
+
1647
+ /**
1648
+ * Creates a new channel
1649
+ * @param {string} title The channel name
1650
+ * @param {CreateChannelOptions} options
1651
+ * @returns {Promise<CreateChannelResult|string>} Returns an object that handles the result for the channel creation or an error message as a string
1652
+ */
1653
+ async createChannel(title, options = {}) {
1654
+ return await this.pupPage.evaluate(async (title, options) => {
1655
+ let response, { description = null, picture = null } = options;
1656
+
1657
+ if (!window.Store.ChannelUtils.isNewsletterCreationEnabled()) {
1658
+ return 'CreateChannelError: A channel creation is not enabled';
1659
+ }
1660
+
1661
+ if (picture) {
1662
+ picture = await window.WWebJS.cropAndResizeImage(picture, {
1663
+ asDataUrl: true,
1664
+ mimetype: 'image/jpeg',
1665
+ size: 640,
1666
+ quality: 1
1667
+ });
1668
+ }
1669
+
1670
+ try {
1671
+ response = await window.Store.ChannelUtils.createNewsletterQuery({
1672
+ name: title,
1673
+ description: description,
1674
+ picture: picture,
1675
+ });
1676
+ } catch (err) {
1677
+ if (err.name === 'ServerStatusCodeError') {
1678
+ return 'CreateChannelError: An error occupied while creating a channel';
1679
+ }
1680
+ throw err;
1681
+ }
1682
+
1683
+ return {
1684
+ title: title,
1685
+ nid: window.Store.JidToWid.newsletterJidToWid(response.idJid),
1686
+ inviteLink: `https://whatsapp.com/channel/${response.newsletterInviteLinkMetadataMixin.inviteCode}`,
1687
+ createdAtTs: response.newsletterCreationTimeMetadataMixin.creationTimeValue
1688
+ };
1689
+ }, title, options);
1690
+ }
1691
+
1692
+ /**
1693
+ * Subscribe to channel
1694
+ * @param {string} channelId The channel ID
1695
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1696
+ */
1697
+ async subscribeToChannel(channelId) {
1698
+ return await this.pupPage.evaluate(async (channelId) => {
1699
+ return await window.WWebJS.subscribeToUnsubscribeFromChannel(channelId, 'Subscribe');
1700
+ }, channelId);
1701
+ }
1702
+
1703
+ /**
1704
+ * Options for unsubscribe from a channel
1705
+ * @typedef {Object} UnsubscribeOptions
1706
+ * @property {boolean} [deleteLocalModels = false] If true, after an unsubscription, it will completely remove a channel from the channel collection making it seem like the current user have never interacted with it. Otherwise it will only remove a channel from the list of channels the current user is subscribed to and will set the membership type for that channel to GUEST
1707
+ */
1708
+
1709
+ /**
1710
+ * Unsubscribe from channel
1711
+ * @param {string} channelId The channel ID
1712
+ * @param {UnsubscribeOptions} options
1713
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1714
+ */
1715
+ async unsubscribeFromChannel(channelId, options) {
1716
+ return await this.pupPage.evaluate(async (channelId, options) => {
1717
+ return await window.WWebJS.subscribeToUnsubscribeFromChannel(channelId, 'Unsubscribe', options);
1718
+ }, channelId, options);
1719
+ }
1720
+
1721
+ /**
1722
+ * Options for transferring a channel ownership to another user
1723
+ * @typedef {Object} TransferChannelOwnershipOptions
1724
+ * @property {boolean} [shouldDismissSelfAsAdmin = false] If true, after the channel ownership is being transferred to another user, the current user will be dismissed as a channel admin and will become to a channel subscriber.
1725
+ */
1726
+
1727
+ /**
1728
+ * Transfers a channel ownership to another user.
1729
+ * Note: the user you are transferring the channel ownership to must be a channel admin.
1730
+ * @param {string} channelId
1731
+ * @param {string} newOwnerId
1732
+ * @param {TransferChannelOwnershipOptions} options
1733
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1734
+ */
1735
+ async transferChannelOwnership(channelId, newOwnerId, options = {}) {
1736
+ return await this.pupPage.evaluate(async (channelId, newOwnerId, options) => {
1737
+ const channel = await window.WWebJS.getChat(channelId, { getAsModel: false });
1738
+ const newOwner = window.Store.Contact.get(newOwnerId) || (await window.Store.Contact.find(newOwnerId));
1739
+ if (!channel.newsletterMetadata) {
1740
+ await window.Store.NewsletterMetadataCollection.update(channel.id);
1741
+ }
1742
+
1743
+ try {
1744
+ await window.Store.ChannelUtils.changeNewsletterOwnerAction(channel, newOwner);
1745
+
1746
+ if (options.shouldDismissSelfAsAdmin) {
1747
+ const meContact = window.Store.ContactCollection.getMeContact();
1748
+ meContact && (await window.Store.ChannelUtils.demoteNewsletterAdminAction(channel, meContact));
1749
+ }
1750
+ } catch (error) {
1751
+ return false;
1752
+ }
1753
+
1754
+ return true;
1755
+ }, channelId, newOwnerId, options);
1756
+ }
1757
+
1758
+ /**
1759
+ * Searches for channels based on search criteria, there are some notes:
1760
+ * 1. The method finds only channels you are not subscribed to currently
1761
+ * 2. If you have never been subscribed to a found channel
1762
+ * or you have unsubscribed from it with {@link UnsubscribeOptions.deleteLocalModels} set to 'true',
1763
+ * the lastMessage property of a found channel will be 'null'
1764
+ *
1765
+ * @param {Object} searchOptions Search options
1766
+ * @param {string} [searchOptions.searchText = ''] Text to search
1767
+ * @param {Array<string>} [searchOptions.countryCodes = [your local region]] Array of country codes in 'ISO 3166-1 alpha-2' standart (@see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to search for channels created in these countries
1768
+ * @param {boolean} [searchOptions.skipSubscribedNewsletters = false] If true, channels that user is subscribed to won't appear in found channels
1769
+ * @param {number} [searchOptions.view = 0] View type, makes sense only when the searchText is empty. Valid values to provide are:
1770
+ * 0 for RECOMMENDED channels
1771
+ * 1 for TRENDING channels
1772
+ * 2 for POPULAR channels
1773
+ * 3 for NEW channels
1774
+ * @param {number} [searchOptions.limit = 50] The limit of found channels to be appear in the returnig result
1775
+ * @returns {Promise<Array<Channel>|[]>} Returns an array of Channel objects or an empty array if no channels were found
1776
+ */
1777
+ async searchChannels(searchOptions = {}) {
1778
+ return await this.pupPage.evaluate(async ({
1779
+ searchText = '',
1780
+ countryCodes = [window.Store.ChannelUtils.currentRegion],
1781
+ skipSubscribedNewsletters = false,
1782
+ view = 0,
1783
+ limit = 50
1784
+ }) => {
1785
+ searchText = searchText.trim();
1786
+ const currentRegion = window.Store.ChannelUtils.currentRegion;
1787
+ if (![0, 1, 2, 3].includes(view)) view = 0;
1788
+
1789
+ countryCodes = countryCodes.length === 1 && countryCodes[0] === currentRegion
1790
+ ? countryCodes
1791
+ : countryCodes.filter((code) => Object.keys(window.Store.ChannelUtils.countryCodesIso).includes(code));
1792
+
1793
+ const viewTypeMapping = {
1794
+ 0: 'RECOMMENDED',
1795
+ 1: 'TRENDING',
1796
+ 2: 'POPULAR',
1797
+ 3: 'NEW'
1798
+ };
1799
+
1800
+ searchOptions = {
1801
+ searchText: searchText,
1802
+ countryCodes: countryCodes,
1803
+ skipSubscribedNewsletters: skipSubscribedNewsletters,
1804
+ view: viewTypeMapping[view],
1805
+ categories: [],
1806
+ cursorToken: ''
1807
+ };
1808
+
1809
+ const originalFunction = window.Store.ChannelUtils.getNewsletterDirectoryPageSize;
1810
+ limit !== 50 && (window.Store.ChannelUtils.getNewsletterDirectoryPageSize = () => limit);
1811
+
1812
+ const channels = (await window.Store.ChannelUtils.fetchNewsletterDirectories(searchOptions)).newsletters;
1813
+
1814
+ limit !== 50 && (window.Store.ChannelUtils.getNewsletterDirectoryPageSize = originalFunction);
1815
+
1816
+ return channels
1817
+ ? await Promise.all(channels.map((channel) => window.WWebJS.getChatModel(channel, { isChannel: true })))
1818
+ : [];
1819
+ }, searchOptions);
1820
+ }
1821
+
1822
+ /**
1823
+ * Deletes the channel you created
1824
+ * @param {string} channelId The ID of a channel to delete
1825
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1826
+ */
1827
+ async deleteChannel(channelId) {
1828
+ return await this.client.pupPage.evaluate(async (channelId) => {
1829
+ const channel = await window.WWebJS.getChat(channelId, { getAsModel: false });
1830
+ if (!channel) return false;
1831
+ try {
1832
+ await window.Store.ChannelUtils.deleteNewsletterAction(channel);
1833
+ return true;
1834
+ } catch (err) {
1835
+ if (err.name === 'ServerStatusCodeError') return false;
1836
+ throw err;
1837
+ }
1838
+ }, channelId);
1839
+ }
1840
+
1482
1841
  /**
1483
1842
  * Get all current Labels
1484
1843
  * @returns {Promise<Array<Label>>}
@@ -1490,6 +1849,17 @@ class Client extends EventEmitter {
1490
1849
 
1491
1850
  return labels.map(data => new Label(this, data));
1492
1851
  }
1852
+
1853
+ /**
1854
+ * Get all current Broadcast
1855
+ * @returns {Promise<Array<Broadcast>>}
1856
+ */
1857
+ async getBroadcasts() {
1858
+ const broadcasts = await this.pupPage.evaluate(async () => {
1859
+ return window.WWebJS.getAllStatuses();
1860
+ });
1861
+ return broadcasts.map(data => new Broadcast(this, data));
1862
+ }
1493
1863
 
1494
1864
  /**
1495
1865
  * Get Label instance by ID
@@ -1728,20 +2098,91 @@ class Client extends EventEmitter {
1728
2098
  }, flag);
1729
2099
  }
1730
2100
 
2101
+ /**
2102
+ * Setting background synchronization.
2103
+ * NOTE: this action will take effect after you restart the client.
2104
+ * @param {boolean} flag true/false
2105
+ * @returns {Promise<boolean>}
2106
+ */
2107
+ async setBackgroundSync(flag) {
2108
+ return await this.pupPage.evaluate(async flag => {
2109
+ const backSync = window.Store.Settings.getGlobalOfflineNotifications();
2110
+ if (backSync === flag) {
2111
+ return flag;
2112
+ }
2113
+ await window.Store.Settings.setGlobalOfflineNotifications(flag);
2114
+ return flag;
2115
+ }, flag);
2116
+ }
2117
+
1731
2118
  /**
1732
2119
  * Get user device count by ID
1733
2120
  * Each WaWeb Connection counts as one device, and the phone (if exists) counts as one
1734
2121
  * So for a non-enterprise user with one WaWeb connection it should return "2"
1735
- * @param {string} contactId
1736
- * @returns {number}
2122
+ * @param {string} userId
2123
+ * @returns {Promise<number>}
1737
2124
  */
1738
- async getContactDeviceCount(contactId) {
1739
- let devices = await window.Store.DeviceList.getDeviceIds([window.Store.WidFactory.createWid(contactId)]);
1740
- if(devices && devices.length && devices[0] != null && typeof devices[0].devices == 'object'){
1741
- return devices[0].devices.length;
1742
- }
1743
- return 0;
2125
+ async getContactDeviceCount(userId) {
2126
+ return await this.pupPage.evaluate(async (userId) => {
2127
+ const devices = await window.Store.DeviceList.getDeviceIds([window.Store.WidFactory.createWid(userId)]);
2128
+ if (devices && devices.length && devices[0] != null && typeof devices[0].devices == 'object') {
2129
+ return devices[0].devices.length;
2130
+ }
2131
+ return 0;
2132
+ }, userId);
2133
+ }
2134
+
2135
+ /**
2136
+ * Sync chat history conversation
2137
+ * @param {string} chatId
2138
+ * @return {Promise<boolean>} True if operation completed successfully, false otherwise.
2139
+ */
2140
+ async syncHistory(chatId) {
2141
+ return await this.pupPage.evaluate(async (chatId) => {
2142
+ const chatWid = window.Store.WidFactory.createWid(chatId);
2143
+ const chat = window.Store.Chat.get(chatWid) ?? (await window.Store.Chat.find(chatWid));
2144
+ if (chat?.endOfHistoryTransferType === 0) {
2145
+ await window.Store.HistorySync.sendPeerDataOperationRequest(3, {
2146
+ chatId: chat.id
2147
+ });
2148
+ return true;
2149
+ }
2150
+ return false;
2151
+ }, chatId);
2152
+ }
2153
+
2154
+ /**
2155
+ * Save new contact to user's addressbook or edit the existing one
2156
+ * @param {string} phoneNumber The contact's phone number in a format "17182222222", where "1" is a country code
2157
+ * @param {string} firstName
2158
+ * @param {string} lastName
2159
+ * @param {boolean} [syncToAddressbook = false] If set to true, the contact will also be saved to the user's address book on their phone. False by default
2160
+ * @returns {Promise<import('..').ChatId>} Object in a wid format
2161
+ */
2162
+ async saveOrEditAddressbookContact(phoneNumber, firstName, lastName, syncToAddressbook = false)
2163
+ {
2164
+ return await this.pupPage.evaluate(async (phoneNumber, firstName, lastName, syncToAddressbook) => {
2165
+ return await window.Store.AddressbookContactUtils.saveContactAction(
2166
+ phoneNumber,
2167
+ null,
2168
+ firstName,
2169
+ lastName,
2170
+ syncToAddressbook
2171
+ );
2172
+ }, phoneNumber, firstName, lastName, syncToAddressbook);
2173
+ }
2174
+
2175
+ /**
2176
+ * Deletes the contact from user's addressbook
2177
+ * @param {string} phoneNumber The contact's phone number in a format "17182222222", where "1" is a country code
2178
+ * @returns {Promise<void>}
2179
+ */
2180
+ async deleteAddressbookContact(phoneNumber)
2181
+ {
2182
+ return await this.pupPage.evaluate(async (phoneNumber) => {
2183
+ return await window.Store.AddressbookContactUtils.deleteContactAction(phoneNumber);
2184
+ }, phoneNumber);
1744
2185
  }
1745
2186
  }
1746
2187
 
1747
- module.exports = Client;
2188
+ module.exports = Client;