whatsapp-web-jf.js 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.
Files changed (55) hide show
  1. package/.env.example +3 -0
  2. package/CODE_OF_CONDUCT.md +133 -0
  3. package/LICENSE +201 -0
  4. package/README.md +185 -0
  5. package/example.js +680 -0
  6. package/index.d.ts +2069 -0
  7. package/index.js +34 -0
  8. package/package.json +55 -0
  9. package/shell.js +36 -0
  10. package/src/Client.js +2188 -0
  11. package/src/authStrategies/BaseAuthStrategy.js +27 -0
  12. package/src/authStrategies/LocalAuth.js +58 -0
  13. package/src/authStrategies/NoAuth.js +12 -0
  14. package/src/authStrategies/RemoteAuth.js +210 -0
  15. package/src/factories/ChatFactory.js +21 -0
  16. package/src/factories/ContactFactory.js +16 -0
  17. package/src/structures/Base.js +22 -0
  18. package/src/structures/Broadcast.js +69 -0
  19. package/src/structures/BusinessContact.js +21 -0
  20. package/src/structures/Buttons.js +82 -0
  21. package/src/structures/Call.js +76 -0
  22. package/src/structures/Channel.js +382 -0
  23. package/src/structures/Chat.js +291 -0
  24. package/src/structures/ClientInfo.js +71 -0
  25. package/src/structures/Contact.js +208 -0
  26. package/src/structures/GroupChat.js +473 -0
  27. package/src/structures/GroupNotification.js +104 -0
  28. package/src/structures/Label.js +50 -0
  29. package/src/structures/List.js +79 -0
  30. package/src/structures/Location.js +62 -0
  31. package/src/structures/Message.js +704 -0
  32. package/src/structures/MessageMedia.js +111 -0
  33. package/src/structures/Order.js +52 -0
  34. package/src/structures/Payment.js +79 -0
  35. package/src/structures/Poll.js +44 -0
  36. package/src/structures/PollVote.js +61 -0
  37. package/src/structures/PrivateChat.js +13 -0
  38. package/src/structures/PrivateContact.js +13 -0
  39. package/src/structures/Product.js +68 -0
  40. package/src/structures/ProductMetadata.js +25 -0
  41. package/src/structures/Reaction.js +69 -0
  42. package/src/structures/index.js +26 -0
  43. package/src/util/Constants.js +176 -0
  44. package/src/util/Injected/AuthStore/AuthStore.js +17 -0
  45. package/src/util/Injected/AuthStore/LegacyAuthStore.js +22 -0
  46. package/src/util/Injected/LegacyStore.js +146 -0
  47. package/src/util/Injected/Store.js +215 -0
  48. package/src/util/Injected/Utils.js +1139 -0
  49. package/src/util/InterfaceController.js +126 -0
  50. package/src/util/Puppeteer.js +23 -0
  51. package/src/util/Util.js +186 -0
  52. package/src/webCache/LocalWebCache.js +40 -0
  53. package/src/webCache/RemoteWebCache.js +40 -0
  54. package/src/webCache/WebCache.js +14 -0
  55. package/src/webCache/WebCacheFactory.js +20 -0
package/src/Client.js ADDED
@@ -0,0 +1,2188 @@
1
+ 'use strict';
2
+
3
+ const EventEmitter = require('events');
4
+ const puppeteer = require('puppeteer');
5
+ const moduleRaid = require('@pedroslopez/moduleraid/moduleraid');
6
+
7
+ const Util = require('./util/Util');
8
+ const InterfaceController = require('./util/InterfaceController');
9
+ const { WhatsWebURL, DefaultOptions, Events, WAState } = require('./util/Constants');
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');
15
+ const ChatFactory = require('./factories/ChatFactory');
16
+ const ContactFactory = require('./factories/ContactFactory');
17
+ const WebCacheFactory = require('./webCache/WebCacheFactory');
18
+ const { Broadcast, Buttons, Call, ClientInfo, Contact, GroupNotification, Label, List, Location, Message, MessageMedia, Poll, PollVote, Reaction } = require('./structures');
19
+ const NoAuth = require('./authStrategies/NoAuth');
20
+ const {exposeFunctionIfAbsent} = require('./util/Puppeteer');
21
+ const pie = require('puppeteer-in-electron');
22
+
23
+ /**
24
+ * Starting point for interacting with the WhatsApp Web API
25
+ * @extends {EventEmitter}
26
+ * @param {object} options - Client options
27
+ * @param {AuthStrategy} options.authStrategy - Determines how to save and restore sessions. Will use LegacySessionAuth if options.session is set. Otherwise, NoAuth will be used.
28
+ * @param {string} options.webVersion - The version of WhatsApp Web to use. Use options.webVersionCache to configure how the version is retrieved.
29
+ * @param {object} options.webVersionCache - Determines how to retrieve the WhatsApp Web version. Defaults to a local cache (LocalWebCache) that falls back to latest if the requested version is not found.
30
+ * @param {number} options.authTimeoutMs - Timeout for authentication selector in puppeteer
31
+ * @param {object} options.puppeteer - Puppeteer launch options. View docs here: https://github.com/puppeteer/puppeteer/
32
+ * @param {number} options.qrMaxRetries - How many times should the qrcode be refreshed before giving up
33
+ * @param {string} options.restartOnAuthFail - @deprecated This option should be set directly on the LegacySessionAuth.
34
+ * @param {object} options.session - @deprecated Only here for backwards-compatibility. You should move to using LocalAuth, or set the authStrategy to LegacySessionAuth explicitly.
35
+ * @param {number} options.takeoverOnConflict - If another whatsapp web session is detected (another browser), take over the session in the current browser
36
+ * @param {number} options.takeoverTimeoutMs - How much time to wait before taking over the session
37
+ * @param {string} options.userAgent - User agent to use in puppeteer
38
+ * @param {string} options.ffmpegPath - Ffmpeg path to use when formatting videos to webp while sending stickers
39
+ * @param {boolean} options.bypassCSP - Sets bypassing of page's Content-Security-Policy.
40
+ * @param {object} options.proxyAuthentication - Proxy Authentication object.
41
+ *
42
+ * @fires Client#qr
43
+ * @fires Client#authenticated
44
+ * @fires Client#auth_failure
45
+ * @fires Client#ready
46
+ * @fires Client#message
47
+ * @fires Client#message_ack
48
+ * @fires Client#message_create
49
+ * @fires Client#message_revoke_me
50
+ * @fires Client#message_revoke_everyone
51
+ * @fires Client#message_ciphertext
52
+ * @fires Client#message_edit
53
+ * @fires Client#media_uploaded
54
+ * @fires Client#group_join
55
+ * @fires Client#group_leave
56
+ * @fires Client#group_update
57
+ * @fires Client#disconnected
58
+ * @fires Client#change_state
59
+ * @fires Client#contact_changed
60
+ * @fires Client#group_admin_changed
61
+ * @fires Client#group_membership_request
62
+ * @fires Client#vote_update
63
+ */
64
+ class Client extends EventEmitter {
65
+ constructor(puppeteerBrowser, browserWindow, options = {}) {
66
+ super();
67
+
68
+ this.options = Util.mergeDefault(DefaultOptions, options);
69
+
70
+ if(!this.options.authStrategy) {
71
+ this.authStrategy = new NoAuth();
72
+ } else {
73
+ this.authStrategy = this.options.authStrategy;
74
+ }
75
+
76
+ this.authStrategy.setup(this);
77
+
78
+ /**
79
+ * @type {puppeteer.Browser}
80
+ */
81
+ this.pupBrowser = puppeteerBrowser;
82
+ /**
83
+ * @type {puppeteer.Page}
84
+ */
85
+ this.browserWindow = browserWindow;
86
+
87
+ this.currentIndexHtml = null;
88
+ this.lastLoggedOut = false;
89
+
90
+ Util.setFfmpegPath(this.options.ffmpegPath);
91
+ }
92
+ /**
93
+ * Injection logic
94
+ * Private function
95
+ */
96
+ async inject() {
97
+ await this.pupPage.waitForFunction('window.Debug?.VERSION != undefined', {timeout: this.options.authTimeoutMs});
98
+
99
+ const version = await this.getWWebVersion();
100
+ const isCometOrAbove = parseInt(version.split('.')?.[1]) >= 3000;
101
+
102
+ if (isCometOrAbove) {
103
+ await this.pupPage.evaluate(ExposeAuthStore);
104
+ } else {
105
+ await this.pupPage.evaluate(ExposeLegacyAuthStore, moduleRaid.toString());
106
+ }
107
+
108
+ const needAuthentication = await this.pupPage.evaluate(async () => {
109
+ let state = window.AuthStore.AppState.state;
110
+
111
+ if (state === 'OPENING' || state === 'UNLAUNCHED' || state === 'PAIRING') {
112
+ // wait till state changes
113
+ await new Promise(r => {
114
+ window.AuthStore.AppState.on('change:state', function waitTillInit(_AppState, state) {
115
+ if (state !== 'OPENING' && state !== 'UNLAUNCHED' && state !== 'PAIRING') {
116
+ window.AuthStore.AppState.off('change:state', waitTillInit);
117
+ r();
118
+ }
119
+ });
120
+ });
121
+ }
122
+ state = window.AuthStore.AppState.state;
123
+ return state == 'UNPAIRED' || state == 'UNPAIRED_IDLE';
124
+ });
125
+
126
+ if (needAuthentication) {
127
+ const { failed, failureEventPayload, restart } = await this.authStrategy.onAuthenticationNeeded();
128
+
129
+ if(failed) {
130
+ /**
131
+ * Emitted when there has been an error while trying to restore an existing session
132
+ * @event Client#auth_failure
133
+ * @param {string} message
134
+ */
135
+ this.emit(Events.AUTHENTICATION_FAILURE, failureEventPayload);
136
+ await this.destroy();
137
+ if (restart) {
138
+ // session restore failed so try again but without session to force new authentication
139
+ return this.initialize();
140
+ }
141
+ return;
142
+ }
143
+
144
+ // Register qr events
145
+ let qrRetries = 0;
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();
158
+ }
159
+ }
160
+ });
161
+
162
+
163
+ await this.pupPage.evaluate(async () => {
164
+ const registrationInfo = await window.AuthStore.RegistrationUtils.waSignalStore.getRegistrationInfo();
165
+ const noiseKeyPair = await window.AuthStore.RegistrationUtils.waNoiseInfo.get();
166
+ const staticKeyB64 = window.AuthStore.Base64Tools.encodeB64(noiseKeyPair.staticKeyPair.pubKey);
167
+ const identityKeyB64 = window.AuthStore.Base64Tools.encodeB64(registrationInfo.identityKeyPair.pubKey);
168
+ const advSecretKey = await window.AuthStore.RegistrationUtils.getADVSecretKey();
169
+ const platform = window.AuthStore.RegistrationUtils.DEVICE_PLATFORM;
170
+ const getQR = (ref) => ref + ',' + staticKeyB64 + ',' + identityKeyB64 + ',' + advSecretKey + ',' + platform;
171
+
172
+ window.onQRChangedEvent(getQR(window.AuthStore.Conn.ref)); // initial qr
173
+ window.AuthStore.Conn.on('change:ref', (_, ref) => { window.onQRChangedEvent(getQR(ref)); }); // future QR changes
174
+ });
175
+ }
176
+
177
+ await exposeFunctionIfAbsent(this.pupPage, 'onAuthAppStateChangedEvent', async (state) => {
178
+ if (state == 'UNPAIRED_IDLE') {
179
+ // refresh qr code
180
+ window.Store.Cmd.refreshQR();
181
+ }
182
+ });
183
+
184
+ await exposeFunctionIfAbsent(this.pupPage, 'onAppStateHasSyncedEvent', async () => {
185
+ const authEventPayload = await this.authStrategy.getAuthEventPayload();
186
+ /**
187
+ * Emitted when authentication is successful
188
+ * @event Client#authenticated
189
+ */
190
+ this.emit(Events.AUTHENTICATED, authEventPayload);
191
+
192
+ const injected = await this.pupPage.evaluate(async () => {
193
+ return typeof window.Store !== 'undefined' && typeof window.WWebJS !== 'undefined';
194
+ });
195
+
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);
200
+
201
+ await webCache.persist(this.currentIndexHtml, version);
202
+ }
203
+
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
+ }
212
+
213
+ // Check window.Store Injection
214
+ await this.pupPage.waitForFunction('window.Store != undefined');
215
+
216
+ /**
217
+ * Current connection information
218
+ * @type {ClientInfo}
219
+ */
220
+ this.info = new ClientInfo(this, await this.pupPage.evaluate(() => {
221
+ return { ...window.Store.Conn.serialize(), wid: window.Store.User.getMeUser() };
222
+ }));
223
+
224
+ this.interface = new InterfaceController(this);
225
+
226
+ //Load util functions (serializers, helper functions)
227
+ await this.pupPage.evaluate(LoadUtils);
228
+
229
+ await this.attachEventListeners();
230
+ }
231
+ /**
232
+ * Emitted when the client has initialized and is ready to receive messages.
233
+ * @event Client#ready
234
+ */
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((_) => _);
248
+ });
249
+ await this.pupPage.evaluate(() => {
250
+ window.AuthStore.AppState.on('change:state', (_AppState, state) => { window.onAuthAppStateChangedEvent(state); });
251
+ window.AuthStore.AppState.on('change:hasSynced', () => { window.onAppStateHasSyncedEvent(); });
252
+ window.AuthStore.Cmd.on('offline_progress_update', () => {
253
+ window.onOfflineProgressUpdateEvent(window.AuthStore.OfflineMessageHandler.getOfflineDeliveryProgress());
254
+ });
255
+ window.AuthStore.Cmd.on('logout', async () => {
256
+ await window.onLogoutEvent();
257
+ });
258
+ });
259
+ }
260
+
261
+ /**
262
+ * Sets up events and requirements, kicks off authentication request
263
+ */
264
+ async initialize() {
265
+
266
+ // let
267
+ // /**
268
+ // * @type {puppeteer.Browser}
269
+ // */
270
+ // browser,
271
+ // /**
272
+ // * @type {puppeteer.Page}
273
+ // */
274
+ // page;
275
+
276
+ // browser = null;
277
+ // page = null;
278
+
279
+ // await this.authStrategy.beforeBrowserInitialized();
280
+
281
+ // const puppeteerOpts = this.options.puppeteer;
282
+ // if (puppeteerOpts && (puppeteerOpts.browserWSEndpoint || puppeteerOpts.browserURL)) {
283
+ // browser = await puppeteer.connect(puppeteerOpts);
284
+ // page = await browser.newPage();
285
+ // } else {
286
+ // const browserArgs = [...(puppeteerOpts.args || [])];
287
+ // if(!browserArgs.find(arg => arg.includes('--user-agent'))) {
288
+ // browserArgs.push(`--user-agent=${this.options.userAgent}`);
289
+ // }
290
+ // // navigator.webdriver fix
291
+ // browserArgs.push('--disable-blink-features=AutomationControlled');
292
+
293
+ // browser = await puppeteer.launch({...puppeteerOpts, args: browserArgs});
294
+ // page = (await browser.pages())[0];
295
+ // }
296
+
297
+ let page = await pie.getPage(this.pupBrowser, this.browserWindow);
298
+
299
+ if (this.options.proxyAuthentication !== undefined) {
300
+ await page.authenticate(this.options.proxyAuthentication);
301
+ }
302
+
303
+ await page.setUserAgent(this.options.userAgent);
304
+ if (this.options.bypassCSP) await page.setBypassCSP(true);
305
+
306
+ // this.pupBrowser = browser;
307
+ this.pupPage = page;
308
+
309
+ await this.authStrategy.afterBrowserInitialized();
310
+ await this.initWebVersionCache();
311
+
312
+ // ocVersion (isOfficialClient patch)
313
+ // remove after 2.3000.x hard release
314
+ await page.evaluateOnNewDocument(() => {
315
+ const originalError = Error;
316
+ window.originalError = originalError;
317
+ //eslint-disable-next-line no-global-assign
318
+ Error = function (message) {
319
+ const error = new originalError(message);
320
+ const originalStack = error.stack;
321
+ if (error.stack.includes('moduleRaid')) error.stack = originalStack + '\n at https://web.whatsapp.com/vendors~lazy_loaded_low_priority_components.05e98054dbd60f980427.js:2:44';
322
+ return error;
323
+ };
324
+ });
325
+
326
+ await page.goto(WhatsWebURL, {
327
+ waitUntil: 'load',
328
+ timeout: 0,
329
+ referer: 'https://whatsapp.com/'
330
+ });
331
+
332
+ await this.inject();
333
+
334
+ this.pupPage.on('framenavigated', async (frame) => {
335
+ if(frame.url().includes('post_logout=1') || this.lastLoggedOut) {
336
+ this.emit(Events.DISCONNECTED, 'LOGOUT');
337
+ await this.authStrategy.logout();
338
+ await this.authStrategy.beforeBrowserInitialized();
339
+ await this.authStrategy.afterBrowserInitialized();
340
+ this.lastLoggedOut = false;
341
+ }
342
+ await this.inject();
343
+ });
344
+ }
345
+
346
+ /**
347
+ * Request authentication via pairing code instead of QR code
348
+ * @param {string} phoneNumber - Phone number in international, symbol-free format (e.g. 12025550108 for US, 551155501234 for Brazil)
349
+ * @param {boolean} showNotification - Show notification to pair on phone number
350
+ * @returns {Promise<string>} - Returns a pairing code in format "ABCDEFGH"
351
+ */
352
+ async requestPairingCode(phoneNumber, showNotification = true) {
353
+ return await this.pupPage.evaluate(async (phoneNumber, showNotification) => {
354
+ window.AuthStore.PairingCodeLinkUtils.setPairingType('ALT_DEVICE_LINKING');
355
+ await window.AuthStore.PairingCodeLinkUtils.initializeAltDeviceLinking();
356
+ return window.AuthStore.PairingCodeLinkUtils.startAltLinkingFlow(phoneNumber, showNotification);
357
+ }, phoneNumber, showNotification);
358
+ }
359
+
360
+ /**
361
+ * Attach event listeners to WA Web
362
+ * Private function
363
+ * @property {boolean} reinject is this a reinject?
364
+ */
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
+ /**
371
+ * Emitted when a user joins the chat via invite link or is added by an admin.
372
+ * @event Client#group_join
373
+ * @param {GroupNotification} notification GroupNotification with more information about the action
374
+ */
375
+ this.emit(Events.GROUP_JOIN, notification);
376
+ } else if (msg.subtype === 'remove' || msg.subtype === 'leave') {
377
+ /**
378
+ * Emitted when a user leaves the chat or is removed by an admin.
379
+ * @event Client#group_leave
380
+ * @param {GroupNotification} notification GroupNotification with more information about the action
381
+ */
382
+ this.emit(Events.GROUP_LEAVE, notification);
383
+ } else if (msg.subtype === 'promote' || msg.subtype === 'demote') {
384
+ /**
385
+ * Emitted when a current user is promoted to an admin or demoted to a regular user.
386
+ * @event Client#group_admin_changed
387
+ * @param {GroupNotification} notification GroupNotification with more information about the action
388
+ */
389
+ this.emit(Events.GROUP_ADMIN_CHANGED, notification);
390
+ } else if (msg.subtype === 'membership_approval_request') {
391
+ /**
392
+ * Emitted when some user requested to join the group
393
+ * that has the membership approval mode turned on
394
+ * @event Client#group_membership_request
395
+ * @param {GroupNotification} notification GroupNotification with more information about the action
396
+ * @param {string} notification.chatId The group ID the request was made for
397
+ * @param {string} notification.author The user ID that made a request
398
+ * @param {number} notification.timestamp The timestamp the request was made at
399
+ */
400
+ this.emit(Events.GROUP_MEMBERSHIP_REQUEST, notification);
401
+ } else {
402
+ /**
403
+ * Emitted when group settings are updated, such as subject, description or picture.
404
+ * @event Client#group_update
405
+ * @param {GroupNotification} notification GroupNotification with more information about the action
406
+ */
407
+ this.emit(Events.GROUP_UPDATE, notification);
408
+ }
409
+ return;
410
+ }
411
+
412
+ const message = new Message(this, msg);
413
+
414
+ /**
415
+ * Emitted when a new message is created, which may include the current user's own messages.
416
+ * @event Client#message_create
417
+ * @param {Message} message The message that was created
418
+ */
419
+ this.emit(Events.MESSAGE_CREATE, message);
420
+
421
+ if (msg.id.fromMe) return;
422
+
423
+ /**
424
+ * Emitted when a new message is received.
425
+ * @event Client#message
426
+ * @param {Message} message The message that was received
427
+ */
428
+ this.emit(Events.MESSAGE_RECEIVED, message);
429
+ });
430
+
431
+ let last_message;
432
+
433
+ await exposeFunctionIfAbsent(this.pupPage, 'onChangeMessageTypeEvent', (msg) => {
434
+
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
+ }
441
+
442
+ /**
443
+ * Emitted when a message is deleted for everyone in the chat.
444
+ * @event Client#message_revoke_everyone
445
+ * @param {Message} message The message that was revoked, in its current state. It will not contain the original message's data.
446
+ * @param {?Message} revoked_msg The message that was revoked, before it was revoked. It will contain the message's original data.
447
+ * Note that due to the way this data is captured, it may be possible that this param will be undefined.
448
+ */
449
+ this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg);
450
+ }
451
+
452
+ });
453
+
454
+ await exposeFunctionIfAbsent(this.pupPage, 'onChangeMessageEvent', (msg) => {
455
+
456
+ if (msg.type !== 'revoked') {
457
+ last_message = msg;
458
+ }
459
+
460
+ /**
461
+ * The event notification that is received when one of
462
+ * the group participants changes their phone number.
463
+ */
464
+ const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify';
465
+
466
+ /**
467
+ * The event notification that is received when one of
468
+ * the contacts changes their phone number.
469
+ */
470
+ const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number';
471
+
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);
475
+
476
+ const newId = isParticipant ? msg.recipients[0] : msg.to;
477
+ const oldId = isParticipant ? msg.author : msg.templateParams.find(id => id !== newId);
478
+
479
+ /**
480
+ * Emitted when a contact or a group participant changes their phone number.
481
+ * @event Client#contact_changed
482
+ * @param {Message} message Message with more information about the event.
483
+ * @param {String} oldId The user's id (an old one) who changed their phone number
484
+ * and who triggered the notification.
485
+ * @param {String} newId The user's new id after the change.
486
+ * @param {Boolean} isContact Indicates if a contact or a group participant changed their phone number.
487
+ */
488
+ this.emit(Events.CONTACT_CHANGED, message, oldId, newId, isContact);
489
+ }
490
+ });
491
+
492
+ await exposeFunctionIfAbsent(this.pupPage, 'onRemoveMessageEvent', (msg) => {
493
+
494
+ if (!msg.isNewMsg) return;
495
+
496
+ const message = new Message(this, msg);
497
+
498
+ /**
499
+ * Emitted when a message is deleted by the current user.
500
+ * @event Client#message_revoke_me
501
+ * @param {Message} message The message that was revoked
502
+ */
503
+ this.emit(Events.MESSAGE_REVOKED_ME, message);
504
+
505
+ });
506
+
507
+ await exposeFunctionIfAbsent(this.pupPage, 'onMessageAckEvent', (msg, ack) => {
508
+
509
+ const message = new Message(this, msg);
510
+
511
+ /**
512
+ * Emitted when an ack event occurrs on message type.
513
+ * @event Client#message_ack
514
+ * @param {Message} message The message that was affected
515
+ * @param {MessageAck} ack The new ACK value
516
+ */
517
+ this.emit(Events.MESSAGE_ACK, message, ack);
518
+
519
+ });
520
+
521
+ await exposeFunctionIfAbsent(this.pupPage, 'onChatUnreadCountEvent', async (data) =>{
522
+ const chat = await this.getChatById(data.id);
523
+
524
+ /**
525
+ * Emitted when the chat unread count changes
526
+ */
527
+ this.emit(Events.UNREAD_COUNT, chat);
528
+ });
529
+
530
+ await exposeFunctionIfAbsent(this.pupPage, 'onMessageMediaUploadedEvent', (msg) => {
531
+
532
+ const message = new Message(this, msg);
533
+
534
+ /**
535
+ * Emitted when media has been uploaded for a message sent by the client.
536
+ * @event Client#media_uploaded
537
+ * @param {Message} message The message with media that was uploaded
538
+ */
539
+ this.emit(Events.MEDIA_UPLOADED, message);
540
+ });
541
+
542
+ await exposeFunctionIfAbsent(this.pupPage, 'onAppStateChangedEvent', async (state) => {
543
+ /**
544
+ * Emitted when the connection state changes
545
+ * @event Client#change_state
546
+ * @param {WAState} state the new connection state
547
+ */
548
+ this.emit(Events.STATE_CHANGED, state);
549
+
550
+ const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT];
551
+
552
+ if (this.options.takeoverOnConflict) {
553
+ ACCEPTED_STATES.push(WAState.CONFLICT);
554
+
555
+ if (state === WAState.CONFLICT) {
556
+ setTimeout(() => {
557
+ this.pupPage.evaluate(() => window.Store.AppState.takeover());
558
+ }, this.options.takeoverTimeoutMs);
559
+ }
560
+ }
561
+
562
+ if (!ACCEPTED_STATES.includes(state)) {
563
+ /**
564
+ * Emitted when the client has been disconnected
565
+ * @event Client#disconnected
566
+ * @param {WAState|"LOGOUT"} reason reason that caused the disconnect
567
+ */
568
+ await this.authStrategy.disconnect();
569
+ this.emit(Events.DISCONNECTED, state);
570
+ this.destroy();
571
+ }
572
+ });
573
+
574
+ await exposeFunctionIfAbsent(this.pupPage, 'onBatteryStateChangedEvent', (state) => {
575
+ const { battery, plugged } = state;
576
+
577
+ if (battery === undefined) return;
578
+
579
+ /**
580
+ * Emitted when the battery percentage for the attached device changes. Will not be sent if using multi-device.
581
+ * @event Client#change_battery
582
+ * @param {object} batteryInfo
583
+ * @param {number} batteryInfo.battery - The current battery percentage
584
+ * @param {boolean} batteryInfo.plugged - Indicates if the phone is plugged in (true) or not (false)
585
+ * @deprecated
586
+ */
587
+ this.emit(Events.BATTERY_CHANGED, { battery, plugged });
588
+ });
589
+
590
+ await exposeFunctionIfAbsent(this.pupPage, 'onIncomingCall', (call) => {
591
+ /**
592
+ * Emitted when a call is received
593
+ * @event Client#incoming_call
594
+ * @param {object} call
595
+ * @param {number} call.id - Call id
596
+ * @param {string} call.peerJid - Who called
597
+ * @param {boolean} call.isVideo - if is video
598
+ * @param {boolean} call.isGroup - if is group
599
+ * @param {boolean} call.canHandleLocally - if we can handle in waweb
600
+ * @param {boolean} call.outgoing - if is outgoing
601
+ * @param {boolean} call.webClientShouldHandle - If Waweb should handle
602
+ * @param {object} call.participants - Participants
603
+ */
604
+ const cll = new Call(this, call);
605
+ this.emit(Events.INCOMING_CALL, cll);
606
+ });
607
+
608
+ await exposeFunctionIfAbsent(this.pupPage, 'onReaction', (reactions) => {
609
+ for (const reaction of reactions) {
610
+ /**
611
+ * Emitted when a reaction is sent, received, updated or removed
612
+ * @event Client#message_reaction
613
+ * @param {object} reaction
614
+ * @param {object} reaction.id - Reaction id
615
+ * @param {number} reaction.orphan - Orphan
616
+ * @param {?string} reaction.orphanReason - Orphan reason
617
+ * @param {number} reaction.timestamp - Timestamp
618
+ * @param {string} reaction.reaction - Reaction
619
+ * @param {boolean} reaction.read - Read
620
+ * @param {object} reaction.msgId - Parent message id
621
+ * @param {string} reaction.senderId - Sender id
622
+ * @param {?number} reaction.ack - Ack
623
+ */
624
+
625
+ this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction));
626
+ }
627
+ });
628
+
629
+ await exposeFunctionIfAbsent(this.pupPage, 'onRemoveChatEvent', async (chat) => {
630
+ const _chat = await this.getChatById(chat.id);
631
+
632
+ /**
633
+ * Emitted when a chat is removed
634
+ * @event Client#chat_removed
635
+ * @param {Chat} chat
636
+ */
637
+ this.emit(Events.CHAT_REMOVED, _chat);
638
+ });
639
+
640
+ await exposeFunctionIfAbsent(this.pupPage, 'onArchiveChatEvent', async (chat, currState, prevState) => {
641
+ const _chat = await this.getChatById(chat.id);
642
+
643
+ /**
644
+ * Emitted when a chat is archived/unarchived
645
+ * @event Client#chat_archived
646
+ * @param {Chat} chat
647
+ * @param {boolean} currState
648
+ * @param {boolean} prevState
649
+ */
650
+ this.emit(Events.CHAT_ARCHIVED, _chat, currState, prevState);
651
+ });
652
+
653
+ await exposeFunctionIfAbsent(this.pupPage, 'onEditMessageEvent', (msg, newBody, prevBody) => {
654
+
655
+ if(msg.type === 'revoked'){
656
+ return;
657
+ }
658
+ /**
659
+ * Emitted when messages are edited
660
+ * @event Client#message_edit
661
+ * @param {Message} message
662
+ * @param {string} newBody
663
+ * @param {string} prevBody
664
+ */
665
+ this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody);
666
+ });
667
+
668
+ await exposeFunctionIfAbsent(this.pupPage, 'onAddMessageCiphertextEvent', msg => {
669
+
670
+ /**
671
+ * Emitted when messages are edited
672
+ * @event Client#message_ciphertext
673
+ * @param {Message} message
674
+ */
675
+ this.emit(Events.MESSAGE_CIPHERTEXT, new Message(this, msg));
676
+ });
677
+
678
+ await exposeFunctionIfAbsent(this.pupPage, 'onPollVoteEvent', (vote) => {
679
+ const _vote = new PollVote(this, vote);
680
+ /**
681
+ * Emitted when some poll option is selected or deselected,
682
+ * shows a user's current selected option(s) on the poll
683
+ * @event Client#vote_update
684
+ */
685
+ this.emit(Events.VOTE_UPDATE, _vote);
686
+ });
687
+
688
+ await this.pupPage.evaluate(() => {
689
+ window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(window.WWebJS.getMessageModel(msg)); });
690
+ window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(window.WWebJS.getMessageModel(msg)); });
691
+ window.Store.Msg.on('change:ack', (msg, ack) => { window.onMessageAckEvent(window.WWebJS.getMessageModel(msg), ack); });
692
+ window.Store.Msg.on('change:isUnsentMedia', (msg, unsent) => { if (msg.id.fromMe && !unsent) window.onMessageMediaUploadedEvent(window.WWebJS.getMessageModel(msg)); });
693
+ window.Store.Msg.on('remove', (msg) => { if (msg.isNewMsg) window.onRemoveMessageEvent(window.WWebJS.getMessageModel(msg)); });
694
+ window.Store.Msg.on('change:body change:caption', (msg, newBody, prevBody) => { window.onEditMessageEvent(window.WWebJS.getMessageModel(msg), newBody, prevBody); });
695
+ window.Store.AppState.on('change:state', (_AppState, state) => { window.onAppStateChangedEvent(state); });
696
+ window.Store.Conn.on('change:battery', (state) => { window.onBatteryStateChangedEvent(state); });
697
+ window.Store.Call.on('add', (call) => { window.onIncomingCall(call); });
698
+ window.Store.Chat.on('remove', async (chat) => { window.onRemoveChatEvent(await window.WWebJS.getChatModel(chat)); });
699
+ window.Store.Chat.on('change:archive', async (chat, currState, prevState) => { window.onArchiveChatEvent(await window.WWebJS.getChatModel(chat), currState, prevState); });
700
+ window.Store.Msg.on('add', (msg) => {
701
+ if (msg.isNewMsg) {
702
+ if(msg.type === 'ciphertext') {
703
+ // defer message event until ciphertext is resolved (type changed)
704
+ msg.once('change:type', (_msg) => window.onAddMessageEvent(window.WWebJS.getMessageModel(_msg)));
705
+ window.onAddMessageCiphertextEvent(window.WWebJS.getMessageModel(msg));
706
+ } else {
707
+ window.onAddMessageEvent(window.WWebJS.getMessageModel(msg));
708
+ }
709
+ }
710
+ });
711
+ window.Store.Chat.on('change:unreadCount', (chat) => {window.onChatUnreadCountEvent(chat);});
712
+ window.Store.PollVote.on('add', async (vote) => {
713
+ const pollVoteModel = await window.WWebJS.getPollVoteModel(vote);
714
+ pollVoteModel && window.onPollVoteEvent(pollVoteModel);
715
+ });
716
+
717
+ if (window.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.1014111620')) {
718
+ const module = window.Store.AddonReactionTable;
719
+ const ogMethod = module.bulkUpsert;
720
+ module.bulkUpsert = ((...args) => {
721
+ window.onReaction(args[0].map(reaction => {
722
+ const msgKey = reaction.id;
723
+ const parentMsgKey = reaction.reactionParentKey;
724
+ const timestamp = reaction.reactionTimestamp / 1000;
725
+ const sender = reaction.author ?? reaction.from;
726
+ const senderUserJid = sender._serialized;
727
+
728
+ return {...reaction, msgKey, parentMsgKey, senderUserJid, timestamp };
729
+ }));
730
+
731
+ return ogMethod(...args);
732
+ }).bind(module);
733
+ } else {
734
+ const module = window.Store.createOrUpdateReactionsModule;
735
+ const ogMethod = module.createOrUpdateReactions;
736
+ module.createOrUpdateReactions = ((...args) => {
737
+ window.onReaction(args[0].map(reaction => {
738
+ const msgKey = window.Store.MsgKey.fromString(reaction.msgKey);
739
+ const parentMsgKey = window.Store.MsgKey.fromString(reaction.parentMsgKey);
740
+ const timestamp = reaction.timestamp / 1000;
741
+
742
+ return {...reaction, msgKey, parentMsgKey, timestamp };
743
+ }));
744
+
745
+ return ogMethod(...args);
746
+ }).bind(module);
747
+ }
748
+ });
749
+ }
750
+
751
+ async initWebVersionCache() {
752
+ const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache;
753
+ const webCache = WebCacheFactory.createWebCache(webCacheType, webCacheOptions);
754
+
755
+ const requestedVersion = this.options.webVersion;
756
+ const versionContent = await webCache.resolve(requestedVersion);
757
+
758
+ if(versionContent) {
759
+ await this.pupPage.setRequestInterception(true);
760
+ this.pupPage.on('request', async (req) => {
761
+ if(req.url() === WhatsWebURL) {
762
+ req.respond({
763
+ status: 200,
764
+ contentType: 'text/html',
765
+ body: versionContent
766
+ });
767
+ } else {
768
+ req.continue();
769
+ }
770
+ });
771
+ } else {
772
+ this.pupPage.on('response', async (res) => {
773
+ if(res.ok() && res.url() === WhatsWebURL) {
774
+ const indexHtml = await res.text();
775
+ this.currentIndexHtml = indexHtml;
776
+ }
777
+ });
778
+ }
779
+ }
780
+
781
+ /**
782
+ * Closes the client
783
+ */
784
+ async destroy() {
785
+ await this.pupBrowser.close();
786
+ await this.authStrategy.destroy();
787
+ }
788
+
789
+ /**
790
+ * Logs out the client, closing the current session
791
+ */
792
+ async logout() {
793
+ await this.pupPage.evaluate(() => {
794
+ if (window.Store && window.Store.AppState && typeof window.Store.AppState.logout === 'function') {
795
+ return window.Store.AppState.logout();
796
+ }
797
+ });
798
+ // await this.pupBrowser.close();
799
+
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
+ // }
805
+
806
+ // await this.authStrategy.logout();
807
+ }
808
+
809
+ /**
810
+ * Returns the version of WhatsApp Web currently being run
811
+ * @returns {Promise<string>}
812
+ */
813
+ async getWWebVersion() {
814
+ return await this.pupPage.evaluate(() => {
815
+ return window.Debug.VERSION;
816
+ });
817
+ }
818
+
819
+ /**
820
+ * Mark as seen for the Chat
821
+ * @param {string} chatId
822
+ * @returns {Promise<boolean>} result
823
+ *
824
+ */
825
+ async sendSeen(chatId) {
826
+ return await this.pupPage.evaluate(async (chatId) => {
827
+ return window.WWebJS.sendSeen(chatId);
828
+ }, chatId);
829
+ }
830
+
831
+ /**
832
+ * An object representing mentions of groups
833
+ * @typedef {Object} GroupMention
834
+ * @property {string} subject - The name of a group to mention (can be custom)
835
+ * @property {string} id - The group ID, e.g.: 'XXXXXXXXXX@g.us'
836
+ */
837
+
838
+ /**
839
+ * Message options.
840
+ * @typedef {Object} MessageSendOptions
841
+ * @property {boolean} [linkPreview=true] - Show links preview. Has no effect on multi-device accounts.
842
+ * @property {boolean} [sendAudioAsVoice=false] - Send audio as voice message with a generated waveform
843
+ * @property {boolean} [sendVideoAsGif=false] - Send video as gif
844
+ * @property {boolean} [sendMediaAsSticker=false] - Send media as a sticker
845
+ * @property {boolean} [sendMediaAsDocument=false] - Send media as a document
846
+ * @property {boolean} [sendMediaAsHd=false] - Send image as quality HD
847
+ * @property {boolean} [isViewOnce=false] - Send photo/video as a view once message
848
+ * @property {boolean} [parseVCards=true] - Automatically parse vCards and send them as contacts
849
+ * @property {string} [caption] - Image or video caption
850
+ * @property {string} [quotedMessageId] - Id of the message that is being quoted (or replied to)
851
+ * @property {GroupMention[]} [groupMentions] - An array of object that handle group mentions
852
+ * @property {string[]} [mentions] - User IDs to mention in the message
853
+ * @property {boolean} [sendSeen=true] - Mark the conversation as seen after sending the message
854
+ * @property {string} [invokedBotWid=undefined] - Bot Wid when doing a bot mention like @Meta AI
855
+ * @property {string} [stickerAuthor=undefined] - Sets the author of the sticker, (if sendMediaAsSticker is true).
856
+ * @property {string} [stickerName=undefined] - Sets the name of the sticker, (if sendMediaAsSticker is true).
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?
859
+ * @property {MessageMedia} [media] - Media to be sent
860
+ * @property {any} [extra] - Extra options
861
+ */
862
+
863
+ /**
864
+ * Send a message to a specific chatId
865
+ * @param {string} chatId
866
+ * @param {string|MessageMedia|Location|Poll|Contact|Array<Contact>|Buttons|List} content
867
+ * @param {MessageSendOptions} [options] - Options used when sending the message
868
+ *
869
+ * @returns {Promise<Message>} Message that was just sent
870
+ */
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
+
885
+ if (options.mentions) {
886
+ !Array.isArray(options.mentions) && (options.mentions = [options.mentions]);
887
+ if (options.mentions.some((possiblyContact) => possiblyContact instanceof Contact)) {
888
+ console.warn('Mentions with an array of Contact are now deprecated. See more at https://github.com/pedroslopez/whatsapp-web.js/pull/2166.');
889
+ options.mentions = options.mentions.map((a) => a.id._serialized);
890
+ }
891
+ }
892
+
893
+ options.groupMentions && !Array.isArray(options.groupMentions) && (options.groupMentions = [options.groupMentions]);
894
+
895
+ let internalOptions = {
896
+ linkPreview: options.linkPreview === false ? undefined : true,
897
+ sendAudioAsVoice: options.sendAudioAsVoice,
898
+ sendVideoAsGif: options.sendVideoAsGif,
899
+ sendMediaAsSticker: options.sendMediaAsSticker,
900
+ sendMediaAsDocument: options.sendMediaAsDocument,
901
+ sendMediaAsHd: options.sendMediaAsHd,
902
+ caption: options.caption,
903
+ quotedMessageId: options.quotedMessageId,
904
+ parseVCards: options.parseVCards !== false,
905
+ mentionedJidList: options.mentions || [],
906
+ groupMentions: options.groupMentions,
907
+ invokedBotWid: options.invokedBotWid,
908
+ ignoreQuoteErrors: options.ignoreQuoteErrors !== false,
909
+ extraOptions: options.extra
910
+ };
911
+
912
+ const sendSeen = options.sendSeen !== false;
913
+
914
+ if (content instanceof MessageMedia) {
915
+ internalOptions.media = content;
916
+ internalOptions.isViewOnce = options.isViewOnce,
917
+ content = '';
918
+ } else if (options.media instanceof MessageMedia) {
919
+ internalOptions.media = options.media;
920
+ internalOptions.caption = content;
921
+ internalOptions.isViewOnce = options.isViewOnce,
922
+ content = '';
923
+ } else if (content instanceof Location) {
924
+ internalOptions.location = content;
925
+ content = '';
926
+ } else if (content instanceof Poll) {
927
+ internalOptions.poll = content;
928
+ content = '';
929
+ } else if (content instanceof Contact) {
930
+ internalOptions.contactCard = content.id._serialized;
931
+ content = '';
932
+ } else if (Array.isArray(content) && content.length > 0 && content[0] instanceof Contact) {
933
+ internalOptions.contactCardList = content.map(contact => contact.id._serialized);
934
+ content = '';
935
+ } else if (content instanceof Buttons) {
936
+ console.warn('Buttons are now deprecated. See more at https://www.youtube.com/watch?v=hv1R1rLeVVE.');
937
+ if (content.type !== 'chat') { internalOptions.attachment = content.body; }
938
+ internalOptions.buttons = content;
939
+ content = '';
940
+ } else if (content instanceof List) {
941
+ console.warn('Lists are now deprecated. See more at https://www.youtube.com/watch?v=hv1R1rLeVVE.');
942
+ internalOptions.list = content;
943
+ content = '';
944
+ }
945
+
946
+ if (internalOptions.sendMediaAsSticker && internalOptions.media) {
947
+ internalOptions.media = await Util.formatToWebpSticker(
948
+ internalOptions.media, {
949
+ name: options.stickerName,
950
+ author: options.stickerAuthor,
951
+ categories: options.stickerCategories
952
+ }, this.pupPage
953
+ );
954
+ }
955
+
956
+ const sentMsg = await this.pupPage.evaluate(async (chatId, content, options, sendSeen) => {
957
+ const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
958
+
959
+ if (!chat) return null;
960
+
961
+ if (sendSeen) {
962
+ await window.WWebJS.sendSeen(chatId);
963
+ }
964
+
965
+ const msg = await window.WWebJS.sendMessage(chat, content, options);
966
+ return msg
967
+ ? window.WWebJS.getMessageModel(msg)
968
+ : undefined;
969
+ }, chatId, content, internalOptions, sendSeen);
970
+
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';
1010
+ }
1011
+
1012
+ /**
1013
+ * Searches for messages
1014
+ * @param {string} query
1015
+ * @param {Object} [options]
1016
+ * @param {number} [options.page]
1017
+ * @param {number} [options.limit]
1018
+ * @param {string} [options.chatId]
1019
+ * @returns {Promise<Message[]>}
1020
+ */
1021
+ async searchMessages(query, options = {}) {
1022
+ const messages = await this.pupPage.evaluate(async (query, page, count, remote) => {
1023
+ const { messages } = await window.Store.Msg.search(query, page, count, remote);
1024
+ return messages.map(msg => window.WWebJS.getMessageModel(msg));
1025
+ }, query, options.page, options.limit, options.chatId);
1026
+
1027
+ return messages.map(msg => new Message(this, msg));
1028
+ }
1029
+
1030
+ /**
1031
+ * Get all current chat instances
1032
+ * @returns {Promise<Array<Chat>>}
1033
+ */
1034
+ async getChats() {
1035
+ const chats = await this.pupPage.evaluate(async () => {
1036
+ return await window.WWebJS.getChats();
1037
+ });
1038
+
1039
+ return chats.map(chat => ChatFactory.create(this, chat));
1040
+ }
1041
+
1042
+ /**
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
1056
+ * @param {string} chatId
1057
+ * @returns {Promise<Chat|Channel>}
1058
+ */
1059
+ async getChatById(chatId) {
1060
+ const chat = await this.pupPage.evaluate(async chatId => {
1061
+ return await window.WWebJS.getChat(chatId);
1062
+ }, chatId);
1063
+ return chat
1064
+ ? ChatFactory.create(this, chat)
1065
+ : undefined;
1066
+ }
1067
+
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;
1088
+ }
1089
+
1090
+ /**
1091
+ * Get all current contact instances
1092
+ * @returns {Promise<Array<Contact>>}
1093
+ */
1094
+ async getContacts() {
1095
+ let contacts = await this.pupPage.evaluate(() => {
1096
+ return window.WWebJS.getContacts();
1097
+ });
1098
+
1099
+ return contacts.map(contact => ContactFactory.create(this, contact));
1100
+ }
1101
+
1102
+ /**
1103
+ * Get contact instance by ID
1104
+ * @param {string} contactId
1105
+ * @returns {Promise<Contact>}
1106
+ */
1107
+ async getContactById(contactId) {
1108
+ let contact = await this.pupPage.evaluate(contactId => {
1109
+ return window.WWebJS.getContact(contactId);
1110
+ }, contactId);
1111
+
1112
+ return ContactFactory.create(this, contact);
1113
+ }
1114
+
1115
+ async getMessageById(messageId) {
1116
+ const msg = await this.pupPage.evaluate(async messageId => {
1117
+ let msg = window.Store.Msg.get(messageId);
1118
+ if(msg) return window.WWebJS.getMessageModel(msg);
1119
+
1120
+ const params = messageId.split('_');
1121
+ if (params.length !== 3 && params.length !== 4) throw new Error('Invalid serialized message id specified');
1122
+
1123
+ let messagesObject = await window.Store.Msg.getMessagesById([messageId]);
1124
+ if (messagesObject && messagesObject.messages.length) msg = messagesObject.messages[0];
1125
+
1126
+ if(msg) return window.WWebJS.getMessageModel(msg);
1127
+ }, messageId);
1128
+
1129
+ if(msg) return new Message(this, msg);
1130
+ return null;
1131
+ }
1132
+
1133
+ /**
1134
+ * Returns an object with information about the invite code's group
1135
+ * @param {string} inviteCode
1136
+ * @returns {Promise<object>} Invite information
1137
+ */
1138
+ async getInviteInfo(inviteCode) {
1139
+ return await this.pupPage.evaluate(inviteCode => {
1140
+ return window.Store.GroupInvite.queryGroupInvite(inviteCode);
1141
+ }, inviteCode);
1142
+ }
1143
+
1144
+ /**
1145
+ * Accepts an invitation to join a group
1146
+ * @param {string} inviteCode Invitation code
1147
+ * @returns {Promise<string>} Id of the joined Chat
1148
+ */
1149
+ async acceptInvite(inviteCode) {
1150
+ const res = await this.pupPage.evaluate(async inviteCode => {
1151
+ return await window.Store.GroupInvite.joinGroupViaInvite(inviteCode);
1152
+ }, inviteCode);
1153
+
1154
+ return res.gid._serialized;
1155
+ }
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
+
1212
+ /**
1213
+ * Accepts a private invitation to join a group
1214
+ * @param {object} inviteInfo Invite V4 Info
1215
+ * @returns {Promise<Object>}
1216
+ */
1217
+ async acceptGroupV4Invite(inviteInfo) {
1218
+ if (!inviteInfo.inviteCode) throw 'Invalid invite code, try passing the message.inviteV4 object';
1219
+ if (inviteInfo.inviteCodeExp == 0) throw 'Expired invite code';
1220
+ return this.pupPage.evaluate(async inviteInfo => {
1221
+ let { groupId, fromId, inviteCode, inviteCodeExp } = inviteInfo;
1222
+ let userWid = window.Store.WidFactory.createWid(fromId);
1223
+ return await window.Store.GroupInviteV4.joinGroupViaInviteV4(inviteCode, String(inviteCodeExp), groupId, userWid);
1224
+ }, inviteInfo);
1225
+ }
1226
+
1227
+ /**
1228
+ * Sets the current user's status message
1229
+ * @param {string} status New status message
1230
+ */
1231
+ async setStatus(status) {
1232
+ await this.pupPage.evaluate(async status => {
1233
+ return await window.Store.StatusUtils.setMyStatus(status);
1234
+ }, status);
1235
+ }
1236
+
1237
+ /**
1238
+ * Sets the current user's display name.
1239
+ * This is the name shown to WhatsApp users that have not added you as a contact beside your number in groups and in your profile.
1240
+ * @param {string} displayName New display name
1241
+ * @returns {Promise<Boolean>}
1242
+ */
1243
+ async setDisplayName(displayName) {
1244
+ const couldSet = await this.pupPage.evaluate(async displayName => {
1245
+ if(!window.Store.Conn.canSetMyPushname()) return false;
1246
+ await window.Store.Settings.setPushname(displayName);
1247
+ return true;
1248
+ }, displayName);
1249
+
1250
+ return couldSet;
1251
+ }
1252
+
1253
+ /**
1254
+ * Gets the current connection state for the client
1255
+ * @returns {WAState}
1256
+ */
1257
+ async getState() {
1258
+ return await this.pupPage.evaluate(() => {
1259
+ if(!window.Store) return null;
1260
+ return window.Store.AppState.state;
1261
+ });
1262
+ }
1263
+
1264
+ /**
1265
+ * Marks the client as online
1266
+ */
1267
+ async sendPresenceAvailable() {
1268
+ return await this.pupPage.evaluate(() => {
1269
+ return window.Store.PresenceUtils.sendPresenceAvailable();
1270
+ });
1271
+ }
1272
+
1273
+ /**
1274
+ * Marks the client as unavailable
1275
+ */
1276
+ async sendPresenceUnavailable() {
1277
+ return await this.pupPage.evaluate(() => {
1278
+ return window.Store.PresenceUtils.sendPresenceUnavailable();
1279
+ });
1280
+ }
1281
+
1282
+ /**
1283
+ * Enables and returns the archive state of the Chat
1284
+ * @returns {boolean}
1285
+ */
1286
+ async archiveChat(chatId) {
1287
+ return await this.pupPage.evaluate(async chatId => {
1288
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1289
+ await window.Store.Cmd.archiveChat(chat, true);
1290
+ return true;
1291
+ }, chatId);
1292
+ }
1293
+
1294
+ /**
1295
+ * Changes and returns the archive state of the Chat
1296
+ * @returns {boolean}
1297
+ */
1298
+ async unarchiveChat(chatId) {
1299
+ return await this.pupPage.evaluate(async chatId => {
1300
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1301
+ await window.Store.Cmd.archiveChat(chat, false);
1302
+ return false;
1303
+ }, chatId);
1304
+ }
1305
+
1306
+ /**
1307
+ * Pins the Chat
1308
+ * @returns {Promise<boolean>} New pin state. Could be false if the max number of pinned chats was reached.
1309
+ */
1310
+ async pinChat(chatId) {
1311
+ return this.pupPage.evaluate(async chatId => {
1312
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1313
+ if (chat.pin) {
1314
+ return true;
1315
+ }
1316
+ const MAX_PIN_COUNT = 3;
1317
+ const chatModels = window.Store.Chat.getModelsArray();
1318
+ if (chatModels.length > MAX_PIN_COUNT) {
1319
+ let maxPinned = chatModels[MAX_PIN_COUNT - 1].pin;
1320
+ if (maxPinned) {
1321
+ return false;
1322
+ }
1323
+ }
1324
+ await window.Store.Cmd.pinChat(chat, true);
1325
+ return true;
1326
+ }, chatId);
1327
+ }
1328
+
1329
+ /**
1330
+ * Unpins the Chat
1331
+ * @returns {Promise<boolean>} New pin state
1332
+ */
1333
+ async unpinChat(chatId) {
1334
+ return this.pupPage.evaluate(async chatId => {
1335
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1336
+ if (!chat.pin) {
1337
+ return false;
1338
+ }
1339
+ await window.Store.Cmd.pinChat(chat, false);
1340
+ return false;
1341
+ }, chatId);
1342
+ }
1343
+
1344
+ /**
1345
+ * Mutes this chat forever, unless a date is specified
1346
+ * @param {string} chatId ID of the chat that will be muted
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}>}
1349
+ */
1350
+ async muteChat(chatId, unmuteDate) {
1351
+ unmuteDate = unmuteDate ? Math.floor(unmuteDate.getTime() / 1000) : -1;
1352
+ return this._muteUnmuteChat(chatId, 'MUTE', unmuteDate);
1353
+ }
1354
+
1355
+ /**
1356
+ * Unmutes the Chat
1357
+ * @param {string} chatId ID of the chat that will be unmuted
1358
+ * @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
1359
+ */
1360
+ async unmuteChat(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);
1379
+ }
1380
+
1381
+ /**
1382
+ * Mark the Chat as unread
1383
+ * @param {string} chatId ID of the chat that will be marked as unread
1384
+ */
1385
+ async markChatUnread(chatId) {
1386
+ await this.pupPage.evaluate(async chatId => {
1387
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1388
+ await window.Store.Cmd.markChatUnread(chat, true);
1389
+ }, chatId);
1390
+ }
1391
+
1392
+ /**
1393
+ * Returns the contact ID's profile picture URL, if privacy settings allow it
1394
+ * @param {string} contactId the whatsapp user's ID
1395
+ * @returns {Promise<string>}
1396
+ */
1397
+ async getProfilePicUrl(contactId) {
1398
+ const profilePic = await this.pupPage.evaluate(async contactId => {
1399
+ try {
1400
+ const chatWid = window.Store.WidFactory.createWid(contactId);
1401
+ return window.compareWwebVersions(window.Debug.VERSION, '<', '2.3000.0')
1402
+ ? await window.Store.ProfilePic.profilePicFind(chatWid)
1403
+ : await window.Store.ProfilePic.requestProfilePicFromServer(chatWid);
1404
+ } catch (err) {
1405
+ if(err.name === 'ServerStatusCodeError') return undefined;
1406
+ throw err;
1407
+ }
1408
+ }, contactId);
1409
+
1410
+ return profilePic ? profilePic.eurl : undefined;
1411
+ }
1412
+
1413
+ /**
1414
+ * Gets the Contact's common groups with you. Returns empty array if you don't have any common group.
1415
+ * @param {string} contactId the whatsapp user's ID (_serialized format)
1416
+ * @returns {Promise<WAWebJS.ChatId[]>}
1417
+ */
1418
+ async getCommonGroups(contactId) {
1419
+ const commonGroups = await this.pupPage.evaluate(async (contactId) => {
1420
+ let contact = window.Store.Contact.get(contactId);
1421
+ if (!contact) {
1422
+ const wid = window.Store.WidFactory.createUserWid(contactId);
1423
+ const chatConstructor = window.Store.Contact.getModelsArray().find(c=>!c.isGroup).constructor;
1424
+ contact = new chatConstructor({id: wid});
1425
+ }
1426
+
1427
+ if (contact.commonGroups) {
1428
+ return contact.commonGroups.serialize();
1429
+ }
1430
+ const status = await window.Store.findCommonGroups(contact);
1431
+ if (status) {
1432
+ return contact.commonGroups.serialize();
1433
+ }
1434
+ return [];
1435
+ }, contactId);
1436
+ const chats = [];
1437
+ for (const group of commonGroups) {
1438
+ chats.push(group.id);
1439
+ }
1440
+ return chats;
1441
+ }
1442
+
1443
+ /**
1444
+ * Force reset of connection state for the client
1445
+ */
1446
+ async resetState() {
1447
+ await this.pupPage.evaluate(() => {
1448
+ window.Store.AppState.reconnect();
1449
+ });
1450
+ }
1451
+
1452
+ /**
1453
+ * Check if a given ID is registered in whatsapp
1454
+ * @param {string} id the whatsapp user's ID
1455
+ * @returns {Promise<Boolean>}
1456
+ */
1457
+ async isRegisteredUser(id) {
1458
+ return Boolean(await this.getNumberId(id));
1459
+ }
1460
+
1461
+ /**
1462
+ * Get the registered WhatsApp ID for a number.
1463
+ * Will return null if the number is not registered on WhatsApp.
1464
+ * @param {string} number Number or ID ("@c.us" will be automatically appended if not specified)
1465
+ * @returns {Promise<Object|null>}
1466
+ */
1467
+ async getNumberId(number) {
1468
+ if (!number.endsWith('@c.us')) {
1469
+ number += '@c.us';
1470
+ }
1471
+
1472
+ return await this.pupPage.evaluate(async number => {
1473
+ const wid = window.Store.WidFactory.createWid(number);
1474
+ const result = await window.Store.QueryExist(wid);
1475
+ if (!result || result.wid === undefined) return null;
1476
+ return result.wid;
1477
+ }, number);
1478
+ }
1479
+
1480
+ /**
1481
+ * Get the formatted number of a WhatsApp ID.
1482
+ * @param {string} number Number or ID
1483
+ * @returns {Promise<string>}
1484
+ */
1485
+ async getFormattedNumber(number) {
1486
+ if (!number.endsWith('@s.whatsapp.net')) number = number.replace('c.us', 's.whatsapp.net');
1487
+ if (!number.includes('@s.whatsapp.net')) number = `${number}@s.whatsapp.net`;
1488
+
1489
+ return await this.pupPage.evaluate(async numberId => {
1490
+ return window.Store.NumberInfo.formattedPhoneNumber(numberId);
1491
+ }, number);
1492
+ }
1493
+
1494
+ /**
1495
+ * Get the country code of a WhatsApp ID.
1496
+ * @param {string} number Number or ID
1497
+ * @returns {Promise<string>}
1498
+ */
1499
+ async getCountryCode(number) {
1500
+ number = number.replace(' ', '').replace('+', '').replace('@c.us', '');
1501
+
1502
+ return await this.pupPage.evaluate(async numberId => {
1503
+ return window.Store.NumberInfo.findCC(numberId);
1504
+ }, number);
1505
+ }
1506
+
1507
+ /**
1508
+ * An object that represents the result for a participant added to a group
1509
+ * @typedef {Object} ParticipantResult
1510
+ * @property {number} statusCode The status code of the result
1511
+ * @property {string} message The result message
1512
+ * @property {boolean} isGroupCreator Indicates if the participant is a group creator
1513
+ * @property {boolean} isInviteV4Sent Indicates if the inviteV4 was sent to the participant
1514
+ */
1515
+
1516
+ /**
1517
+ * An object that handles the result for {@link createGroup} method
1518
+ * @typedef {Object} CreateGroupResult
1519
+ * @property {string} title A group title
1520
+ * @property {Object} gid An object that handles the newly created group ID
1521
+ * @property {string} gid.server
1522
+ * @property {string} gid.user
1523
+ * @property {string} gid._serialized
1524
+ * @property {Object.<string, ParticipantResult>} participants An object that handles the result value for each added to the group participant
1525
+ */
1526
+
1527
+ /**
1528
+ * An object that handles options for group creation
1529
+ * @typedef {Object} CreateGroupOptions
1530
+ * @property {number} [messageTimer = 0] The number of seconds for the messages to disappear in the group (0 by default, won't take an effect if the group is been creating with myself only)
1531
+ * @property {string|undefined} parentGroupId The ID of a parent community group to link the newly created group with (won't take an effect if the group is been creating with myself only)
1532
+ * @property {boolean} [autoSendInviteV4 = true] If true, the inviteV4 will be sent to those participants who have restricted others from being automatically added to groups, otherwise the inviteV4 won't be sent (true by default)
1533
+ * @property {string} [comment = ''] The comment to be added to an inviteV4 (empty string by default)
1534
+ */
1535
+
1536
+ /**
1537
+ * Creates a new group
1538
+ * @param {string} title Group title
1539
+ * @param {string|Contact|Array<Contact|string>|undefined} participants A single Contact object or an ID as a string or an array of Contact objects or contact IDs to add to the group
1540
+ * @param {CreateGroupOptions} options An object that handles options for group creation
1541
+ * @returns {Promise<CreateGroupResult|string>} Object with resulting data or an error message as a string
1542
+ */
1543
+ async createGroup(title, participants = [], options = {}) {
1544
+ !Array.isArray(participants) && (participants = [participants]);
1545
+ participants.map(p => (p instanceof Contact) ? p.id._serialized : p);
1546
+
1547
+ return await this.pupPage.evaluate(async (title, participants, options) => {
1548
+ const { messageTimer = 0, parentGroupId, autoSendInviteV4 = true, comment = '' } = options;
1549
+ const participantData = {}, participantWids = [], failedParticipants = [];
1550
+ let createGroupResult, parentGroupWid;
1551
+
1552
+ const addParticipantResultCodes = {
1553
+ default: 'An unknown error occupied while adding a participant',
1554
+ 200: 'The participant was added successfully',
1555
+ 403: 'The participant can be added by sending private invitation only',
1556
+ 404: 'The phone number is not registered on WhatsApp'
1557
+ };
1558
+
1559
+ for (const participant of participants) {
1560
+ const pWid = window.Store.WidFactory.createWid(participant);
1561
+ if ((await window.Store.QueryExist(pWid))?.wid) participantWids.push(pWid);
1562
+ else failedParticipants.push(participant);
1563
+ }
1564
+
1565
+ parentGroupId && (parentGroupWid = window.Store.WidFactory.createWid(parentGroupId));
1566
+
1567
+ try {
1568
+ createGroupResult = await window.Store.GroupUtils.createGroup(
1569
+ {
1570
+ 'memberAddMode': options.memberAddMode === undefined ? true : options.memberAddMode,
1571
+ 'membershipApprovalMode': options.membershipApprovalMode === undefined ? false : options.membershipApprovalMode,
1572
+ 'announce': options.announce === undefined ? true : options.announce,
1573
+ 'ephemeralDuration': messageTimer,
1574
+ 'full': undefined,
1575
+ 'parentGroupId': parentGroupWid,
1576
+ 'restrict': options.restrict === undefined ? true : options.restrict,
1577
+ 'thumb': undefined,
1578
+ 'title': title,
1579
+ },
1580
+ participantWids
1581
+ );
1582
+ } catch (err) {
1583
+ return 'CreateGroupError: An unknown error occupied while creating a group';
1584
+ }
1585
+
1586
+ for (const participant of createGroupResult.participants) {
1587
+ let isInviteV4Sent = false;
1588
+ const participantId = participant.wid._serialized;
1589
+ const statusCode = participant.error || 200;
1590
+
1591
+ if (autoSendInviteV4 && statusCode === 403) {
1592
+ window.Store.Contact.gadd(participant.wid, { silent: true });
1593
+ const addParticipantResult = await window.Store.GroupInviteV4.sendGroupInviteMessage(
1594
+ await window.Store.Chat.find(participant.wid),
1595
+ createGroupResult.wid._serialized,
1596
+ createGroupResult.subject,
1597
+ participant.invite_code,
1598
+ participant.invite_code_exp,
1599
+ comment,
1600
+ await window.WWebJS.getProfilePicThumbToBase64(createGroupResult.wid)
1601
+ );
1602
+ isInviteV4Sent = window.compareWwebVersions(window.Debug.VERSION, '<', '2.2335.6')
1603
+ ? addParticipantResult === 'OK'
1604
+ : addParticipantResult.messageSendResult === 'OK';
1605
+ }
1606
+
1607
+ participantData[participantId] = {
1608
+ statusCode: statusCode,
1609
+ message: addParticipantResultCodes[statusCode] || addParticipantResultCodes.default,
1610
+ isGroupCreator: participant.type === 'superadmin',
1611
+ isInviteV4Sent: isInviteV4Sent
1612
+ };
1613
+ }
1614
+
1615
+ for (const f of failedParticipants) {
1616
+ participantData[f] = {
1617
+ statusCode: 404,
1618
+ message: addParticipantResultCodes[404],
1619
+ isGroupCreator: false,
1620
+ isInviteV4Sent: false
1621
+ };
1622
+ }
1623
+
1624
+ return { title: title, gid: createGroupResult.wid, participants: participantData };
1625
+ }, title, participants, options);
1626
+ }
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
+
1841
+ /**
1842
+ * Get all current Labels
1843
+ * @returns {Promise<Array<Label>>}
1844
+ */
1845
+ async getLabels() {
1846
+ const labels = await this.pupPage.evaluate(async () => {
1847
+ return window.WWebJS.getLabels();
1848
+ });
1849
+
1850
+ return labels.map(data => new Label(this, data));
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
+ }
1863
+
1864
+ /**
1865
+ * Get Label instance by ID
1866
+ * @param {string} labelId
1867
+ * @returns {Promise<Label>}
1868
+ */
1869
+ async getLabelById(labelId) {
1870
+ const label = await this.pupPage.evaluate(async (labelId) => {
1871
+ return window.WWebJS.getLabel(labelId);
1872
+ }, labelId);
1873
+
1874
+ return new Label(this, label);
1875
+ }
1876
+
1877
+ /**
1878
+ * Get all Labels assigned to a chat
1879
+ * @param {string} chatId
1880
+ * @returns {Promise<Array<Label>>}
1881
+ */
1882
+ async getChatLabels(chatId) {
1883
+ const labels = await this.pupPage.evaluate(async (chatId) => {
1884
+ return window.WWebJS.getChatLabels(chatId);
1885
+ }, chatId);
1886
+
1887
+ return labels.map(data => new Label(this, data));
1888
+ }
1889
+
1890
+ /**
1891
+ * Get all Chats for a specific Label
1892
+ * @param {string} labelId
1893
+ * @returns {Promise<Array<Chat>>}
1894
+ */
1895
+ async getChatsByLabelId(labelId) {
1896
+ const chatIds = await this.pupPage.evaluate(async (labelId) => {
1897
+ const label = window.Store.Label.get(labelId);
1898
+ const labelItems = label.labelItemCollection.getModelsArray();
1899
+ return labelItems.reduce((result, item) => {
1900
+ if (item.parentType === 'Chat') {
1901
+ result.push(item.parentId);
1902
+ }
1903
+ return result;
1904
+ }, []);
1905
+ }, labelId);
1906
+
1907
+ return Promise.all(chatIds.map(id => this.getChatById(id)));
1908
+ }
1909
+
1910
+ /**
1911
+ * Gets all blocked contacts by host account
1912
+ * @returns {Promise<Array<Contact>>}
1913
+ */
1914
+ async getBlockedContacts() {
1915
+ const blockedContacts = await this.pupPage.evaluate(() => {
1916
+ let chatIds = window.Store.Blocklist.getModelsArray().map(a => a.id._serialized);
1917
+ return Promise.all(chatIds.map(id => window.WWebJS.getContact(id)));
1918
+ });
1919
+
1920
+ return blockedContacts.map(contact => ContactFactory.create(this.client, contact));
1921
+ }
1922
+
1923
+ /**
1924
+ * Sets the current user's profile picture.
1925
+ * @param {MessageMedia} media
1926
+ * @returns {Promise<boolean>} Returns true if the picture was properly updated.
1927
+ */
1928
+ async setProfilePicture(media) {
1929
+ const success = await this.pupPage.evaluate((chatid, media) => {
1930
+ return window.WWebJS.setPicture(chatid, media);
1931
+ }, this.info.wid._serialized, media);
1932
+
1933
+ return success;
1934
+ }
1935
+
1936
+ /**
1937
+ * Deletes the current user's profile picture.
1938
+ * @returns {Promise<boolean>} Returns true if the picture was properly deleted.
1939
+ */
1940
+ async deleteProfilePicture() {
1941
+ const success = await this.pupPage.evaluate((chatid) => {
1942
+ return window.WWebJS.deletePicture(chatid);
1943
+ }, this.info.wid._serialized);
1944
+
1945
+ return success;
1946
+ }
1947
+
1948
+ /**
1949
+ * Change labels in chats
1950
+ * @param {Array<number|string>} labelIds
1951
+ * @param {Array<string>} chatIds
1952
+ * @returns {Promise<void>}
1953
+ */
1954
+ async addOrRemoveLabels(labelIds, chatIds) {
1955
+
1956
+ return this.pupPage.evaluate(async (labelIds, chatIds) => {
1957
+ if (['smba', 'smbi'].indexOf(window.Store.Conn.platform) === -1) {
1958
+ throw '[LT01] Only Whatsapp business';
1959
+ }
1960
+ const labels = window.WWebJS.getLabels().filter(e => labelIds.find(l => l == e.id) !== undefined);
1961
+ const chats = window.Store.Chat.filter(e => chatIds.includes(e.id._serialized));
1962
+
1963
+ let actions = labels.map(label => ({id: label.id, type: 'add'}));
1964
+
1965
+ chats.forEach(chat => {
1966
+ (chat.labels || []).forEach(n => {
1967
+ if (!actions.find(e => e.id == n)) {
1968
+ actions.push({id: n, type: 'remove'});
1969
+ }
1970
+ });
1971
+ });
1972
+
1973
+ return await window.Store.Label.addOrRemoveLabels(actions, chats);
1974
+ }, labelIds, chatIds);
1975
+ }
1976
+
1977
+ /**
1978
+ * An object that handles the information about the group membership request
1979
+ * @typedef {Object} GroupMembershipRequest
1980
+ * @property {Object} id The wid of a user who requests to enter the group
1981
+ * @property {Object} addedBy The wid of a user who created that request
1982
+ * @property {Object|null} parentGroupId The wid of a community parent group to which the current group is linked
1983
+ * @property {string} requestMethod The method used to create the request: NonAdminAdd/InviteLink/LinkedGroupJoin
1984
+ * @property {number} t The timestamp the request was created at
1985
+ */
1986
+
1987
+ /**
1988
+ * Gets an array of membership requests
1989
+ * @param {string} groupId The ID of a group to get membership requests for
1990
+ * @returns {Promise<Array<GroupMembershipRequest>>} An array of membership requests
1991
+ */
1992
+ async getGroupMembershipRequests(groupId) {
1993
+ return await this.pupPage.evaluate(async (groupId) => {
1994
+ const groupWid = window.Store.WidFactory.createWid(groupId);
1995
+ return await window.Store.MembershipRequestUtils.getMembershipApprovalRequests(groupWid);
1996
+ }, groupId);
1997
+ }
1998
+
1999
+ /**
2000
+ * An object that handles the result for membership request action
2001
+ * @typedef {Object} MembershipRequestActionResult
2002
+ * @property {string} requesterId User ID whos membership request was approved/rejected
2003
+ * @property {number|undefined} error An error code that occurred during the operation for the participant
2004
+ * @property {string} message A message with a result of membership request action
2005
+ */
2006
+
2007
+ /**
2008
+ * An object that handles options for {@link approveGroupMembershipRequests} and {@link rejectGroupMembershipRequests} methods
2009
+ * @typedef {Object} MembershipRequestActionOptions
2010
+ * @property {Array<string>|string|null} requesterIds User ID/s who requested to join the group, if no value is provided, the method will search for all membership requests for that group
2011
+ * @property {Array<number>|number|null} sleep The number of milliseconds to wait before performing an operation for the next requester. If it is an array, a random sleep time between the sleep[0] and sleep[1] values will be added (the difference must be >=100 ms, otherwise, a random sleep time between sleep[1] and sleep[1] + 100 will be added). If sleep is a number, a sleep time equal to its value will be added. By default, sleep is an array with a value of [250, 500]
2012
+ */
2013
+
2014
+ /**
2015
+ * Approves membership requests if any
2016
+ * @param {string} groupId The group ID to get the membership request for
2017
+ * @param {MembershipRequestActionOptions} options Options for performing a membership request action
2018
+ * @returns {Promise<Array<MembershipRequestActionResult>>} Returns an array of requester IDs whose membership requests were approved and an error for each requester, if any occurred during the operation. If there are no requests, an empty array will be returned
2019
+ */
2020
+ async approveGroupMembershipRequests(groupId, options = {}) {
2021
+ return await this.pupPage.evaluate(async (groupId, options) => {
2022
+ const { requesterIds = null, sleep = [250, 500] } = options;
2023
+ return await window.WWebJS.membershipRequestAction(groupId, 'Approve', requesterIds, sleep);
2024
+ }, groupId, options);
2025
+ }
2026
+
2027
+ /**
2028
+ * Rejects membership requests if any
2029
+ * @param {string} groupId The group ID to get the membership request for
2030
+ * @param {MembershipRequestActionOptions} options Options for performing a membership request action
2031
+ * @returns {Promise<Array<MembershipRequestActionResult>>} Returns an array of requester IDs whose membership requests were rejected and an error for each requester, if any occurred during the operation. If there are no requests, an empty array will be returned
2032
+ */
2033
+ async rejectGroupMembershipRequests(groupId, options = {}) {
2034
+ return await this.pupPage.evaluate(async (groupId, options) => {
2035
+ const { requesterIds = null, sleep = [250, 500] } = options;
2036
+ return await window.WWebJS.membershipRequestAction(groupId, 'Reject', requesterIds, sleep);
2037
+ }, groupId, options);
2038
+ }
2039
+
2040
+
2041
+ /**
2042
+ * Setting autoload download audio
2043
+ * @param {boolean} flag true/false
2044
+ */
2045
+ async setAutoDownloadAudio(flag) {
2046
+ await this.pupPage.evaluate(async flag => {
2047
+ const autoDownload = window.Store.Settings.getAutoDownloadAudio();
2048
+ if (autoDownload === flag) {
2049
+ return flag;
2050
+ }
2051
+ await window.Store.Settings.setAutoDownloadAudio(flag);
2052
+ return flag;
2053
+ }, flag);
2054
+ }
2055
+
2056
+ /**
2057
+ * Setting autoload download documents
2058
+ * @param {boolean} flag true/false
2059
+ */
2060
+ async setAutoDownloadDocuments(flag) {
2061
+ await this.pupPage.evaluate(async flag => {
2062
+ const autoDownload = window.Store.Settings.getAutoDownloadDocuments();
2063
+ if (autoDownload === flag) {
2064
+ return flag;
2065
+ }
2066
+ await window.Store.Settings.setAutoDownloadDocuments(flag);
2067
+ return flag;
2068
+ }, flag);
2069
+ }
2070
+
2071
+ /**
2072
+ * Setting autoload download photos
2073
+ * @param {boolean} flag true/false
2074
+ */
2075
+ async setAutoDownloadPhotos(flag) {
2076
+ await this.pupPage.evaluate(async flag => {
2077
+ const autoDownload = window.Store.Settings.getAutoDownloadPhotos();
2078
+ if (autoDownload === flag) {
2079
+ return flag;
2080
+ }
2081
+ await window.Store.Settings.setAutoDownloadPhotos(flag);
2082
+ return flag;
2083
+ }, flag);
2084
+ }
2085
+
2086
+ /**
2087
+ * Setting autoload download videos
2088
+ * @param {boolean} flag true/false
2089
+ */
2090
+ async setAutoDownloadVideos(flag) {
2091
+ await this.pupPage.evaluate(async flag => {
2092
+ const autoDownload = window.Store.Settings.getAutoDownloadVideos();
2093
+ if (autoDownload === flag) {
2094
+ return flag;
2095
+ }
2096
+ await window.Store.Settings.setAutoDownloadVideos(flag);
2097
+ return flag;
2098
+ }, flag);
2099
+ }
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
+
2118
+ /**
2119
+ * Get user device count by ID
2120
+ * Each WaWeb Connection counts as one device, and the phone (if exists) counts as one
2121
+ * So for a non-enterprise user with one WaWeb connection it should return "2"
2122
+ * @param {string} userId
2123
+ * @returns {Promise<number>}
2124
+ */
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);
2185
+ }
2186
+ }
2187
+
2188
+ module.exports = Client;