@another-trial/whatsapp-web.js 1.34.1 → 1.34.5-alpha.3

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 (56) hide show
  1. package/.env.example +2 -2
  2. package/CODE_OF_CONDUCT.md +133 -133
  3. package/LICENSE +201 -201
  4. package/README.md +155 -185
  5. package/example.js +706 -690
  6. package/index.d.ts +2259 -2202
  7. package/index.js +35 -35
  8. package/package.json +59 -59
  9. package/shell.js +36 -36
  10. package/src/Client.js +2533 -2361
  11. package/src/authStrategies/BaseAuthStrategy.js +26 -26
  12. package/src/authStrategies/LocalAuth.js +58 -58
  13. package/src/authStrategies/NoAuth.js +11 -11
  14. package/src/authStrategies/RemoteAuth.js +210 -210
  15. package/src/factories/ChatFactory.js +21 -21
  16. package/src/factories/ContactFactory.js +15 -15
  17. package/src/structures/Base.js +21 -21
  18. package/src/structures/Broadcast.js +69 -69
  19. package/src/structures/BusinessContact.js +20 -20
  20. package/src/structures/Buttons.js +81 -81
  21. package/src/structures/Call.js +75 -75
  22. package/src/structures/Channel.js +382 -382
  23. package/src/structures/Chat.js +329 -299
  24. package/src/structures/ClientInfo.js +70 -70
  25. package/src/structures/Contact.js +215 -208
  26. package/src/structures/GroupChat.js +485 -485
  27. package/src/structures/GroupNotification.js +104 -104
  28. package/src/structures/Label.js +49 -49
  29. package/src/structures/List.js +79 -79
  30. package/src/structures/Location.js +61 -61
  31. package/src/structures/Message.js +787 -747
  32. package/src/structures/MessageMedia.js +111 -111
  33. package/src/structures/Order.js +51 -51
  34. package/src/structures/Payment.js +79 -79
  35. package/src/structures/Poll.js +44 -44
  36. package/src/structures/PollVote.js +75 -75
  37. package/src/structures/PrivateChat.js +12 -12
  38. package/src/structures/PrivateContact.js +12 -12
  39. package/src/structures/Product.js +67 -67
  40. package/src/structures/ProductMetadata.js +24 -24
  41. package/src/structures/Reaction.js +68 -68
  42. package/src/structures/ScheduledEvent.js +71 -71
  43. package/src/structures/index.js +27 -27
  44. package/src/util/Constants.js +186 -183
  45. package/src/util/Injected/AuthStore/AuthStore.js +16 -16
  46. package/src/util/Injected/AuthStore/LegacyAuthStore.js +21 -21
  47. package/src/util/Injected/LegacyStore.js +145 -145
  48. package/src/util/Injected/Store.js +263 -233
  49. package/src/util/Injected/Utils.js +1221 -1169
  50. package/src/util/InterfaceController.js +126 -126
  51. package/src/util/Puppeteer.js +23 -23
  52. package/src/util/Util.js +186 -186
  53. package/src/webCache/LocalWebCache.js +40 -40
  54. package/src/webCache/RemoteWebCache.js +39 -39
  55. package/src/webCache/WebCache.js +13 -13
  56. package/src/webCache/WebCacheFactory.js +19 -19
package/src/Client.js CHANGED
@@ -1,2361 +1,2533 @@
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 { ClientInfo, Message, MessageMedia, Contact, Location, Poll, PollVote, GroupNotification, Label, Call, Buttons, List, Reaction, Broadcast, ScheduledEvent } = require('./structures');
19
- const NoAuth = require('./authStrategies/NoAuth');
20
- const {exposeFunctionIfAbsent} = require('./util/Puppeteer');
21
-
22
- /**
23
- * Starting point for interacting with the WhatsApp Web API
24
- * @extends {EventEmitter}
25
- * @param {object} options - Client options
26
- * @param {AuthStrategy} options.authStrategy - Determines how to save and restore sessions. Will use LegacySessionAuth if options.session is set. Otherwise, NoAuth will be used.
27
- * @param {string} options.webVersion - The version of WhatsApp Web to use. Use options.webVersionCache to configure how the version is retrieved.
28
- * @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.
29
- * @param {number} options.authTimeoutMs - Timeout for authentication selector in puppeteer
30
- * @param {object} options.puppeteer - Puppeteer launch options. View docs here: https://github.com/puppeteer/puppeteer/
31
- * @param {number} options.qrMaxRetries - How many times should the qrcode be refreshed before giving up
32
- * @param {string} options.restartOnAuthFail - @deprecated This option should be set directly on the LegacySessionAuth.
33
- * @param {object} options.session - @deprecated Only here for backwards-compatibility. You should move to using LocalAuth, or set the authStrategy to LegacySessionAuth explicitly.
34
- * @param {number} options.takeoverOnConflict - If another whatsapp web session is detected (another browser), take over the session in the current browser
35
- * @param {number} options.takeoverTimeoutMs - How much time to wait before taking over the session
36
- * @param {string} options.userAgent - User agent to use in puppeteer
37
- * @param {string} options.ffmpegPath - Ffmpeg path to use when formatting videos to webp while sending stickers
38
- * @param {boolean} options.bypassCSP - Sets bypassing of page's Content-Security-Policy.
39
- * @param {string} options.deviceName - Sets the device name of a current linked device., i.e.: 'TEST'.
40
- * @param {string} options.browserName - Sets the browser name of a current linked device, i.e.: 'Firefox'.
41
- * @param {object} options.proxyAuthentication - Proxy Authentication object.
42
- *
43
- * @fires Client#qr
44
- * @fires Client#authenticated
45
- * @fires Client#auth_failure
46
- * @fires Client#ready
47
- * @fires Client#message
48
- * @fires Client#message_ack
49
- * @fires Client#message_create
50
- * @fires Client#message_revoke_me
51
- * @fires Client#message_revoke_everyone
52
- * @fires Client#message_ciphertext
53
- * @fires Client#message_edit
54
- * @fires Client#media_uploaded
55
- * @fires Client#group_join
56
- * @fires Client#group_leave
57
- * @fires Client#group_update
58
- * @fires Client#disconnected
59
- * @fires Client#change_state
60
- * @fires Client#contact_changed
61
- * @fires Client#group_admin_changed
62
- * @fires Client#group_membership_request
63
- * @fires Client#vote_update
64
- */
65
- class Client extends EventEmitter {
66
- constructor(options = {}) {
67
- super();
68
-
69
- this.options = Util.mergeDefault(DefaultOptions, options);
70
-
71
- if(!this.options.authStrategy) {
72
- this.authStrategy = new NoAuth();
73
- } else {
74
- this.authStrategy = this.options.authStrategy;
75
- }
76
-
77
- this.authStrategy.setup(this);
78
-
79
- /**
80
- * @type {puppeteer.Browser}
81
- */
82
- this.pupBrowser = null;
83
- /**
84
- * @type {puppeteer.Page}
85
- */
86
- this.pupPage = null;
87
-
88
- this.currentIndexHtml = null;
89
- this.lastLoggedOut = false;
90
-
91
- Util.setFfmpegPath(this.options.ffmpegPath);
92
- }
93
- /**
94
- * Injection logic
95
- * Private function
96
- */
97
- async inject() {
98
- await this.pupPage.waitForFunction('window.Debug?.VERSION != undefined', {timeout: this.options.authTimeoutMs});
99
- await this.setDeviceName(this.options.deviceName, this.options.browserName);
100
- const pairWithPhoneNumber = this.options.pairWithPhoneNumber;
101
- const version = await this.getWWebVersion();
102
- const isCometOrAbove = parseInt(version.split('.')?.[1]) >= 3000;
103
-
104
- if (isCometOrAbove) {
105
- await this.pupPage.evaluate(ExposeAuthStore);
106
- } else {
107
- await this.pupPage.evaluate(ExposeLegacyAuthStore, moduleRaid.toString());
108
- }
109
-
110
- const needAuthentication = await this.pupPage.evaluate(async () => {
111
- let state = window.AuthStore.AppState.state;
112
-
113
- if (state === 'OPENING' || state === 'UNLAUNCHED' || state === 'PAIRING') {
114
- // wait till state changes
115
- await new Promise(r => {
116
- window.AuthStore.AppState.on('change:state', function waitTillInit(_AppState, state) {
117
- if (state !== 'OPENING' && state !== 'UNLAUNCHED' && state !== 'PAIRING') {
118
- window.AuthStore.AppState.off('change:state', waitTillInit);
119
- r();
120
- }
121
- });
122
- });
123
- }
124
- state = window.AuthStore.AppState.state;
125
- return state == 'UNPAIRED' || state == 'UNPAIRED_IDLE';
126
- });
127
-
128
- if (needAuthentication) {
129
- const { failed, failureEventPayload, restart } = await this.authStrategy.onAuthenticationNeeded();
130
-
131
- if(failed) {
132
- /**
133
- * Emitted when there has been an error while trying to restore an existing session
134
- * @event Client#auth_failure
135
- * @param {string} message
136
- */
137
- this.emit(Events.AUTHENTICATION_FAILURE, failureEventPayload);
138
- await this.destroy();
139
- if (restart) {
140
- // session restore failed so try again but without session to force new authentication
141
- return this.initialize();
142
- }
143
- return;
144
- }
145
-
146
- // Register qr/code events
147
- if (pairWithPhoneNumber.phoneNumber) {
148
- await exposeFunctionIfAbsent(this.pupPage, 'onCodeReceivedEvent', async (code) => {
149
- /**
150
- * Emitted when a pairing code is received
151
- * @event Client#code
152
- * @param {string} code Code
153
- * @returns {string} Code that was just received
154
- */
155
- this.emit(Events.CODE_RECEIVED, code);
156
- return code;
157
- });
158
- this.requestPairingCode(pairWithPhoneNumber.phoneNumber, pairWithPhoneNumber.showNotification, pairWithPhoneNumber.intervalMs);
159
- } else {
160
- let qrRetries = 0;
161
- await exposeFunctionIfAbsent(this.pupPage, 'onQRChangedEvent', async (qr) => {
162
- /**
163
- * Emitted when a QR code is received
164
- * @event Client#qr
165
- * @param {string} qr QR Code
166
- */
167
- this.emit(Events.QR_RECEIVED, qr);
168
- if (this.options.qrMaxRetries > 0) {
169
- qrRetries++;
170
- if (qrRetries > this.options.qrMaxRetries) {
171
- this.emit(Events.DISCONNECTED, 'Max qrcode retries reached');
172
- await this.destroy();
173
- }
174
- }
175
- });
176
-
177
-
178
- await this.pupPage.evaluate(async () => {
179
- const registrationInfo = await window.AuthStore.RegistrationUtils.waSignalStore.getRegistrationInfo();
180
- const noiseKeyPair = await window.AuthStore.RegistrationUtils.waNoiseInfo.get();
181
- const staticKeyB64 = window.AuthStore.Base64Tools.encodeB64(noiseKeyPair.staticKeyPair.pubKey);
182
- const identityKeyB64 = window.AuthStore.Base64Tools.encodeB64(registrationInfo.identityKeyPair.pubKey);
183
- const advSecretKey = await window.AuthStore.RegistrationUtils.getADVSecretKey();
184
- const platform = window.AuthStore.RegistrationUtils.DEVICE_PLATFORM;
185
- const getQR = (ref) => ref + ',' + staticKeyB64 + ',' + identityKeyB64 + ',' + advSecretKey + ',' + platform;
186
-
187
- window.onQRChangedEvent(getQR(window.AuthStore.Conn.ref)); // initial qr
188
- window.AuthStore.Conn.on('change:ref', (_, ref) => { window.onQRChangedEvent(getQR(ref)); }); // future QR changes
189
- });
190
- }
191
- }
192
-
193
- await exposeFunctionIfAbsent(this.pupPage, 'onAuthAppStateChangedEvent', async (state) => {
194
- if (state == 'UNPAIRED_IDLE' && !pairWithPhoneNumber.phoneNumber) {
195
- // refresh qr code
196
- window.Store.Cmd.refreshQR();
197
- }
198
- });
199
-
200
- await exposeFunctionIfAbsent(this.pupPage, 'onAppStateHasSyncedEvent', async () => {
201
- const authEventPayload = await this.authStrategy.getAuthEventPayload();
202
- /**
203
- * Emitted when authentication is successful
204
- * @event Client#authenticated
205
- */
206
- this.emit(Events.AUTHENTICATED, authEventPayload);
207
-
208
- const injected = await this.pupPage.evaluate(async () => {
209
- return typeof window.Store !== 'undefined' && typeof window.WWebJS !== 'undefined';
210
- });
211
-
212
- if (!injected) {
213
- if (this.options.webVersionCache.type === 'local' && this.currentIndexHtml) {
214
- const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache;
215
- const webCache = WebCacheFactory.createWebCache(webCacheType, webCacheOptions);
216
-
217
- await webCache.persist(this.currentIndexHtml, version);
218
- }
219
-
220
- if (isCometOrAbove) {
221
- await this.pupPage.evaluate(ExposeStore);
222
- } else {
223
- // make sure all modules are ready before injection
224
- // 2 second delay after authentication makes sense and does not need to be made dyanmic or removed
225
- await new Promise(r => setTimeout(r, 2000));
226
- await this.pupPage.evaluate(ExposeLegacyStore);
227
- }
228
-
229
- // Check window.Store Injection
230
- await this.pupPage.waitForFunction('window.Store != undefined');
231
-
232
- /**
233
- * Current connection information
234
- * @type {ClientInfo}
235
- */
236
- this.info = new ClientInfo(this, await this.pupPage.evaluate(() => {
237
- return { ...window.Store.Conn.serialize(), wid: window.Store.User.getMaybeMePnUser() || window.Store.User.getMaybeMeLidUser() };
238
- }));
239
-
240
- this.interface = new InterfaceController(this);
241
-
242
- //Load util functions (serializers, helper functions)
243
- await this.pupPage.evaluate(LoadUtils);
244
-
245
- await this.attachEventListeners();
246
- }
247
- /**
248
- * Emitted when the client has initialized and is ready to receive messages.
249
- * @event Client#ready
250
- */
251
- this.emit(Events.READY);
252
- this.authStrategy.afterAuthReady();
253
- });
254
- let lastPercent = null;
255
- await exposeFunctionIfAbsent(this.pupPage, 'onOfflineProgressUpdateEvent', async (percent) => {
256
- if (lastPercent !== percent) {
257
- lastPercent = percent;
258
- this.emit(Events.LOADING_SCREEN, percent, 'WhatsApp'); // Message is hardcoded as "WhatsApp" for now
259
- }
260
- });
261
- await exposeFunctionIfAbsent(this.pupPage, 'onLogoutEvent', async () => {
262
- this.lastLoggedOut = true;
263
- await this.pupPage.waitForNavigation({waitUntil: 'load', timeout: 5000}).catch((_) => _);
264
- });
265
- await this.pupPage.evaluate(() => {
266
- window.AuthStore.AppState.on('change:state', (_AppState, state) => { window.onAuthAppStateChangedEvent(state); });
267
- window.AuthStore.AppState.on('change:hasSynced', () => { window.onAppStateHasSyncedEvent(); });
268
- window.AuthStore.Cmd.on('offline_progress_update', () => {
269
- window.onOfflineProgressUpdateEvent(window.AuthStore.OfflineMessageHandler.getOfflineDeliveryProgress());
270
- });
271
- window.AuthStore.Cmd.on('logout', async () => {
272
- await window.onLogoutEvent();
273
- });
274
- });
275
- }
276
-
277
- /**
278
- * Sets up events and requirements, kicks off authentication request
279
- */
280
- async initialize() {
281
-
282
- let
283
- /**
284
- * @type {puppeteer.Browser}
285
- */
286
- browser,
287
- /**
288
- * @type {puppeteer.Page}
289
- */
290
- page;
291
-
292
- browser = null;
293
- page = null;
294
-
295
- await this.authStrategy.beforeBrowserInitialized();
296
-
297
- const puppeteerOpts = this.options.puppeteer;
298
- if (puppeteerOpts && (puppeteerOpts.browserWSEndpoint || puppeteerOpts.browserURL)) {
299
- browser = await puppeteer.connect(puppeteerOpts);
300
- page = await browser.newPage();
301
- } else {
302
- const browserArgs = [...(puppeteerOpts.args || [])];
303
- if(!browserArgs.find(arg => arg.includes('--user-agent'))) {
304
- browserArgs.push(`--user-agent=${this.options.userAgent}`);
305
- }
306
- // navigator.webdriver fix
307
- browserArgs.push('--disable-blink-features=AutomationControlled');
308
-
309
- browser = await puppeteer.launch({...puppeteerOpts, args: browserArgs});
310
- page = (await browser.pages())[0];
311
- }
312
-
313
- if (this.options.proxyAuthentication !== undefined) {
314
- await page.authenticate(this.options.proxyAuthentication);
315
- }
316
-
317
- await page.setUserAgent(this.options.userAgent);
318
- if (this.options.bypassCSP) await page.setBypassCSP(true);
319
-
320
- this.pupBrowser = browser;
321
- this.pupPage = page;
322
-
323
- await this.authStrategy.afterBrowserInitialized();
324
- await this.initWebVersionCache();
325
-
326
- // ocVersion (isOfficialClient patch)
327
- // remove after 2.3000.x hard release
328
- await page.evaluateOnNewDocument(() => {
329
- const originalError = Error;
330
- window.originalError = originalError;
331
- //eslint-disable-next-line no-global-assign
332
- Error = function (message) {
333
- const error = new originalError(message);
334
- const originalStack = error.stack;
335
- if (error.stack.includes('moduleRaid')) error.stack = originalStack + '\n at https://web.whatsapp.com/vendors~lazy_loaded_low_priority_components.05e98054dbd60f980427.js:2:44';
336
- return error;
337
- };
338
- });
339
-
340
- await page.goto(WhatsWebURL, {
341
- waitUntil: 'load',
342
- timeout: 0,
343
- referer: 'https://whatsapp.com/'
344
- });
345
-
346
- await this.inject();
347
-
348
- this.pupPage.on('framenavigated', async (frame) => {
349
- if(frame.url().includes('post_logout=1') || this.lastLoggedOut) {
350
- this.emit(Events.DISCONNECTED, 'LOGOUT');
351
- await this.authStrategy.logout();
352
- await this.authStrategy.beforeBrowserInitialized();
353
- await this.authStrategy.afterBrowserInitialized();
354
- this.lastLoggedOut = false;
355
- }
356
- await this.inject();
357
- });
358
- }
359
-
360
- /**
361
- * Request authentication via pairing code instead of QR code
362
- * @param {string} phoneNumber - Phone number in international, symbol-free format (e.g. 12025550108 for US, 551155501234 for Brazil)
363
- * @param {boolean} [showNotification = true] - Show notification to pair on phone number
364
- * @param {number} [intervalMs = 180000] - The interval in milliseconds on how frequent to generate pairing code (WhatsApp default to 3 minutes)
365
- * @returns {Promise<string>} - Returns a pairing code in format "ABCDEFGH"
366
- */
367
- async requestPairingCode(phoneNumber, showNotification = true, intervalMs = 180000) {
368
- return await this.pupPage.evaluate(async (phoneNumber, showNotification, intervalMs) => {
369
- const getCode = async () => {
370
- while (!window.AuthStore.PairingCodeLinkUtils) {
371
- await new Promise(resolve => setTimeout(resolve, 250));
372
- }
373
- window.AuthStore.PairingCodeLinkUtils.setPairingType('ALT_DEVICE_LINKING');
374
- await window.AuthStore.PairingCodeLinkUtils.initializeAltDeviceLinking();
375
- return window.AuthStore.PairingCodeLinkUtils.startAltLinkingFlow(phoneNumber, showNotification);
376
- };
377
- if (window.codeInterval) {
378
- clearInterval(window.codeInterval); // remove existing interval
379
- }
380
- window.codeInterval = setInterval(async () => {
381
- if (window.AuthStore.AppState.state != 'UNPAIRED' && window.AuthStore.AppState.state != 'UNPAIRED_IDLE') {
382
- clearInterval(window.codeInterval);
383
- return;
384
- }
385
- window.onCodeReceivedEvent(await getCode());
386
- }, intervalMs);
387
- return window.onCodeReceivedEvent(await getCode());
388
- }, phoneNumber, showNotification, intervalMs);
389
- }
390
-
391
- /**
392
- * Attach event listeners to WA Web
393
- * Private function
394
- * @property {boolean} reinject is this a reinject?
395
- */
396
- async attachEventListeners() {
397
- await exposeFunctionIfAbsent(this.pupPage, 'onAddMessageEvent', msg => {
398
- if (msg.type === 'gp2') {
399
- const notification = new GroupNotification(this, msg);
400
- if (['add', 'invite', 'linked_group_join'].includes(msg.subtype)) {
401
- /**
402
- * Emitted when a user joins the chat via invite link or is added by an admin.
403
- * @event Client#group_join
404
- * @param {GroupNotification} notification GroupNotification with more information about the action
405
- */
406
- this.emit(Events.GROUP_JOIN, notification);
407
- } else if (msg.subtype === 'remove' || msg.subtype === 'leave') {
408
- /**
409
- * Emitted when a user leaves the chat or is removed by an admin.
410
- * @event Client#group_leave
411
- * @param {GroupNotification} notification GroupNotification with more information about the action
412
- */
413
- this.emit(Events.GROUP_LEAVE, notification);
414
- } else if (msg.subtype === 'promote' || msg.subtype === 'demote') {
415
- /**
416
- * Emitted when a current user is promoted to an admin or demoted to a regular user.
417
- * @event Client#group_admin_changed
418
- * @param {GroupNotification} notification GroupNotification with more information about the action
419
- */
420
- this.emit(Events.GROUP_ADMIN_CHANGED, notification);
421
- } else if (msg.subtype === 'membership_approval_request') {
422
- /**
423
- * Emitted when some user requested to join the group
424
- * that has the membership approval mode turned on
425
- * @event Client#group_membership_request
426
- * @param {GroupNotification} notification GroupNotification with more information about the action
427
- * @param {string} notification.chatId The group ID the request was made for
428
- * @param {string} notification.author The user ID that made a request
429
- * @param {number} notification.timestamp The timestamp the request was made at
430
- */
431
- this.emit(Events.GROUP_MEMBERSHIP_REQUEST, notification);
432
- } else {
433
- /**
434
- * Emitted when group settings are updated, such as subject, description or picture.
435
- * @event Client#group_update
436
- * @param {GroupNotification} notification GroupNotification with more information about the action
437
- */
438
- this.emit(Events.GROUP_UPDATE, notification);
439
- }
440
- return;
441
- }
442
-
443
- const message = new Message(this, msg);
444
-
445
- /**
446
- * Emitted when a new message is created, which may include the current user's own messages.
447
- * @event Client#message_create
448
- * @param {Message} message The message that was created
449
- */
450
- this.emit(Events.MESSAGE_CREATE, message);
451
-
452
- if (msg.id.fromMe) return;
453
-
454
- /**
455
- * Emitted when a new message is received.
456
- * @event Client#message
457
- * @param {Message} message The message that was received
458
- */
459
- this.emit(Events.MESSAGE_RECEIVED, message);
460
- });
461
-
462
- let last_message;
463
-
464
- await exposeFunctionIfAbsent(this.pupPage, 'onChangeMessageTypeEvent', (msg) => {
465
-
466
- if (msg.type === 'revoked') {
467
- const message = new Message(this, msg);
468
- let revoked_msg;
469
- if (last_message && msg.id.id === last_message.id.id) {
470
- revoked_msg = new Message(this, last_message);
471
-
472
- if (message.protocolMessageKey)
473
- revoked_msg.id = { ...message.protocolMessageKey };
474
- }
475
-
476
- /**
477
- * Emitted when a message is deleted for everyone in the chat.
478
- * @event Client#message_revoke_everyone
479
- * @param {Message} message The message that was revoked, in its current state. It will not contain the original message's data.
480
- * @param {?Message} revoked_msg The message that was revoked, before it was revoked. It will contain the message's original data.
481
- * Note that due to the way this data is captured, it may be possible that this param will be undefined.
482
- */
483
- this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg);
484
- }
485
-
486
- });
487
-
488
- await exposeFunctionIfAbsent(this.pupPage, 'onChangeMessageEvent', (msg) => {
489
-
490
- if (msg.type !== 'revoked') {
491
- last_message = msg;
492
- }
493
-
494
- /**
495
- * The event notification that is received when one of
496
- * the group participants changes their phone number.
497
- */
498
- const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify';
499
-
500
- /**
501
- * The event notification that is received when one of
502
- * the contacts changes their phone number.
503
- */
504
- const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number';
505
-
506
- if (isParticipant || isContact) {
507
- /** @type {GroupNotification} object does not provide enough information about this event, so a @type {Message} object is used. */
508
- const message = new Message(this, msg);
509
-
510
- const newId = isParticipant ? msg.recipients[0] : msg.to;
511
- const oldId = isParticipant ? msg.author : msg.templateParams.find(id => id !== newId);
512
-
513
- /**
514
- * Emitted when a contact or a group participant changes their phone number.
515
- * @event Client#contact_changed
516
- * @param {Message} message Message with more information about the event.
517
- * @param {String} oldId The user's id (an old one) who changed their phone number
518
- * and who triggered the notification.
519
- * @param {String} newId The user's new id after the change.
520
- * @param {Boolean} isContact Indicates if a contact or a group participant changed their phone number.
521
- */
522
- this.emit(Events.CONTACT_CHANGED, message, oldId, newId, isContact);
523
- }
524
- });
525
-
526
- await exposeFunctionIfAbsent(this.pupPage, 'onRemoveMessageEvent', (msg) => {
527
-
528
- if (!msg.isNewMsg) return;
529
-
530
- const message = new Message(this, msg);
531
-
532
- /**
533
- * Emitted when a message is deleted by the current user.
534
- * @event Client#message_revoke_me
535
- * @param {Message} message The message that was revoked
536
- */
537
- this.emit(Events.MESSAGE_REVOKED_ME, message);
538
-
539
- });
540
-
541
- await exposeFunctionIfAbsent(this.pupPage, 'onMessageAckEvent', (msg, ack) => {
542
-
543
- const message = new Message(this, msg);
544
-
545
- /**
546
- * Emitted when an ack event occurrs on message type.
547
- * @event Client#message_ack
548
- * @param {Message} message The message that was affected
549
- * @param {MessageAck} ack The new ACK value
550
- */
551
- this.emit(Events.MESSAGE_ACK, message, ack);
552
-
553
- });
554
-
555
- await exposeFunctionIfAbsent(this.pupPage, 'onChatUnreadCountEvent', async (data) =>{
556
- const chat = await this.getChatById(data.id);
557
-
558
- /**
559
- * Emitted when the chat unread count changes
560
- */
561
- this.emit(Events.UNREAD_COUNT, chat);
562
- });
563
-
564
- await exposeFunctionIfAbsent(this.pupPage, 'onMessageMediaUploadedEvent', (msg) => {
565
-
566
- const message = new Message(this, msg);
567
-
568
- /**
569
- * Emitted when media has been uploaded for a message sent by the client.
570
- * @event Client#media_uploaded
571
- * @param {Message} message The message with media that was uploaded
572
- */
573
- this.emit(Events.MEDIA_UPLOADED, message);
574
- });
575
-
576
- await exposeFunctionIfAbsent(this.pupPage, 'onAppStateChangedEvent', async (state) => {
577
- /**
578
- * Emitted when the connection state changes
579
- * @event Client#change_state
580
- * @param {WAState} state the new connection state
581
- */
582
- this.emit(Events.STATE_CHANGED, state);
583
-
584
- const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT];
585
-
586
- if (this.options.takeoverOnConflict) {
587
- ACCEPTED_STATES.push(WAState.CONFLICT);
588
-
589
- if (state === WAState.CONFLICT) {
590
- setTimeout(() => {
591
- this.pupPage.evaluate(() => window.Store.AppState.takeover());
592
- }, this.options.takeoverTimeoutMs);
593
- }
594
- }
595
-
596
- if (!ACCEPTED_STATES.includes(state)) {
597
- /**
598
- * Emitted when the client has been disconnected
599
- * @event Client#disconnected
600
- * @param {WAState|"LOGOUT"} reason reason that caused the disconnect
601
- */
602
- await this.authStrategy.disconnect();
603
- this.emit(Events.DISCONNECTED, state);
604
- this.destroy();
605
- }
606
- });
607
-
608
- await exposeFunctionIfAbsent(this.pupPage, 'onBatteryStateChangedEvent', (state) => {
609
- const { battery, plugged } = state;
610
-
611
- if (battery === undefined) return;
612
-
613
- /**
614
- * Emitted when the battery percentage for the attached device changes. Will not be sent if using multi-device.
615
- * @event Client#change_battery
616
- * @param {object} batteryInfo
617
- * @param {number} batteryInfo.battery - The current battery percentage
618
- * @param {boolean} batteryInfo.plugged - Indicates if the phone is plugged in (true) or not (false)
619
- * @deprecated
620
- */
621
- this.emit(Events.BATTERY_CHANGED, { battery, plugged });
622
- });
623
-
624
- await exposeFunctionIfAbsent(this.pupPage, 'onIncomingCall', (call) => {
625
- /**
626
- * Emitted when a call is received
627
- * @event Client#incoming_call
628
- * @param {object} call
629
- * @param {number} call.id - Call id
630
- * @param {string} call.peerJid - Who called
631
- * @param {boolean} call.isVideo - if is video
632
- * @param {boolean} call.isGroup - if is group
633
- * @param {boolean} call.canHandleLocally - if we can handle in waweb
634
- * @param {boolean} call.outgoing - if is outgoing
635
- * @param {boolean} call.webClientShouldHandle - If Waweb should handle
636
- * @param {object} call.participants - Participants
637
- */
638
- const cll = new Call(this, call);
639
- this.emit(Events.INCOMING_CALL, cll);
640
- });
641
-
642
- await exposeFunctionIfAbsent(this.pupPage, 'onReaction', (reactions) => {
643
- for (const reaction of reactions) {
644
- /**
645
- * Emitted when a reaction is sent, received, updated or removed
646
- * @event Client#message_reaction
647
- * @param {object} reaction
648
- * @param {object} reaction.id - Reaction id
649
- * @param {number} reaction.orphan - Orphan
650
- * @param {?string} reaction.orphanReason - Orphan reason
651
- * @param {number} reaction.timestamp - Timestamp
652
- * @param {string} reaction.reaction - Reaction
653
- * @param {boolean} reaction.read - Read
654
- * @param {object} reaction.msgId - Parent message id
655
- * @param {string} reaction.senderId - Sender id
656
- * @param {?number} reaction.ack - Ack
657
- */
658
-
659
- this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction));
660
- }
661
- });
662
-
663
- await exposeFunctionIfAbsent(this.pupPage, 'onRemoveChatEvent', async (chat) => {
664
- const _chat = await this.getChatById(chat.id);
665
-
666
- /**
667
- * Emitted when a chat is removed
668
- * @event Client#chat_removed
669
- * @param {Chat} chat
670
- */
671
- this.emit(Events.CHAT_REMOVED, _chat);
672
- });
673
-
674
- await exposeFunctionIfAbsent(this.pupPage, 'onArchiveChatEvent', async (chat, currState, prevState) => {
675
- const _chat = await this.getChatById(chat.id);
676
-
677
- /**
678
- * Emitted when a chat is archived/unarchived
679
- * @event Client#chat_archived
680
- * @param {Chat} chat
681
- * @param {boolean} currState
682
- * @param {boolean} prevState
683
- */
684
- this.emit(Events.CHAT_ARCHIVED, _chat, currState, prevState);
685
- });
686
-
687
- await exposeFunctionIfAbsent(this.pupPage, 'onEditMessageEvent', (msg, newBody, prevBody) => {
688
-
689
- if(msg.type === 'revoked'){
690
- return;
691
- }
692
- /**
693
- * Emitted when messages are edited
694
- * @event Client#message_edit
695
- * @param {Message} message
696
- * @param {string} newBody
697
- * @param {string} prevBody
698
- */
699
- this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody);
700
- });
701
-
702
- await exposeFunctionIfAbsent(this.pupPage, 'onAddMessageCiphertextEvent', msg => {
703
-
704
- /**
705
- * Emitted when messages are edited
706
- * @event Client#message_ciphertext
707
- * @param {Message} message
708
- */
709
- this.emit(Events.MESSAGE_CIPHERTEXT, new Message(this, msg));
710
- });
711
-
712
- await exposeFunctionIfAbsent(this.pupPage, 'onPollVoteEvent', (votes) => {
713
- for (const vote of votes) {
714
- /**
715
- * Emitted when some poll option is selected or deselected,
716
- * shows a user's current selected option(s) on the poll
717
- * @event Client#vote_update
718
- */
719
- this.emit(Events.VOTE_UPDATE, new PollVote(this, vote));
720
- }
721
- });
722
-
723
- await this.pupPage.evaluate(() => {
724
- window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(window.WWebJS.getMessageModel(msg)); });
725
- window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(window.WWebJS.getMessageModel(msg)); });
726
- window.Store.Msg.on('change:ack', (msg, ack) => { window.onMessageAckEvent(window.WWebJS.getMessageModel(msg), ack); });
727
- window.Store.Msg.on('change:isUnsentMedia', (msg, unsent) => { if (msg.id.fromMe && !unsent) window.onMessageMediaUploadedEvent(window.WWebJS.getMessageModel(msg)); });
728
- window.Store.Msg.on('remove', (msg) => { if (msg.isNewMsg) window.onRemoveMessageEvent(window.WWebJS.getMessageModel(msg)); });
729
- window.Store.Msg.on('change:body change:caption', (msg, newBody, prevBody) => { window.onEditMessageEvent(window.WWebJS.getMessageModel(msg), newBody, prevBody); });
730
- window.Store.AppState.on('change:state', (_AppState, state) => { window.onAppStateChangedEvent(state); });
731
- window.Store.Conn.on('change:battery', (state) => { window.onBatteryStateChangedEvent(state); });
732
- window.Store.Call.on('add', (call) => { window.onIncomingCall(call); });
733
- window.Store.Chat.on('remove', async (chat) => { window.onRemoveChatEvent(await window.WWebJS.getChatModel(chat)); });
734
- window.Store.Chat.on('change:archive', async (chat, currState, prevState) => { window.onArchiveChatEvent(await window.WWebJS.getChatModel(chat), currState, prevState); });
735
- window.Store.Msg.on('add', (msg) => {
736
- if (msg.isNewMsg) {
737
- if(msg.type === 'ciphertext') {
738
- // defer message event until ciphertext is resolved (type changed)
739
- msg.once('change:type', (_msg) => window.onAddMessageEvent(window.WWebJS.getMessageModel(_msg)));
740
- window.onAddMessageCiphertextEvent(window.WWebJS.getMessageModel(msg));
741
- } else {
742
- window.onAddMessageEvent(window.WWebJS.getMessageModel(msg));
743
- }
744
- }
745
- });
746
- window.Store.Chat.on('change:unreadCount', (chat) => {window.onChatUnreadCountEvent(chat);});
747
-
748
- if (window.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.1014111620')) {
749
- const module = window.Store.AddonReactionTable;
750
- const ogMethod = module.bulkUpsert;
751
- module.bulkUpsert = ((...args) => {
752
- window.onReaction(args[0].map(reaction => {
753
- const msgKey = reaction.id;
754
- const parentMsgKey = reaction.reactionParentKey;
755
- const timestamp = reaction.reactionTimestamp / 1000;
756
- const sender = reaction.author ?? reaction.from;
757
- const senderUserJid = sender._serialized;
758
-
759
- return {...reaction, msgKey, parentMsgKey, senderUserJid, timestamp };
760
- }));
761
-
762
- return ogMethod(...args);
763
- }).bind(module);
764
-
765
- const pollVoteModule = window.Store.AddonPollVoteTable;
766
- const ogPollVoteMethod = pollVoteModule.bulkUpsert;
767
-
768
- pollVoteModule.bulkUpsert = (async (...args) => {
769
- const votes = await Promise.all(args[0].map(async vote => {
770
- const msgKey = vote.id;
771
- const parentMsgKey = vote.pollUpdateParentKey;
772
- const timestamp = vote.t / 1000;
773
- const sender = vote.author ?? vote.from;
774
- const senderUserJid = sender._serialized;
775
-
776
- let parentMessage = window.Store.Msg.get(parentMsgKey._serialized);
777
- if (!parentMessage) {
778
- const fetched = await window.Store.Msg.getMessagesById([parentMsgKey._serialized]);
779
- parentMessage = fetched?.messages?.[0] || null;
780
- }
781
-
782
- return {
783
- ...vote,
784
- msgKey,
785
- sender,
786
- parentMsgKey,
787
- senderUserJid,
788
- timestamp,
789
- parentMessage
790
- };
791
- }));
792
-
793
- window.onPollVoteEvent(votes);
794
-
795
- return ogPollVoteMethod.apply(pollVoteModule, args);
796
- }).bind(pollVoteModule);
797
- } else {
798
- const module = window.Store.createOrUpdateReactionsModule;
799
- const ogMethod = module.createOrUpdateReactions;
800
- module.createOrUpdateReactions = ((...args) => {
801
- window.onReaction(args[0].map(reaction => {
802
- const msgKey = window.Store.MsgKey.fromString(reaction.msgKey);
803
- const parentMsgKey = window.Store.MsgKey.fromString(reaction.parentMsgKey);
804
- const timestamp = reaction.timestamp / 1000;
805
-
806
- return {...reaction, msgKey, parentMsgKey, timestamp };
807
- }));
808
-
809
- return ogMethod(...args);
810
- }).bind(module);
811
- }
812
- });
813
- }
814
-
815
- async initWebVersionCache() {
816
- const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache;
817
- const webCache = WebCacheFactory.createWebCache(webCacheType, webCacheOptions);
818
-
819
- const requestedVersion = this.options.webVersion;
820
- const versionContent = await webCache.resolve(requestedVersion);
821
-
822
- if(versionContent) {
823
- await this.pupPage.setRequestInterception(true);
824
- this.pupPage.on('request', async (req) => {
825
- if(req.url() === WhatsWebURL) {
826
- req.respond({
827
- status: 200,
828
- contentType: 'text/html',
829
- body: versionContent
830
- });
831
- } else {
832
- req.continue();
833
- }
834
- });
835
- } else {
836
- this.pupPage.on('response', async (res) => {
837
- if(res.ok() && res.url() === WhatsWebURL) {
838
- const indexHtml = await res.text();
839
- this.currentIndexHtml = indexHtml;
840
- }
841
- });
842
- }
843
- }
844
-
845
- /**
846
- * Closes the client
847
- */
848
- async destroy() {
849
- await this.pupBrowser.close();
850
- await this.authStrategy.destroy();
851
- }
852
-
853
- /**
854
- * Logs out the client, closing the current session
855
- */
856
- async logout() {
857
- await this.pupPage.evaluate(() => {
858
- if (window.Store && window.Store.AppState && typeof window.Store.AppState.logout === 'function') {
859
- return window.Store.AppState.logout();
860
- }
861
- });
862
- await this.pupBrowser.close();
863
-
864
- let maxDelay = 0;
865
- while (this.pupBrowser.isConnected() && (maxDelay < 10)) { // waits a maximum of 1 second before calling the AuthStrategy
866
- await new Promise(resolve => setTimeout(resolve, 100));
867
- maxDelay++;
868
- }
869
-
870
- await this.authStrategy.logout();
871
- }
872
-
873
- /**
874
- * Returns the version of WhatsApp Web currently being run
875
- * @returns {Promise<string>}
876
- */
877
- async getWWebVersion() {
878
- return await this.pupPage.evaluate(() => {
879
- return window.Debug.VERSION;
880
- });
881
- }
882
-
883
- async setDeviceName(deviceName, browserName) {
884
- (deviceName || browserName) && await this.pupPage.evaluate((deviceName, browserName) => {
885
- const func = window.require('WAWebMiscBrowserUtils').info;
886
- window.require('WAWebMiscBrowserUtils').info = () => {
887
- return {
888
- ...func(),
889
- ...(deviceName ? { os: deviceName } : {}),
890
- ...(browserName ? { name: browserName } : {})
891
- };
892
- };
893
- }, deviceName, browserName);
894
- }
895
-
896
- /**
897
- * Mark as seen for the Chat
898
- * @param {string} chatId
899
- * @returns {Promise<boolean>} result
900
- *
901
- */
902
- async sendSeen(chatId) {
903
- return await this.pupPage.evaluate(async (chatId) => {
904
- return window.WWebJS.sendSeen(chatId);
905
- }, chatId);
906
- }
907
-
908
- /**
909
- * An object representing mentions of groups
910
- * @typedef {Object} GroupMention
911
- * @property {string} subject - The name of a group to mention (can be custom)
912
- * @property {string} id - The group ID, e.g.: 'XXXXXXXXXX@g.us'
913
- */
914
-
915
- /**
916
- * Message options.
917
- * @typedef {Object} MessageSendOptions
918
- * @property {boolean} [linkPreview=true] - Show links preview. Has no effect on multi-device accounts.
919
- * @property {boolean} [sendAudioAsVoice=false] - Send audio as voice message with a generated waveform
920
- * @property {boolean} [sendVideoAsGif=false] - Send video as gif
921
- * @property {boolean} [sendMediaAsSticker=false] - Send media as a sticker
922
- * @property {boolean} [sendMediaAsDocument=false] - Send media as a document
923
- * @property {boolean} [sendMediaAsHd=false] - Send image as quality HD
924
- * @property {boolean} [isViewOnce=false] - Send photo/video as a view once message
925
- * @property {boolean} [parseVCards=true] - Automatically parse vCards and send them as contacts
926
- * @property {string} [caption] - Image or video caption
927
- * @property {string} [quotedMessageId] - Id of the message that is being quoted (or replied to)
928
- * @property {GroupMention[]} [groupMentions] - An array of object that handle group mentions
929
- * @property {string[]} [mentions] - User IDs to mention in the message
930
- * @property {boolean} [sendSeen=true] - Mark the conversation as seen after sending the message
931
- * @property {string} [invokedBotWid=undefined] - Bot Wid when doing a bot mention like @Meta AI
932
- * @property {string} [stickerAuthor=undefined] - Sets the author of the sticker, (if sendMediaAsSticker is true).
933
- * @property {string} [stickerName=undefined] - Sets the name of the sticker, (if sendMediaAsSticker is true).
934
- * @property {string[]} [stickerCategories=undefined] - Sets the categories of the sticker, (if sendMediaAsSticker is true). Provide emoji char array, can be null.
935
- * @property {boolean} [ignoreQuoteErrors = true] - Should the bot send a quoted message without the quoted message if it fails to get the quote?
936
- * @property {boolean} [waitUntilMsgSent = false] - Should the bot wait for the message send result?
937
- * @property {MessageMedia} [media] - Media to be sent
938
- * @property {any} [extra] - Extra options
939
- */
940
-
941
- /**
942
- * Send a message to a specific chatId
943
- * @param {string} chatId
944
- * @param {string|MessageMedia|Location|Poll|Contact|Array<Contact>|Buttons|List} content
945
- * @param {MessageSendOptions} [options] - Options used when sending the message
946
- *
947
- * @returns {Promise<Message>} Message that was just sent
948
- */
949
- async sendMessage(chatId, content, options = {}) {
950
- const isChannel = /@\w*newsletter\b/.test(chatId);
951
-
952
- if (isChannel && [
953
- options.sendMediaAsDocument, options.quotedMessageId,
954
- options.parseVCards, options.isViewOnce,
955
- content instanceof Location, content instanceof Contact,
956
- content instanceof Buttons, content instanceof List,
957
- Array.isArray(content) && content.length > 0 && content[0] instanceof Contact
958
- ].includes(true)) {
959
- 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.');
960
- return null;
961
- }
962
-
963
- if (options.mentions) {
964
- !Array.isArray(options.mentions) && (options.mentions = [options.mentions]);
965
- if (options.mentions.some((possiblyContact) => possiblyContact instanceof Contact)) {
966
- console.warn('Mentions with an array of Contact are now deprecated. See more at https://github.com/pedroslopez/whatsapp-web.js/pull/2166.');
967
- options.mentions = options.mentions.map((a) => a.id._serialized);
968
- }
969
- }
970
-
971
- options.groupMentions && !Array.isArray(options.groupMentions) && (options.groupMentions = [options.groupMentions]);
972
-
973
- let internalOptions = {
974
- linkPreview: options.linkPreview === false ? undefined : true,
975
- sendAudioAsVoice: options.sendAudioAsVoice,
976
- sendVideoAsGif: options.sendVideoAsGif,
977
- sendMediaAsSticker: options.sendMediaAsSticker,
978
- sendMediaAsDocument: options.sendMediaAsDocument,
979
- sendMediaAsHd: options.sendMediaAsHd,
980
- caption: options.caption,
981
- quotedMessageId: options.quotedMessageId,
982
- parseVCards: options.parseVCards !== false,
983
- mentionedJidList: options.mentions || [],
984
- groupMentions: options.groupMentions,
985
- invokedBotWid: options.invokedBotWid,
986
- ignoreQuoteErrors: options.ignoreQuoteErrors !== false,
987
- waitUntilMsgSent: options.waitUntilMsgSent || false,
988
- extraOptions: options.extra
989
- };
990
-
991
- const sendSeen = options.sendSeen !== false;
992
-
993
- if (content instanceof MessageMedia) {
994
- internalOptions.media = content;
995
- internalOptions.isViewOnce = options.isViewOnce,
996
- content = '';
997
- } else if (options.media instanceof MessageMedia) {
998
- internalOptions.media = options.media;
999
- internalOptions.caption = content;
1000
- internalOptions.isViewOnce = options.isViewOnce,
1001
- content = '';
1002
- } else if (content instanceof Location) {
1003
- internalOptions.location = content;
1004
- content = '';
1005
- } else if (content instanceof Poll) {
1006
- internalOptions.poll = content;
1007
- content = '';
1008
- } else if (content instanceof ScheduledEvent) {
1009
- internalOptions.event = content;
1010
- content = '';
1011
- } else if (content instanceof Contact) {
1012
- internalOptions.contactCard = content.id._serialized;
1013
- content = '';
1014
- } else if (Array.isArray(content) && content.length > 0 && content[0] instanceof Contact) {
1015
- internalOptions.contactCardList = content.map(contact => contact.id._serialized);
1016
- content = '';
1017
- } else if (content instanceof Buttons) {
1018
- console.warn('Buttons are now deprecated. See more at https://www.youtube.com/watch?v=hv1R1rLeVVE.');
1019
- if (content.type !== 'chat') { internalOptions.attachment = content.body; }
1020
- internalOptions.buttons = content;
1021
- content = '';
1022
- } else if (content instanceof List) {
1023
- console.warn('Lists are now deprecated. See more at https://www.youtube.com/watch?v=hv1R1rLeVVE.');
1024
- internalOptions.list = content;
1025
- content = '';
1026
- }
1027
-
1028
- if (internalOptions.sendMediaAsSticker && internalOptions.media) {
1029
- internalOptions.media = await Util.formatToWebpSticker(
1030
- internalOptions.media, {
1031
- name: options.stickerName,
1032
- author: options.stickerAuthor,
1033
- categories: options.stickerCategories
1034
- }, this.pupPage
1035
- );
1036
- }
1037
-
1038
- const sentMsg = await this.pupPage.evaluate(async (chatId, content, options, sendSeen) => {
1039
- const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1040
-
1041
- if (!chat) return null;
1042
-
1043
- if (sendSeen) {
1044
- await window.WWebJS.sendSeen(chatId);
1045
- }
1046
-
1047
- const msg = await window.WWebJS.sendMessage(chat, content, options);
1048
- return msg
1049
- ? window.WWebJS.getMessageModel(msg)
1050
- : undefined;
1051
- }, chatId, content, internalOptions, sendSeen);
1052
-
1053
- return sentMsg
1054
- ? new Message(this, sentMsg)
1055
- : undefined;
1056
- }
1057
-
1058
- /**
1059
- * @typedef {Object} SendChannelAdminInviteOptions
1060
- * @property {?string} comment The comment to be added to an invitation
1061
- */
1062
-
1063
- /**
1064
- * Sends a channel admin invitation to a user, allowing them to become an admin of the channel
1065
- * @param {string} chatId The ID of a user to send the channel admin invitation to
1066
- * @param {string} channelId The ID of a channel for which the invitation is being sent
1067
- * @param {SendChannelAdminInviteOptions} options
1068
- * @returns {Promise<boolean>} Returns true if an invitation was sent successfully, false otherwise
1069
- */
1070
- async sendChannelAdminInvite(chatId, channelId, options = {}) {
1071
- const response = await this.pupPage.evaluate(async (chatId, channelId, options) => {
1072
- const channelWid = window.Store.WidFactory.createWid(channelId);
1073
- const chatWid = window.Store.WidFactory.createWid(chatId);
1074
- const chat = window.Store.Chat.get(chatWid) || (await window.Store.Chat.find(chatWid));
1075
-
1076
- if (!chatWid.isUser()) {
1077
- return false;
1078
- }
1079
-
1080
- return await window.Store.SendChannelMessage.sendNewsletterAdminInviteMessage(
1081
- chat,
1082
- {
1083
- newsletterWid: channelWid,
1084
- invitee: chatWid,
1085
- inviteMessage: options.comment,
1086
- base64Thumb: await window.WWebJS.getProfilePicThumbToBase64(channelWid)
1087
- }
1088
- );
1089
- }, chatId, channelId, options);
1090
-
1091
- return response.messageSendResult === 'OK';
1092
- }
1093
-
1094
- /**
1095
- * Searches for messages
1096
- * @param {string} query
1097
- * @param {Object} [options]
1098
- * @param {number} [options.page]
1099
- * @param {number} [options.limit]
1100
- * @param {string} [options.chatId]
1101
- * @returns {Promise<Message[]>}
1102
- */
1103
- async searchMessages(query, options = {}) {
1104
- const messages = await this.pupPage.evaluate(async (query, page, count, remote) => {
1105
- const { messages } = await window.Store.Msg.search(query, page, count, remote);
1106
- return messages.map(msg => window.WWebJS.getMessageModel(msg));
1107
- }, query, options.page, options.limit, options.chatId);
1108
-
1109
- return messages.map(msg => new Message(this, msg));
1110
- }
1111
-
1112
- /**
1113
- * Get all current chat instances
1114
- * @returns {Promise<Array<Chat>>}
1115
- */
1116
- async getChats() {
1117
- const chats = await this.pupPage.evaluate(async () => {
1118
- return await window.WWebJS.getChats();
1119
- });
1120
-
1121
- return chats.map(chat => ChatFactory.create(this, chat));
1122
- }
1123
-
1124
- /**
1125
- * Gets all cached {@link Channel} instance
1126
- * @returns {Promise<Array<Channel>>}
1127
- */
1128
- async getChannels() {
1129
- const channels = await this.pupPage.evaluate(async () => {
1130
- return await window.WWebJS.getChannels();
1131
- });
1132
-
1133
- return channels.map((channel) => ChatFactory.create(this, channel));
1134
- }
1135
-
1136
- /**
1137
- * Gets chat or channel instance by ID
1138
- * @param {string} chatId
1139
- * @returns {Promise<Chat|Channel>}
1140
- */
1141
- async getChatById(chatId) {
1142
- const chat = await this.pupPage.evaluate(async chatId => {
1143
- return await window.WWebJS.getChat(chatId);
1144
- }, chatId);
1145
- return chat
1146
- ? ChatFactory.create(this, chat)
1147
- : undefined;
1148
- }
1149
-
1150
- /**
1151
- * Gets a {@link Channel} instance by invite code
1152
- * @param {string} inviteCode The code that comes after the 'https://whatsapp.com/channel/'
1153
- * @returns {Promise<Channel>}
1154
- */
1155
- async getChannelByInviteCode(inviteCode) {
1156
- const channel = await this.pupPage.evaluate(async (inviteCode) => {
1157
- let channelMetadata;
1158
- try {
1159
- channelMetadata = await window.WWebJS.getChannelMetadata(inviteCode);
1160
- } catch (err) {
1161
- if (err.name === 'ServerStatusCodeError') return null;
1162
- throw err;
1163
- }
1164
- return await window.WWebJS.getChat(channelMetadata.id);
1165
- }, inviteCode);
1166
-
1167
- return channel
1168
- ? ChatFactory.create(this, channel)
1169
- : undefined;
1170
- }
1171
-
1172
- /**
1173
- * Get all current contact instances
1174
- * @returns {Promise<Array<Contact>>}
1175
- */
1176
- async getContacts() {
1177
- let contacts = await this.pupPage.evaluate(() => {
1178
- return window.WWebJS.getContacts();
1179
- });
1180
-
1181
- return contacts.map(contact => ContactFactory.create(this, contact));
1182
- }
1183
-
1184
- /**
1185
- * Get contact instance by ID
1186
- * @param {string} contactId
1187
- * @returns {Promise<Contact>}
1188
- */
1189
- async getContactById(contactId) {
1190
- let contact = await this.pupPage.evaluate(contactId => {
1191
- return window.WWebJS.getContact(contactId);
1192
- }, contactId);
1193
-
1194
- return ContactFactory.create(this, contact);
1195
- }
1196
-
1197
- async getMessageById(messageId) {
1198
- const msg = await this.pupPage.evaluate(async messageId => {
1199
- let msg = window.Store.Msg.get(messageId);
1200
- if(msg) return window.WWebJS.getMessageModel(msg);
1201
-
1202
- const params = messageId.split('_');
1203
- if (params.length !== 3 && params.length !== 4) throw new Error('Invalid serialized message id specified');
1204
-
1205
- let messagesObject = await window.Store.Msg.getMessagesById([messageId]);
1206
- if (messagesObject && messagesObject.messages.length) msg = messagesObject.messages[0];
1207
-
1208
- if(msg) return window.WWebJS.getMessageModel(msg);
1209
- }, messageId);
1210
-
1211
- if(msg) return new Message(this, msg);
1212
- return null;
1213
- }
1214
-
1215
- /**
1216
- * Gets instances of all pinned messages in a chat
1217
- * @param {string} chatId The chat ID
1218
- * @returns {Promise<[Message]|[]>}
1219
- */
1220
- async getPinnedMessages(chatId) {
1221
- const pinnedMsgs = await this.pupPage.evaluate(async (chatId) => {
1222
- const chatWid = window.Store.WidFactory.createWid(chatId);
1223
- const chat = window.Store.Chat.get(chatWid) ?? await window.Store.Chat.find(chatWid);
1224
- if (!chat) return [];
1225
-
1226
- const msgs = await window.Store.PinnedMsgUtils.getTable().equals(['chatId'], chatWid.toString());
1227
-
1228
- const pinnedMsgs = msgs.map((msg) => window.Store.Msg.get(msg.parentMsgKey));
1229
-
1230
- return !pinnedMsgs.length
1231
- ? []
1232
- : pinnedMsgs.map((msg) => window.WWebJS.getMessageModel(msg));
1233
- }, chatId);
1234
-
1235
- return pinnedMsgs.map((msg) => new Message(this, msg));
1236
- }
1237
-
1238
- /**
1239
- * Returns an object with information about the invite code's group
1240
- * @param {string} inviteCode
1241
- * @returns {Promise<object>} Invite information
1242
- */
1243
- async getInviteInfo(inviteCode) {
1244
- return await this.pupPage.evaluate(inviteCode => {
1245
- return window.Store.GroupInvite.queryGroupInvite(inviteCode);
1246
- }, inviteCode);
1247
- }
1248
-
1249
- /**
1250
- * Accepts an invitation to join a group
1251
- * @param {string} inviteCode Invitation code
1252
- * @returns {Promise<string>} Id of the joined Chat
1253
- */
1254
- async acceptInvite(inviteCode) {
1255
- const res = await this.pupPage.evaluate(async inviteCode => {
1256
- return await window.Store.GroupInvite.joinGroupViaInvite(inviteCode);
1257
- }, inviteCode);
1258
-
1259
- return res.gid._serialized;
1260
- }
1261
-
1262
- /**
1263
- * Accepts a channel admin invitation and promotes the current user to a channel admin
1264
- * @param {string} channelId The channel ID to accept the admin invitation from
1265
- * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1266
- */
1267
- async acceptChannelAdminInvite(channelId) {
1268
- return await this.pupPage.evaluate(async (channelId) => {
1269
- try {
1270
- await window.Store.ChannelUtils.acceptNewsletterAdminInvite(channelId);
1271
- return true;
1272
- } catch (err) {
1273
- if (err.name === 'ServerStatusCodeError') return false;
1274
- throw err;
1275
- }
1276
- }, channelId);
1277
- }
1278
-
1279
- /**
1280
- * Revokes a channel admin invitation sent to a user by a channel owner
1281
- * @param {string} channelId The channel ID an invitation belongs to
1282
- * @param {string} userId The user ID the invitation was sent to
1283
- * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1284
- */
1285
- async revokeChannelAdminInvite(channelId, userId) {
1286
- return await this.pupPage.evaluate(async (channelId, userId) => {
1287
- try {
1288
- const userWid = window.Store.WidFactory.createWid(userId);
1289
- await window.Store.ChannelUtils.revokeNewsletterAdminInvite(channelId, userWid);
1290
- return true;
1291
- } catch (err) {
1292
- if (err.name === 'ServerStatusCodeError') return false;
1293
- throw err;
1294
- }
1295
- }, channelId, userId);
1296
- }
1297
-
1298
- /**
1299
- * Demotes a channel admin to a regular subscriber (can be used also for self-demotion)
1300
- * @param {string} channelId The channel ID to demote an admin in
1301
- * @param {string} userId The user ID to demote
1302
- * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1303
- */
1304
- async demoteChannelAdmin(channelId, userId) {
1305
- return await this.pupPage.evaluate(async (channelId, userId) => {
1306
- try {
1307
- const userWid = window.Store.WidFactory.createWid(userId);
1308
- await window.Store.ChannelUtils.demoteNewsletterAdmin(channelId, userWid);
1309
- return true;
1310
- } catch (err) {
1311
- if (err.name === 'ServerStatusCodeError') return false;
1312
- throw err;
1313
- }
1314
- }, channelId, userId);
1315
- }
1316
-
1317
- /**
1318
- * Accepts a private invitation to join a group
1319
- * @param {object} inviteInfo Invite V4 Info
1320
- * @returns {Promise<Object>}
1321
- */
1322
- async acceptGroupV4Invite(inviteInfo) {
1323
- if (!inviteInfo.inviteCode) throw 'Invalid invite code, try passing the message.inviteV4 object';
1324
- if (inviteInfo.inviteCodeExp == 0) throw 'Expired invite code';
1325
- return this.pupPage.evaluate(async inviteInfo => {
1326
- let { groupId, fromId, inviteCode, inviteCodeExp } = inviteInfo;
1327
- let userWid = window.Store.WidFactory.createWid(fromId);
1328
- return await window.Store.GroupInviteV4.joinGroupViaInviteV4(inviteCode, String(inviteCodeExp), groupId, userWid);
1329
- }, inviteInfo);
1330
- }
1331
-
1332
- /**
1333
- * Sets the current user's status message
1334
- * @param {string} status New status message
1335
- */
1336
- async setStatus(status) {
1337
- await this.pupPage.evaluate(async status => {
1338
- return await window.Store.StatusUtils.setMyStatus(status);
1339
- }, status);
1340
- }
1341
-
1342
- /**
1343
- * Sets the current user's display name.
1344
- * 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.
1345
- * @param {string} displayName New display name
1346
- * @returns {Promise<Boolean>}
1347
- */
1348
- async setDisplayName(displayName) {
1349
- const couldSet = await this.pupPage.evaluate(async displayName => {
1350
- if(!window.Store.Conn.canSetMyPushname()) return false;
1351
- await window.Store.Settings.setPushname(displayName);
1352
- return true;
1353
- }, displayName);
1354
-
1355
- return couldSet;
1356
- }
1357
-
1358
- /**
1359
- * Gets the current connection state for the client
1360
- * @returns {WAState}
1361
- */
1362
- async getState() {
1363
- return await this.pupPage.evaluate(() => {
1364
- if(!window.Store) return null;
1365
- return window.Store.AppState.state;
1366
- });
1367
- }
1368
-
1369
- /**
1370
- * Marks the client as online
1371
- */
1372
- async sendPresenceAvailable() {
1373
- return await this.pupPage.evaluate(() => {
1374
- return window.Store.PresenceUtils.sendPresenceAvailable();
1375
- });
1376
- }
1377
-
1378
- /**
1379
- * Marks the client as unavailable
1380
- */
1381
- async sendPresenceUnavailable() {
1382
- return await this.pupPage.evaluate(() => {
1383
- return window.Store.PresenceUtils.sendPresenceUnavailable();
1384
- });
1385
- }
1386
-
1387
- /**
1388
- * Enables and returns the archive state of the Chat
1389
- * @returns {boolean}
1390
- */
1391
- async archiveChat(chatId) {
1392
- return await this.pupPage.evaluate(async chatId => {
1393
- let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1394
- await window.Store.Cmd.archiveChat(chat, true);
1395
- return true;
1396
- }, chatId);
1397
- }
1398
-
1399
- /**
1400
- * Changes and returns the archive state of the Chat
1401
- * @returns {boolean}
1402
- */
1403
- async unarchiveChat(chatId) {
1404
- return await this.pupPage.evaluate(async chatId => {
1405
- let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1406
- await window.Store.Cmd.archiveChat(chat, false);
1407
- return false;
1408
- }, chatId);
1409
- }
1410
-
1411
- /**
1412
- * Pins the Chat
1413
- * @returns {Promise<boolean>} New pin state. Could be false if the max number of pinned chats was reached.
1414
- */
1415
- async pinChat(chatId) {
1416
- return this.pupPage.evaluate(async chatId => {
1417
- let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1418
- if (chat.pin) {
1419
- return true;
1420
- }
1421
- const MAX_PIN_COUNT = 3;
1422
- const chatModels = window.Store.Chat.getModelsArray();
1423
- if (chatModels.length > MAX_PIN_COUNT) {
1424
- let maxPinned = chatModels[MAX_PIN_COUNT - 1].pin;
1425
- if (maxPinned) {
1426
- return false;
1427
- }
1428
- }
1429
- await window.Store.Cmd.pinChat(chat, true);
1430
- return true;
1431
- }, chatId);
1432
- }
1433
-
1434
- /**
1435
- * Unpins the Chat
1436
- * @returns {Promise<boolean>} New pin state
1437
- */
1438
- async unpinChat(chatId) {
1439
- return this.pupPage.evaluate(async chatId => {
1440
- let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1441
- if (!chat.pin) {
1442
- return false;
1443
- }
1444
- await window.Store.Cmd.pinChat(chat, false);
1445
- return false;
1446
- }, chatId);
1447
- }
1448
-
1449
- /**
1450
- * Mutes this chat forever, unless a date is specified
1451
- * @param {string} chatId ID of the chat that will be muted
1452
- * @param {?Date} unmuteDate Date when the chat will be unmuted, don't provide a value to mute forever
1453
- * @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
1454
- */
1455
- async muteChat(chatId, unmuteDate) {
1456
- unmuteDate = unmuteDate ? Math.floor(unmuteDate.getTime() / 1000) : -1;
1457
- return this._muteUnmuteChat(chatId, 'MUTE', unmuteDate);
1458
- }
1459
-
1460
- /**
1461
- * Unmutes the Chat
1462
- * @param {string} chatId ID of the chat that will be unmuted
1463
- * @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
1464
- */
1465
- async unmuteChat(chatId) {
1466
- return this._muteUnmuteChat(chatId, 'UNMUTE');
1467
- }
1468
-
1469
- /**
1470
- * Internal method to mute or unmute the chat
1471
- * @param {string} chatId ID of the chat that will be muted/unmuted
1472
- * @param {string} action The action: 'MUTE' or 'UNMUTE'
1473
- * @param {number} unmuteDateTs Timestamp at which the chat will be unmuted
1474
- * @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
1475
- */
1476
- async _muteUnmuteChat (chatId, action, unmuteDateTs) {
1477
- return this.pupPage.evaluate(async (chatId, action, unmuteDateTs) => {
1478
- const chat = window.Store.Chat.get(chatId) ?? await window.Store.Chat.find(chatId);
1479
- action === 'MUTE'
1480
- ? await chat.mute.mute({ expiration: unmuteDateTs, sendDevice: true })
1481
- : await chat.mute.unmute({ sendDevice: true });
1482
- return { isMuted: chat.mute.expiration !== 0, muteExpiration: chat.mute.expiration };
1483
- }, chatId, action, unmuteDateTs || -1);
1484
- }
1485
-
1486
- /**
1487
- * Mark the Chat as unread
1488
- * @param {string} chatId ID of the chat that will be marked as unread
1489
- */
1490
- async markChatUnread(chatId) {
1491
- await this.pupPage.evaluate(async chatId => {
1492
- let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1493
- await window.Store.Cmd.markChatUnread(chat, true);
1494
- }, chatId);
1495
- }
1496
-
1497
- /**
1498
- * Returns the contact ID's profile picture URL, if privacy settings allow it
1499
- * @param {string} contactId the whatsapp user's ID
1500
- * @returns {Promise<string>}
1501
- */
1502
- async getProfilePicUrl(contactId) {
1503
- const profilePic = await this.pupPage.evaluate(async contactId => {
1504
- try {
1505
- const chatWid = window.Store.WidFactory.createWid(contactId);
1506
- return window.compareWwebVersions(window.Debug.VERSION, '<', '2.3000.0')
1507
- ? await window.Store.ProfilePic.profilePicFind(chatWid)
1508
- : await window.Store.ProfilePic.requestProfilePicFromServer(chatWid);
1509
- } catch (err) {
1510
- if(err.name === 'ServerStatusCodeError') return undefined;
1511
- throw err;
1512
- }
1513
- }, contactId);
1514
-
1515
- return profilePic ? profilePic.eurl : undefined;
1516
- }
1517
-
1518
- /**
1519
- * Gets the Contact's common groups with you. Returns empty array if you don't have any common group.
1520
- * @param {string} contactId the whatsapp user's ID (_serialized format)
1521
- * @returns {Promise<WAWebJS.ChatId[]>}
1522
- */
1523
- async getCommonGroups(contactId) {
1524
- const commonGroups = await this.pupPage.evaluate(async (contactId) => {
1525
- let contact = window.Store.Contact.get(contactId);
1526
- if (!contact) {
1527
- const wid = window.Store.WidFactory.createUserWid(contactId);
1528
- const chatConstructor = window.Store.Contact.getModelsArray().find(c=>!c.isGroup).constructor;
1529
- contact = new chatConstructor({id: wid});
1530
- }
1531
-
1532
- if (contact.commonGroups) {
1533
- return contact.commonGroups.serialize();
1534
- }
1535
- const status = await window.Store.findCommonGroups(contact);
1536
- if (status) {
1537
- return contact.commonGroups.serialize();
1538
- }
1539
- return [];
1540
- }, contactId);
1541
- const chats = [];
1542
- for (const group of commonGroups) {
1543
- chats.push(group.id);
1544
- }
1545
- return chats;
1546
- }
1547
-
1548
- /**
1549
- * Force reset of connection state for the client
1550
- */
1551
- async resetState() {
1552
- await this.pupPage.evaluate(() => {
1553
- window.Store.AppState.reconnect();
1554
- });
1555
- }
1556
-
1557
- /**
1558
- * Check if a given ID is registered in whatsapp
1559
- * @param {string} id the whatsapp user's ID
1560
- * @returns {Promise<Boolean>}
1561
- */
1562
- async isRegisteredUser(id) {
1563
- return Boolean(await this.getNumberId(id));
1564
- }
1565
-
1566
- /**
1567
- * Get the registered WhatsApp ID for a number.
1568
- * Will return null if the number is not registered on WhatsApp.
1569
- * @param {string} number Number or ID ("@c.us" will be automatically appended if not specified)
1570
- * @returns {Promise<Object|null>}
1571
- */
1572
- async getNumberId(number) {
1573
- if (!number.endsWith('@c.us')) {
1574
- number += '@c.us';
1575
- }
1576
-
1577
- return await this.pupPage.evaluate(async number => {
1578
- const wid = window.Store.WidFactory.createWid(number);
1579
- const result = await window.Store.QueryExist(wid);
1580
- if (!result || result.wid === undefined) return null;
1581
- return result.wid;
1582
- }, number);
1583
- }
1584
-
1585
- /**
1586
- * Get the formatted number of a WhatsApp ID.
1587
- * @param {string} number Number or ID
1588
- * @returns {Promise<string>}
1589
- */
1590
- async getFormattedNumber(number) {
1591
- if (!number.endsWith('@s.whatsapp.net')) number = number.replace('c.us', 's.whatsapp.net');
1592
- if (!number.includes('@s.whatsapp.net')) number = `${number}@s.whatsapp.net`;
1593
-
1594
- return await this.pupPage.evaluate(async numberId => {
1595
- return window.Store.NumberInfo.formattedPhoneNumber(numberId);
1596
- }, number);
1597
- }
1598
-
1599
- /**
1600
- * Get the country code of a WhatsApp ID.
1601
- * @param {string} number Number or ID
1602
- * @returns {Promise<string>}
1603
- */
1604
- async getCountryCode(number) {
1605
- number = number.replace(' ', '').replace('+', '').replace('@c.us', '');
1606
-
1607
- return await this.pupPage.evaluate(async numberId => {
1608
- return window.Store.NumberInfo.findCC(numberId);
1609
- }, number);
1610
- }
1611
-
1612
- /**
1613
- * An object that represents the result for a participant added to a group
1614
- * @typedef {Object} ParticipantResult
1615
- * @property {number} statusCode The status code of the result
1616
- * @property {string} message The result message
1617
- * @property {boolean} isGroupCreator Indicates if the participant is a group creator
1618
- * @property {boolean} isInviteV4Sent Indicates if the inviteV4 was sent to the participant
1619
- */
1620
-
1621
- /**
1622
- * An object that handles the result for {@link createGroup} method
1623
- * @typedef {Object} CreateGroupResult
1624
- * @property {string} title A group title
1625
- * @property {Object} gid An object that handles the newly created group ID
1626
- * @property {string} gid.server
1627
- * @property {string} gid.user
1628
- * @property {string} gid._serialized
1629
- * @property {Object.<string, ParticipantResult>} participants An object that handles the result value for each added to the group participant
1630
- */
1631
-
1632
- /**
1633
- * An object that handles options for group creation
1634
- * @typedef {Object} CreateGroupOptions
1635
- * @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)
1636
- * @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)
1637
- * @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)
1638
- * @property {string} [comment = ''] The comment to be added to an inviteV4 (empty string by default)
1639
- * @property {boolean} [memberAddMode = false] If true, only admins can add members to the group (false by default)
1640
- * @property {boolean} [membershipApprovalMode = false] If true, group admins will be required to approve anyone who wishes to join the group (false by default)
1641
- * @property {boolean} [isRestrict = true] If true, only admins can change group group info (true by default)
1642
- * @property {boolean} [isAnnounce = false] If true, only admins can send messages (false by default)
1643
- */
1644
-
1645
- /**
1646
- * Creates a new group
1647
- * @param {string} title Group title
1648
- * @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
1649
- * @param {CreateGroupOptions} options An object that handles options for group creation
1650
- * @returns {Promise<CreateGroupResult|string>} Object with resulting data or an error message as a string
1651
- */
1652
- async createGroup(title, participants = [], options = {}) {
1653
- !Array.isArray(participants) && (participants = [participants]);
1654
- participants.map(p => (p instanceof Contact) ? p.id._serialized : p);
1655
-
1656
- return await this.pupPage.evaluate(async (title, participants, options) => {
1657
- const {
1658
- messageTimer = 0,
1659
- parentGroupId,
1660
- autoSendInviteV4 = true,
1661
- comment = '',
1662
- } = options;
1663
- const participantData = {}, participantWids = [], failedParticipants = [];
1664
- let createGroupResult, parentGroupWid;
1665
-
1666
- const addParticipantResultCodes = {
1667
- default: 'An unknown error occupied while adding a participant',
1668
- 200: 'The participant was added successfully',
1669
- 403: 'The participant can be added by sending private invitation only',
1670
- 404: 'The phone number is not registered on WhatsApp'
1671
- };
1672
-
1673
- for (const participant of participants) {
1674
- const pWid = window.Store.WidFactory.createWid(participant);
1675
- if ((await window.Store.QueryExist(pWid))?.wid) {
1676
- participantWids.push({ phoneNumber: pWid });
1677
- }
1678
- else failedParticipants.push(participant);
1679
- }
1680
-
1681
- parentGroupId && (parentGroupWid = window.Store.WidFactory.createWid(parentGroupId));
1682
-
1683
- try {
1684
- createGroupResult = await window.Store.GroupUtils.createGroup(
1685
- {
1686
- 'addressingModeOverride': 'lid',
1687
- 'memberAddMode': options.memberAddMode ?? false,
1688
- 'membershipApprovalMode': options.membershipApprovalMode ?? false,
1689
- 'announce': options.announce ?? false,
1690
- 'restrict': options.isRestrict !== undefined ? !options.isRestrict : false,
1691
- 'ephemeralDuration': messageTimer,
1692
- 'parentGroupId': parentGroupWid,
1693
- 'title': title,
1694
- },
1695
- participantWids
1696
- );
1697
- } catch (err) {
1698
- return 'CreateGroupError: An unknown error occupied while creating a group';
1699
- }
1700
-
1701
- for (const participant of createGroupResult.participants) {
1702
- let isInviteV4Sent = false;
1703
- participant.wid.server == 'lid' && (participant.wid = window.Store.LidUtils.getPhoneNumber(participant.wid));
1704
- const participantId = participant.wid._serialized;
1705
- const statusCode = participant.error || 200;
1706
-
1707
- if (autoSendInviteV4 && statusCode === 403) {
1708
- window.Store.Contact.gadd(participant.wid, { silent: true });
1709
- const addParticipantResult = await window.Store.GroupInviteV4.sendGroupInviteMessage(
1710
- window.Store.Chat.get(participant.wid) || await window.Store.Chat.find(participant.wid),
1711
- createGroupResult.wid._serialized,
1712
- createGroupResult.subject,
1713
- participant.invite_code,
1714
- participant.invite_code_exp,
1715
- comment,
1716
- await window.WWebJS.getProfilePicThumbToBase64(createGroupResult.wid)
1717
- );
1718
- isInviteV4Sent = addParticipantResult.messageSendResult === 'OK';
1719
- }
1720
-
1721
- participantData[participantId] = {
1722
- statusCode: statusCode,
1723
- message: addParticipantResultCodes[statusCode] || addParticipantResultCodes.default,
1724
- isGroupCreator: participant.type === 'superadmin',
1725
- isInviteV4Sent: isInviteV4Sent
1726
- };
1727
- }
1728
-
1729
- for (const f of failedParticipants) {
1730
- participantData[f] = {
1731
- statusCode: 404,
1732
- message: addParticipantResultCodes[404],
1733
- isGroupCreator: false,
1734
- isInviteV4Sent: false
1735
- };
1736
- }
1737
-
1738
- return { title: title, gid: createGroupResult.wid, participants: participantData };
1739
- }, title, participants, options);
1740
- }
1741
-
1742
- /**
1743
- * An object that handles the result for {@link createChannel} method
1744
- * @typedef {Object} CreateChannelResult
1745
- * @property {string} title A channel title
1746
- * @property {ChatId} nid An object that handels the newly created channel ID
1747
- * @property {string} nid.server 'newsletter'
1748
- * @property {string} nid.user 'XXXXXXXXXX'
1749
- * @property {string} nid._serialized 'XXXXXXXXXX@newsletter'
1750
- * @property {string} inviteLink The channel invite link, starts with 'https://whatsapp.com/channel/'
1751
- * @property {number} createdAtTs The timestamp the channel was created at
1752
- */
1753
-
1754
- /**
1755
- * Options for the channel creation
1756
- * @typedef {Object} CreateChannelOptions
1757
- * @property {?string} description The channel description
1758
- * @property {?MessageMedia} picture The channel profile picture
1759
- */
1760
-
1761
- /**
1762
- * Creates a new channel
1763
- * @param {string} title The channel name
1764
- * @param {CreateChannelOptions} options
1765
- * @returns {Promise<CreateChannelResult|string>} Returns an object that handles the result for the channel creation or an error message as a string
1766
- */
1767
- async createChannel(title, options = {}) {
1768
- return await this.pupPage.evaluate(async (title, options) => {
1769
- let response, { description = null, picture = null } = options;
1770
-
1771
- if (!window.Store.ChannelUtils.isNewsletterCreationEnabled()) {
1772
- return 'CreateChannelError: A channel creation is not enabled';
1773
- }
1774
-
1775
- if (picture) {
1776
- picture = await window.WWebJS.cropAndResizeImage(picture, {
1777
- asDataUrl: true,
1778
- mimetype: 'image/jpeg',
1779
- size: 640,
1780
- quality: 1
1781
- });
1782
- }
1783
-
1784
- try {
1785
- response = await window.Store.ChannelUtils.createNewsletterQuery({
1786
- name: title,
1787
- description: description,
1788
- picture: picture,
1789
- });
1790
- } catch (err) {
1791
- if (err.name === 'ServerStatusCodeError') {
1792
- return 'CreateChannelError: An error occupied while creating a channel';
1793
- }
1794
- throw err;
1795
- }
1796
-
1797
- return {
1798
- title: title,
1799
- nid: window.Store.JidToWid.newsletterJidToWid(response.idJid),
1800
- inviteLink: `https://whatsapp.com/channel/${response.newsletterInviteLinkMetadataMixin.inviteCode}`,
1801
- createdAtTs: response.newsletterCreationTimeMetadataMixin.creationTimeValue
1802
- };
1803
- }, title, options);
1804
- }
1805
-
1806
- /**
1807
- * Subscribe to channel
1808
- * @param {string} channelId The channel ID
1809
- * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1810
- */
1811
- async subscribeToChannel(channelId) {
1812
- return await this.pupPage.evaluate(async (channelId) => {
1813
- return await window.WWebJS.subscribeToUnsubscribeFromChannel(channelId, 'Subscribe');
1814
- }, channelId);
1815
- }
1816
-
1817
- /**
1818
- * Options for unsubscribe from a channel
1819
- * @typedef {Object} UnsubscribeOptions
1820
- * @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
1821
- */
1822
-
1823
- /**
1824
- * Unsubscribe from channel
1825
- * @param {string} channelId The channel ID
1826
- * @param {UnsubscribeOptions} options
1827
- * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1828
- */
1829
- async unsubscribeFromChannel(channelId, options) {
1830
- return await this.pupPage.evaluate(async (channelId, options) => {
1831
- return await window.WWebJS.subscribeToUnsubscribeFromChannel(channelId, 'Unsubscribe', options);
1832
- }, channelId, options);
1833
- }
1834
-
1835
- /**
1836
- * Options for transferring a channel ownership to another user
1837
- * @typedef {Object} TransferChannelOwnershipOptions
1838
- * @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.
1839
- */
1840
-
1841
- /**
1842
- * Transfers a channel ownership to another user.
1843
- * Note: the user you are transferring the channel ownership to must be a channel admin.
1844
- * @param {string} channelId
1845
- * @param {string} newOwnerId
1846
- * @param {TransferChannelOwnershipOptions} options
1847
- * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1848
- */
1849
- async transferChannelOwnership(channelId, newOwnerId, options = {}) {
1850
- return await this.pupPage.evaluate(async (channelId, newOwnerId, options) => {
1851
- const channel = await window.WWebJS.getChat(channelId, { getAsModel: false });
1852
- const newOwner = window.Store.Contact.get(newOwnerId) || (await window.Store.Contact.find(newOwnerId));
1853
- if (!channel.newsletterMetadata) {
1854
- await window.Store.NewsletterMetadataCollection.update(channel.id);
1855
- }
1856
-
1857
- try {
1858
- await window.Store.ChannelUtils.changeNewsletterOwnerAction(channel, newOwner);
1859
-
1860
- if (options.shouldDismissSelfAsAdmin) {
1861
- const meContact = window.Store.ContactCollection.getMeContact();
1862
- meContact && (await window.Store.ChannelUtils.demoteNewsletterAdminAction(channel, meContact));
1863
- }
1864
- } catch (error) {
1865
- return false;
1866
- }
1867
-
1868
- return true;
1869
- }, channelId, newOwnerId, options);
1870
- }
1871
-
1872
- /**
1873
- * Searches for channels based on search criteria, there are some notes:
1874
- * 1. The method finds only channels you are not subscribed to currently
1875
- * 2. If you have never been subscribed to a found channel
1876
- * or you have unsubscribed from it with {@link UnsubscribeOptions.deleteLocalModels} set to 'true',
1877
- * the lastMessage property of a found channel will be 'null'
1878
- *
1879
- * @param {Object} searchOptions Search options
1880
- * @param {string} [searchOptions.searchText = ''] Text to search
1881
- * @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
1882
- * @param {boolean} [searchOptions.skipSubscribedNewsletters = false] If true, channels that user is subscribed to won't appear in found channels
1883
- * @param {number} [searchOptions.view = 0] View type, makes sense only when the searchText is empty. Valid values to provide are:
1884
- * 0 for RECOMMENDED channels
1885
- * 1 for TRENDING channels
1886
- * 2 for POPULAR channels
1887
- * 3 for NEW channels
1888
- * @param {number} [searchOptions.limit = 50] The limit of found channels to be appear in the returnig result
1889
- * @returns {Promise<Array<Channel>|[]>} Returns an array of Channel objects or an empty array if no channels were found
1890
- */
1891
- async searchChannels(searchOptions = {}) {
1892
- return await this.pupPage.evaluate(async ({
1893
- searchText = '',
1894
- countryCodes = [window.Store.ChannelUtils.currentRegion],
1895
- skipSubscribedNewsletters = false,
1896
- view = 0,
1897
- limit = 50
1898
- }) => {
1899
- searchText = searchText.trim();
1900
- const currentRegion = window.Store.ChannelUtils.currentRegion;
1901
- if (![0, 1, 2, 3].includes(view)) view = 0;
1902
-
1903
- countryCodes = countryCodes.length === 1 && countryCodes[0] === currentRegion
1904
- ? countryCodes
1905
- : countryCodes.filter((code) => Object.keys(window.Store.ChannelUtils.countryCodesIso).includes(code));
1906
-
1907
- const viewTypeMapping = {
1908
- 0: 'RECOMMENDED',
1909
- 1: 'TRENDING',
1910
- 2: 'POPULAR',
1911
- 3: 'NEW'
1912
- };
1913
-
1914
- searchOptions = {
1915
- searchText: searchText,
1916
- countryCodes: countryCodes,
1917
- skipSubscribedNewsletters: skipSubscribedNewsletters,
1918
- view: viewTypeMapping[view],
1919
- categories: [],
1920
- cursorToken: ''
1921
- };
1922
-
1923
- const originalFunction = window.Store.ChannelUtils.getNewsletterDirectoryPageSize;
1924
- limit !== 50 && (window.Store.ChannelUtils.getNewsletterDirectoryPageSize = () => limit);
1925
-
1926
- const channels = (await window.Store.ChannelUtils.fetchNewsletterDirectories(searchOptions)).newsletters;
1927
-
1928
- limit !== 50 && (window.Store.ChannelUtils.getNewsletterDirectoryPageSize = originalFunction);
1929
-
1930
- return channels
1931
- ? await Promise.all(channels.map((channel) => window.WWebJS.getChatModel(channel, { isChannel: true })))
1932
- : [];
1933
- }, searchOptions);
1934
- }
1935
-
1936
- /**
1937
- * Deletes the channel you created
1938
- * @param {string} channelId The ID of a channel to delete
1939
- * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1940
- */
1941
- async deleteChannel(channelId) {
1942
- return await this.client.pupPage.evaluate(async (channelId) => {
1943
- const channel = await window.WWebJS.getChat(channelId, { getAsModel: false });
1944
- if (!channel) return false;
1945
- try {
1946
- await window.Store.ChannelUtils.deleteNewsletterAction(channel);
1947
- return true;
1948
- } catch (err) {
1949
- if (err.name === 'ServerStatusCodeError') return false;
1950
- throw err;
1951
- }
1952
- }, channelId);
1953
- }
1954
-
1955
- /**
1956
- * Get all current Labels
1957
- * @returns {Promise<Array<Label>>}
1958
- */
1959
- async getLabels() {
1960
- const labels = await this.pupPage.evaluate(async () => {
1961
- return window.WWebJS.getLabels();
1962
- });
1963
-
1964
- return labels.map(data => new Label(this, data));
1965
- }
1966
-
1967
- /**
1968
- * Get all current Broadcast
1969
- * @returns {Promise<Array<Broadcast>>}
1970
- */
1971
- async getBroadcasts() {
1972
- const broadcasts = await this.pupPage.evaluate(async () => {
1973
- return window.WWebJS.getAllStatuses();
1974
- });
1975
- return broadcasts.map(data => new Broadcast(this, data));
1976
- }
1977
-
1978
- /**
1979
- * Get Label instance by ID
1980
- * @param {string} labelId
1981
- * @returns {Promise<Label>}
1982
- */
1983
- async getLabelById(labelId) {
1984
- const label = await this.pupPage.evaluate(async (labelId) => {
1985
- return window.WWebJS.getLabel(labelId);
1986
- }, labelId);
1987
-
1988
- return new Label(this, label);
1989
- }
1990
-
1991
- /**
1992
- * Get all Labels assigned to a chat
1993
- * @param {string} chatId
1994
- * @returns {Promise<Array<Label>>}
1995
- */
1996
- async getChatLabels(chatId) {
1997
- const labels = await this.pupPage.evaluate(async (chatId) => {
1998
- return window.WWebJS.getChatLabels(chatId);
1999
- }, chatId);
2000
-
2001
- return labels.map(data => new Label(this, data));
2002
- }
2003
-
2004
- /**
2005
- * Get all Chats for a specific Label
2006
- * @param {string} labelId
2007
- * @returns {Promise<Array<Chat>>}
2008
- */
2009
- async getChatsByLabelId(labelId) {
2010
- const chatIds = await this.pupPage.evaluate(async (labelId) => {
2011
- const label = window.Store.Label.get(labelId);
2012
- const labelItems = label.labelItemCollection.getModelsArray();
2013
- return labelItems.reduce((result, item) => {
2014
- if (item.parentType === 'Chat') {
2015
- result.push(item.parentId);
2016
- }
2017
- return result;
2018
- }, []);
2019
- }, labelId);
2020
-
2021
- return Promise.all(chatIds.map(id => this.getChatById(id)));
2022
- }
2023
-
2024
- /**
2025
- * Gets all blocked contacts by host account
2026
- * @returns {Promise<Array<Contact>>}
2027
- */
2028
- async getBlockedContacts() {
2029
- const blockedContacts = await this.pupPage.evaluate(() => {
2030
- let chatIds = window.Store.Blocklist.getModelsArray().map(a => a.id._serialized);
2031
- return Promise.all(chatIds.map(id => window.WWebJS.getContact(id)));
2032
- });
2033
-
2034
- return blockedContacts.map(contact => ContactFactory.create(this.client, contact));
2035
- }
2036
-
2037
- /**
2038
- * Sets the current user's profile picture.
2039
- * @param {MessageMedia} media
2040
- * @returns {Promise<boolean>} Returns true if the picture was properly updated.
2041
- */
2042
- async setProfilePicture(media) {
2043
- const success = await this.pupPage.evaluate((chatid, media) => {
2044
- return window.WWebJS.setPicture(chatid, media);
2045
- }, this.info.wid._serialized, media);
2046
-
2047
- return success;
2048
- }
2049
-
2050
- /**
2051
- * Deletes the current user's profile picture.
2052
- * @returns {Promise<boolean>} Returns true if the picture was properly deleted.
2053
- */
2054
- async deleteProfilePicture() {
2055
- const success = await this.pupPage.evaluate((chatid) => {
2056
- return window.WWebJS.deletePicture(chatid);
2057
- }, this.info.wid._serialized);
2058
-
2059
- return success;
2060
- }
2061
-
2062
- /**
2063
- * Change labels in chats
2064
- * @param {Array<number|string>} labelIds
2065
- * @param {Array<string>} chatIds
2066
- * @returns {Promise<void>}
2067
- */
2068
- async addOrRemoveLabels(labelIds, chatIds) {
2069
-
2070
- return this.pupPage.evaluate(async (labelIds, chatIds) => {
2071
- if (['smba', 'smbi'].indexOf(window.Store.Conn.platform) === -1) {
2072
- throw '[LT01] Only Whatsapp business';
2073
- }
2074
- const labels = window.WWebJS.getLabels().filter(e => labelIds.find(l => l == e.id) !== undefined);
2075
- const chats = window.Store.Chat.filter(e => chatIds.includes(e.id._serialized));
2076
-
2077
- let actions = labels.map(label => ({id: label.id, type: 'add'}));
2078
-
2079
- chats.forEach(chat => {
2080
- (chat.labels || []).forEach(n => {
2081
- if (!actions.find(e => e.id == n)) {
2082
- actions.push({id: n, type: 'remove'});
2083
- }
2084
- });
2085
- });
2086
-
2087
- return await window.Store.Label.addOrRemoveLabels(actions, chats);
2088
- }, labelIds, chatIds);
2089
- }
2090
-
2091
- /**
2092
- * An object that handles the information about the group membership request
2093
- * @typedef {Object} GroupMembershipRequest
2094
- * @property {Object} id The wid of a user who requests to enter the group
2095
- * @property {Object} addedBy The wid of a user who created that request
2096
- * @property {Object|null} parentGroupId The wid of a community parent group to which the current group is linked
2097
- * @property {string} requestMethod The method used to create the request: NonAdminAdd/InviteLink/LinkedGroupJoin
2098
- * @property {number} t The timestamp the request was created at
2099
- */
2100
-
2101
- /**
2102
- * Gets an array of membership requests
2103
- * @param {string} groupId The ID of a group to get membership requests for
2104
- * @returns {Promise<Array<GroupMembershipRequest>>} An array of membership requests
2105
- */
2106
- async getGroupMembershipRequests(groupId) {
2107
- return await this.pupPage.evaluate(async (groupId) => {
2108
- const groupWid = window.Store.WidFactory.createWid(groupId);
2109
- return await window.Store.MembershipRequestUtils.getMembershipApprovalRequests(groupWid);
2110
- }, groupId);
2111
- }
2112
-
2113
- /**
2114
- * An object that handles the result for membership request action
2115
- * @typedef {Object} MembershipRequestActionResult
2116
- * @property {string} requesterId User ID whos membership request was approved/rejected
2117
- * @property {number|undefined} error An error code that occurred during the operation for the participant
2118
- * @property {string} message A message with a result of membership request action
2119
- */
2120
-
2121
- /**
2122
- * An object that handles options for {@link approveGroupMembershipRequests} and {@link rejectGroupMembershipRequests} methods
2123
- * @typedef {Object} MembershipRequestActionOptions
2124
- * @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
2125
- * @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]
2126
- */
2127
-
2128
- /**
2129
- * Approves membership requests if any
2130
- * @param {string} groupId The group ID to get the membership request for
2131
- * @param {MembershipRequestActionOptions} options Options for performing a membership request action
2132
- * @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
2133
- */
2134
- async approveGroupMembershipRequests(groupId, options = {}) {
2135
- return await this.pupPage.evaluate(async (groupId, options) => {
2136
- const { requesterIds = null, sleep = [250, 500] } = options;
2137
- return await window.WWebJS.membershipRequestAction(groupId, 'Approve', requesterIds, sleep);
2138
- }, groupId, options);
2139
- }
2140
-
2141
- /**
2142
- * Rejects membership requests if any
2143
- * @param {string} groupId The group ID to get the membership request for
2144
- * @param {MembershipRequestActionOptions} options Options for performing a membership request action
2145
- * @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
2146
- */
2147
- async rejectGroupMembershipRequests(groupId, options = {}) {
2148
- return await this.pupPage.evaluate(async (groupId, options) => {
2149
- const { requesterIds = null, sleep = [250, 500] } = options;
2150
- return await window.WWebJS.membershipRequestAction(groupId, 'Reject', requesterIds, sleep);
2151
- }, groupId, options);
2152
- }
2153
-
2154
-
2155
- /**
2156
- * Setting autoload download audio
2157
- * @param {boolean} flag true/false
2158
- */
2159
- async setAutoDownloadAudio(flag) {
2160
- await this.pupPage.evaluate(async flag => {
2161
- const autoDownload = window.Store.Settings.getAutoDownloadAudio();
2162
- if (autoDownload === flag) {
2163
- return flag;
2164
- }
2165
- await window.Store.Settings.setAutoDownloadAudio(flag);
2166
- return flag;
2167
- }, flag);
2168
- }
2169
-
2170
- /**
2171
- * Setting autoload download documents
2172
- * @param {boolean} flag true/false
2173
- */
2174
- async setAutoDownloadDocuments(flag) {
2175
- await this.pupPage.evaluate(async flag => {
2176
- const autoDownload = window.Store.Settings.getAutoDownloadDocuments();
2177
- if (autoDownload === flag) {
2178
- return flag;
2179
- }
2180
- await window.Store.Settings.setAutoDownloadDocuments(flag);
2181
- return flag;
2182
- }, flag);
2183
- }
2184
-
2185
- /**
2186
- * Setting autoload download photos
2187
- * @param {boolean} flag true/false
2188
- */
2189
- async setAutoDownloadPhotos(flag) {
2190
- await this.pupPage.evaluate(async flag => {
2191
- const autoDownload = window.Store.Settings.getAutoDownloadPhotos();
2192
- if (autoDownload === flag) {
2193
- return flag;
2194
- }
2195
- await window.Store.Settings.setAutoDownloadPhotos(flag);
2196
- return flag;
2197
- }, flag);
2198
- }
2199
-
2200
- /**
2201
- * Setting autoload download videos
2202
- * @param {boolean} flag true/false
2203
- */
2204
- async setAutoDownloadVideos(flag) {
2205
- await this.pupPage.evaluate(async flag => {
2206
- const autoDownload = window.Store.Settings.getAutoDownloadVideos();
2207
- if (autoDownload === flag) {
2208
- return flag;
2209
- }
2210
- await window.Store.Settings.setAutoDownloadVideos(flag);
2211
- return flag;
2212
- }, flag);
2213
- }
2214
-
2215
- /**
2216
- * Setting background synchronization.
2217
- * NOTE: this action will take effect after you restart the client.
2218
- * @param {boolean} flag true/false
2219
- * @returns {Promise<boolean>}
2220
- */
2221
- async setBackgroundSync(flag) {
2222
- return await this.pupPage.evaluate(async flag => {
2223
- const backSync = window.Store.Settings.getGlobalOfflineNotifications();
2224
- if (backSync === flag) {
2225
- return flag;
2226
- }
2227
- await window.Store.Settings.setGlobalOfflineNotifications(flag);
2228
- return flag;
2229
- }, flag);
2230
- }
2231
-
2232
- /**
2233
- * Get user device count by ID
2234
- * Each WaWeb Connection counts as one device, and the phone (if exists) counts as one
2235
- * So for a non-enterprise user with one WaWeb connection it should return "2"
2236
- * @param {string} userId
2237
- * @returns {Promise<number>}
2238
- */
2239
- async getContactDeviceCount(userId) {
2240
- return await this.pupPage.evaluate(async (userId) => {
2241
- const devices = await window.Store.DeviceList.getDeviceIds([window.Store.WidFactory.createWid(userId)]);
2242
- if (devices && devices.length && devices[0] != null && typeof devices[0].devices == 'object') {
2243
- return devices[0].devices.length;
2244
- }
2245
- return 0;
2246
- }, userId);
2247
- }
2248
-
2249
- /**
2250
- * Sync chat history conversation
2251
- * @param {string} chatId
2252
- * @return {Promise<boolean>} True if operation completed successfully, false otherwise.
2253
- */
2254
- async syncHistory(chatId) {
2255
- return await this.pupPage.evaluate(async (chatId) => {
2256
- const chatWid = window.Store.WidFactory.createWid(chatId);
2257
- const chat = window.Store.Chat.get(chatWid) ?? (await window.Store.Chat.find(chatWid));
2258
- if (chat?.endOfHistoryTransferType === 0) {
2259
- await window.Store.HistorySync.sendPeerDataOperationRequest(3, {
2260
- chatId: chat.id
2261
- });
2262
- return true;
2263
- }
2264
- return false;
2265
- }, chatId);
2266
- }
2267
-
2268
- /**
2269
- * Generates a WhatsApp call link (video call or voice call)
2270
- * @param {Date} startTime The start time of the call
2271
- * @param {string} callType The type of a WhatsApp call link to generate, valid values are: `video` | `voice`
2272
- * @returns {Promise<string>} The WhatsApp call link (https://call.whatsapp.com/video/XxXxXxXxXxXxXx) or an empty string if a generation failed.
2273
- */
2274
- async createCallLink(startTime, callType) {
2275
- if (!['video', 'voice'].includes(callType)) {
2276
- throw new class CreateCallLinkError extends Error {
2277
- constructor(m) { super(m); }
2278
- }('Invalid \'callType\' parameter value is provided. Valid values are: \'voice\' | \'video\'.');
2279
- }
2280
-
2281
- startTime = Math.floor(startTime.getTime() / 1000);
2282
-
2283
- return await this.pupPage.evaluate(async (startTimeTs, callType) => {
2284
- const response = await window.Store.ScheduledEventMsgUtils.createEventCallLink(startTimeTs, callType);
2285
- return response ?? '';
2286
- }, startTime, callType);
2287
- }
2288
-
2289
- /**
2290
- * Sends a response to the scheduled event message, indicating whether a user is going to attend the event or not
2291
- * @param {number} response The response code to the scheduled event message. Valid values are: `0` for NONE response (removes a previous response) | `1` for GOING | `2` for NOT GOING | `3` for MAYBE going
2292
- * @param {string} eventMessageId The scheduled event message ID
2293
- * @returns {Promise<boolean>}
2294
- */
2295
- async sendResponseToScheduledEvent(response, eventMessageId) {
2296
- if (![0, 1, 2, 3].includes(response)) return false;
2297
-
2298
- return await this.pupPage.evaluate(async (response, msgId) => {
2299
- const eventMsg = window.Store.Msg.get(msgId) || (await window.Store.Msg.getMessagesById([msgId]))?.messages?.[0];
2300
- if (!eventMsg) return false;
2301
-
2302
- await window.Store.ScheduledEventMsgUtils.sendEventResponseMsg(response, eventMsg);
2303
- return true;
2304
- }, response, eventMessageId);
2305
- }
2306
-
2307
- /**
2308
- * Save new contact to user's addressbook or edit the existing one
2309
- * @param {string} phoneNumber The contact's phone number in a format "17182222222", where "1" is a country code
2310
- * @param {string} firstName
2311
- * @param {string} lastName
2312
- * @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
2313
- * @returns {Promise<import('..').ChatId>} Object in a wid format
2314
- */
2315
- async saveOrEditAddressbookContact(phoneNumber, firstName, lastName, syncToAddressbook = false)
2316
- {
2317
- return await this.pupPage.evaluate(async (phoneNumber, firstName, lastName, syncToAddressbook) => {
2318
- return await window.Store.AddressbookContactUtils.saveContactAction(
2319
- phoneNumber,
2320
- null,
2321
- firstName,
2322
- lastName,
2323
- syncToAddressbook
2324
- );
2325
- }, phoneNumber, firstName, lastName, syncToAddressbook);
2326
- }
2327
-
2328
- /**
2329
- * Deletes the contact from user's addressbook
2330
- * @param {string} phoneNumber The contact's phone number in a format "17182222222", where "1" is a country code
2331
- * @returns {Promise<void>}
2332
- */
2333
- async deleteAddressbookContact(phoneNumber)
2334
- {
2335
- return await this.pupPage.evaluate(async (phoneNumber) => {
2336
- return await window.Store.AddressbookContactUtils.deleteContactAction(phoneNumber);
2337
- }, phoneNumber);
2338
- }
2339
-
2340
- /**
2341
- * Get lid and phone number for multiple users
2342
- * @param {string[]} userIds - Array of user IDs
2343
- * @returns {Promise<Array<{ lid: string, pn: string }>>}
2344
- */
2345
- async getContactLidAndPhone(userIds) {
2346
- return await this.pupPage.evaluate(async (userIds) => {
2347
- if (!Array.isArray(userIds)) userIds = [userIds];
2348
-
2349
- return await Promise.all(userIds.map(async (userId) => {
2350
- const { lid, phone } = await window.WWebJS.enforceLidAndPnRetrieval(userId);
2351
-
2352
- return {
2353
- lid: lid?._serialized,
2354
- pn: phone?._serialized
2355
- };
2356
- }));
2357
- }, userIds);
2358
- }
2359
- }
2360
-
2361
- module.exports = Client;
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, MessageTypes } = 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 { ClientInfo, Message, MessageMedia, Contact, Location, Poll, PollVote, GroupNotification, Label, Call, Buttons, List, Reaction, Broadcast, ScheduledEvent } = require('./structures');
19
+ const NoAuth = require('./authStrategies/NoAuth');
20
+ const {exposeFunctionIfAbsent} = require('./util/Puppeteer');
21
+
22
+ /**
23
+ * Starting point for interacting with the WhatsApp Web API
24
+ * @extends {EventEmitter}
25
+ * @param {object} options - Client options
26
+ * @param {AuthStrategy} options.authStrategy - Determines how to save and restore sessions. Will use LegacySessionAuth if options.session is set. Otherwise, NoAuth will be used.
27
+ * @param {string} options.webVersion - The version of WhatsApp Web to use. Use options.webVersionCache to configure how the version is retrieved.
28
+ * @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.
29
+ * @param {number} options.authTimeoutMs - Timeout for authentication selector in puppeteer
30
+ * @param {function} options.evalOnNewDoc - function to eval on new doc
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 {string} options.deviceName - Sets the device name of a current linked device., i.e.: 'TEST'.
41
+ * @param {string} options.browserName - Sets the browser name of a current linked device, i.e.: 'Firefox'.
42
+ * @param {object} options.proxyAuthentication - Proxy Authentication object.
43
+ *
44
+ * @fires Client#qr
45
+ * @fires Client#authenticated
46
+ * @fires Client#auth_failure
47
+ * @fires Client#ready
48
+ * @fires Client#message
49
+ * @fires Client#message_ack
50
+ * @fires Client#message_create
51
+ * @fires Client#message_revoke_me
52
+ * @fires Client#message_revoke_everyone
53
+ * @fires Client#message_ciphertext
54
+ * @fires Client#message_edit
55
+ * @fires Client#media_uploaded
56
+ * @fires Client#group_join
57
+ * @fires Client#group_leave
58
+ * @fires Client#group_update
59
+ * @fires Client#disconnected
60
+ * @fires Client#change_state
61
+ * @fires Client#contact_changed
62
+ * @fires Client#group_admin_changed
63
+ * @fires Client#group_membership_request
64
+ * @fires Client#vote_update
65
+ */
66
+ class Client extends EventEmitter {
67
+ constructor(options = {}) {
68
+ super();
69
+
70
+ this.options = Util.mergeDefault(DefaultOptions, options);
71
+
72
+ if(!this.options.authStrategy) {
73
+ this.authStrategy = new NoAuth();
74
+ } else {
75
+ this.authStrategy = this.options.authStrategy;
76
+ }
77
+
78
+ this.authStrategy.setup(this);
79
+
80
+ /**
81
+ * @type {puppeteer.Browser}
82
+ */
83
+ this.pupBrowser = null;
84
+ /**
85
+ * @type {puppeteer.Page}
86
+ */
87
+ this.pupPage = null;
88
+
89
+ this.currentIndexHtml = null;
90
+ this.lastLoggedOut = false;
91
+
92
+ Util.setFfmpegPath(this.options.ffmpegPath);
93
+ }
94
+ /**
95
+ * Injection logic
96
+ * Private function
97
+ */
98
+ async inject() {
99
+ if(this.options.authTimeoutMs === undefined || this.options.authTimeoutMs==0){
100
+ this.options.authTimeoutMs = 30000;
101
+ }
102
+ let start = Date.now();
103
+ let timeout = this.options.authTimeoutMs;
104
+ let res = false;
105
+ while(start > (Date.now() - timeout)){
106
+ res = await this.pupPage.evaluate('window.Debug?.VERSION != undefined');
107
+ if(res){break;}
108
+ await new Promise(r => setTimeout(r, 200));
109
+ }
110
+ if(!res){
111
+ throw 'auth timeout';
112
+ }
113
+ await this.setDeviceName(this.options.deviceName, this.options.browserName);
114
+ const pairWithPhoneNumber = this.options.pairWithPhoneNumber;
115
+ const version = await this.getWWebVersion();
116
+ const isCometOrAbove = parseInt(version.split('.')?.[1]) >= 3000;
117
+
118
+ if (isCometOrAbove) {
119
+ await this.pupPage.evaluate(ExposeAuthStore);
120
+ } else {
121
+ await this.pupPage.evaluate(ExposeLegacyAuthStore, moduleRaid.toString());
122
+ }
123
+
124
+ const needAuthentication = await this.pupPage.evaluate(async () => {
125
+ let state = window.AuthStore.AppState.state;
126
+
127
+ if (state === 'OPENING' || state === 'UNLAUNCHED' || state === 'PAIRING') {
128
+ // wait till state changes
129
+ await new Promise(r => {
130
+ window.AuthStore.AppState.on('change:state', function waitTillInit(_AppState, state) {
131
+ if (state !== 'OPENING' && state !== 'UNLAUNCHED' && state !== 'PAIRING') {
132
+ window.AuthStore.AppState.off('change:state', waitTillInit);
133
+ r();
134
+ }
135
+ });
136
+ });
137
+ }
138
+ state = window.AuthStore.AppState.state;
139
+ return state == 'UNPAIRED' || state == 'UNPAIRED_IDLE';
140
+ });
141
+
142
+ if (needAuthentication) {
143
+ const { failed, failureEventPayload, restart } = await this.authStrategy.onAuthenticationNeeded();
144
+
145
+ if(failed) {
146
+ /**
147
+ * Emitted when there has been an error while trying to restore an existing session
148
+ * @event Client#auth_failure
149
+ * @param {string} message
150
+ */
151
+ this.emit(Events.AUTHENTICATION_FAILURE, failureEventPayload);
152
+ await this.destroy();
153
+ if (restart) {
154
+ // session restore failed so try again but without session to force new authentication
155
+ return this.initialize();
156
+ }
157
+ return;
158
+ }
159
+
160
+ // Register qr/code events
161
+ if (pairWithPhoneNumber.phoneNumber) {
162
+ await exposeFunctionIfAbsent(this.pupPage, 'onCodeReceivedEvent', async (code) => {
163
+ /**
164
+ * Emitted when a pairing code is received
165
+ * @event Client#code
166
+ * @param {string} code Code
167
+ * @returns {string} Code that was just received
168
+ */
169
+ this.emit(Events.CODE_RECEIVED, code);
170
+ return code;
171
+ });
172
+ this.requestPairingCode(pairWithPhoneNumber.phoneNumber, pairWithPhoneNumber.showNotification, pairWithPhoneNumber.intervalMs);
173
+ } else {
174
+ let qrRetries = 0;
175
+ await exposeFunctionIfAbsent(this.pupPage, 'onQRChangedEvent', async (qr) => {
176
+ /**
177
+ * Emitted when a QR code is received
178
+ * @event Client#qr
179
+ * @param {string} qr QR Code
180
+ */
181
+ this.emit(Events.QR_RECEIVED, qr);
182
+ if (this.options.qrMaxRetries > 0) {
183
+ qrRetries++;
184
+ if (qrRetries > this.options.qrMaxRetries) {
185
+ this.emit(Events.DISCONNECTED, 'Max qrcode retries reached');
186
+ await this.destroy();
187
+ }
188
+ }
189
+ });
190
+
191
+
192
+ await this.pupPage.evaluate(async () => {
193
+ const registrationInfo = await window.AuthStore.RegistrationUtils.waSignalStore.getRegistrationInfo();
194
+ const noiseKeyPair = await window.AuthStore.RegistrationUtils.waNoiseInfo.get();
195
+ const staticKeyB64 = window.AuthStore.Base64Tools.encodeB64(noiseKeyPair.staticKeyPair.pubKey);
196
+ const identityKeyB64 = window.AuthStore.Base64Tools.encodeB64(registrationInfo.identityKeyPair.pubKey);
197
+ const advSecretKey = await window.AuthStore.RegistrationUtils.getADVSecretKey();
198
+ const platform = window.AuthStore.RegistrationUtils.DEVICE_PLATFORM;
199
+ const getQR = (ref) => ref + ',' + staticKeyB64 + ',' + identityKeyB64 + ',' + advSecretKey + ',' + platform;
200
+
201
+ window.onQRChangedEvent(getQR(window.AuthStore.Conn.ref)); // initial qr
202
+ window.AuthStore.Conn.on('change:ref', (_, ref) => { window.onQRChangedEvent(getQR(ref)); }); // future QR changes
203
+ });
204
+ }
205
+ }
206
+
207
+ await exposeFunctionIfAbsent(this.pupPage, 'onAuthAppStateChangedEvent', async (state) => {
208
+ if (state == 'UNPAIRED_IDLE' && !pairWithPhoneNumber.phoneNumber) {
209
+ // refresh qr code
210
+ window.Store.Cmd.refreshQR();
211
+ }
212
+ });
213
+
214
+ await exposeFunctionIfAbsent(this.pupPage, 'onAppStateHasSyncedEvent', async () => {
215
+ const authEventPayload = await this.authStrategy.getAuthEventPayload();
216
+ /**
217
+ * Emitted when authentication is successful
218
+ * @event Client#authenticated
219
+ */
220
+ this.emit(Events.AUTHENTICATED, authEventPayload);
221
+
222
+ const injected = await this.pupPage.evaluate(async () => {
223
+ return typeof window.Store !== 'undefined' && typeof window.WWebJS !== 'undefined';
224
+ });
225
+
226
+ if (!injected) {
227
+ if (this.options.webVersionCache.type === 'local' && this.currentIndexHtml) {
228
+ const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache;
229
+ const webCache = WebCacheFactory.createWebCache(webCacheType, webCacheOptions);
230
+
231
+ await webCache.persist(this.currentIndexHtml, version);
232
+ }
233
+
234
+ if (isCometOrAbove) {
235
+ await this.pupPage.evaluate(ExposeStore);
236
+ } else {
237
+ // make sure all modules are ready before injection
238
+ // 2 second delay after authentication makes sense and does not need to be made dyanmic or removed
239
+ await new Promise(r => setTimeout(r, 2000));
240
+ await this.pupPage.evaluate(ExposeLegacyStore);
241
+ }
242
+ let start = Date.now();
243
+ let res = false;
244
+ while(start > (Date.now() - 30000)){
245
+ // Check window.Store Injection
246
+ res = await this.pupPage.evaluate('window.Store != undefined');
247
+ if(res){break;}
248
+ await new Promise(r => setTimeout(r, 200));
249
+ }
250
+ if(!res){
251
+ throw 'ready timeout';
252
+ }
253
+
254
+ /**
255
+ * Current connection information
256
+ * @type {ClientInfo}
257
+ */
258
+ this.info = new ClientInfo(this, await this.pupPage.evaluate(() => {
259
+ return { ...window.Store.Conn.serialize(), wid: window.Store.User.getMaybeMePnUser() || window.Store.User.getMaybeMeLidUser() };
260
+ }));
261
+
262
+ this.interface = new InterfaceController(this);
263
+
264
+ //Load util functions (serializers, helper functions)
265
+ await this.pupPage.evaluate(LoadUtils);
266
+
267
+ await this.attachEventListeners();
268
+ }
269
+ /**
270
+ * Emitted when the client has initialized and is ready to receive messages.
271
+ * @event Client#ready
272
+ */
273
+ this.emit(Events.READY);
274
+ this.authStrategy.afterAuthReady();
275
+ });
276
+ let lastPercent = null;
277
+ await exposeFunctionIfAbsent(this.pupPage, 'onOfflineProgressUpdateEvent', async (percent) => {
278
+ if (lastPercent !== percent) {
279
+ lastPercent = percent;
280
+ this.emit(Events.LOADING_SCREEN, percent, 'WhatsApp'); // Message is hardcoded as "WhatsApp" for now
281
+ }
282
+ });
283
+ await exposeFunctionIfAbsent(this.pupPage, 'onLogoutEvent', async () => {
284
+ this.lastLoggedOut = true;
285
+ await this.pupPage.waitForNavigation({waitUntil: 'load', timeout: 5000}).catch((_) => _);
286
+ });
287
+ await this.pupPage.evaluate(() => {
288
+ window.AuthStore.AppState.on('change:state', (_AppState, state) => { window.onAuthAppStateChangedEvent(state); });
289
+ window.AuthStore.AppState.on('change:hasSynced', () => { window.onAppStateHasSyncedEvent(); });
290
+ window.AuthStore.Cmd.on('offline_progress_update', () => {
291
+ window.onOfflineProgressUpdateEvent(window.AuthStore.OfflineMessageHandler.getOfflineDeliveryProgress());
292
+ });
293
+ window.AuthStore.Cmd.on('logout', async () => {
294
+ await window.onLogoutEvent();
295
+ });
296
+ });
297
+ }
298
+
299
+ /**
300
+ * Sets up events and requirements, kicks off authentication request
301
+ */
302
+ async initialize() {
303
+
304
+ let
305
+ /**
306
+ * @type {puppeteer.Browser}
307
+ */
308
+ browser,
309
+ /**
310
+ * @type {puppeteer.Page}
311
+ */
312
+ page;
313
+
314
+ browser = null;
315
+ page = null;
316
+
317
+ await this.authStrategy.beforeBrowserInitialized();
318
+
319
+ const puppeteerOpts = this.options.puppeteer;
320
+ if (puppeteerOpts && (puppeteerOpts.browserWSEndpoint || puppeteerOpts.browserURL)) {
321
+ browser = await puppeteer.connect(puppeteerOpts);
322
+ page = await browser.newPage();
323
+ } else {
324
+ const browserArgs = [...(puppeteerOpts.args || [])];
325
+ if(this.options.userAgent !== false && !browserArgs.find(arg => arg.includes('--user-agent'))) {
326
+ browserArgs.push(`--user-agent=${this.options.userAgent}`);
327
+ }
328
+ // navigator.webdriver fix
329
+ browserArgs.push('--disable-blink-features=AutomationControlled');
330
+
331
+ browser = await puppeteer.launch({...puppeteerOpts, args: browserArgs});
332
+ page = (await browser.pages())[0];
333
+ }
334
+
335
+ if (this.options.proxyAuthentication !== undefined) {
336
+ await page.authenticate(this.options.proxyAuthentication);
337
+ }
338
+ if(this.options.userAgent !== false) {
339
+ await page.setUserAgent(this.options.userAgent);
340
+ }
341
+ if (this.options.bypassCSP) await page.setBypassCSP(true);
342
+
343
+ this.pupBrowser = browser;
344
+ this.pupPage = page;
345
+
346
+ await this.authStrategy.afterBrowserInitialized();
347
+ await this.initWebVersionCache();
348
+
349
+ if (this.options.evalOnNewDoc !== undefined) {
350
+ await page.evaluateOnNewDocument(this.options.evalOnNewDoc);
351
+ }
352
+
353
+ // ocVersion (isOfficialClient patch)
354
+ // remove after 2.3000.x hard release
355
+ await page.evaluateOnNewDocument(() => {
356
+ const originalError = Error;
357
+ window.originalError = originalError;
358
+ //eslint-disable-next-line no-global-assign
359
+ Error = function (message) {
360
+ const error = new originalError(message);
361
+ const originalStack = error.stack;
362
+ if (error.stack.includes('moduleRaid')) error.stack = originalStack + '\n at https://web.whatsapp.com/vendors~lazy_loaded_low_priority_components.05e98054dbd60f980427.js:2:44';
363
+ return error;
364
+ };
365
+ });
366
+
367
+ await page.goto(WhatsWebURL, {
368
+ waitUntil: 'load',
369
+ timeout: 0,
370
+ referer: 'https://whatsapp.com/'
371
+ });
372
+
373
+ await this.inject();
374
+
375
+ this.pupPage.on('framenavigated', async (frame) => {
376
+ if(frame.url().includes('post_logout=1') || this.lastLoggedOut) {
377
+ this.emit(Events.DISCONNECTED, 'LOGOUT');
378
+ await this.authStrategy.logout();
379
+ await this.authStrategy.beforeBrowserInitialized();
380
+ await this.authStrategy.afterBrowserInitialized();
381
+ this.lastLoggedOut = false;
382
+ }
383
+ await this.inject();
384
+ });
385
+ }
386
+
387
+ /**
388
+ * Request authentication via pairing code instead of QR code
389
+ * @param {string} phoneNumber - Phone number in international, symbol-free format (e.g. 12025550108 for US, 551155501234 for Brazil)
390
+ * @param {boolean} [showNotification = true] - Show notification to pair on phone number
391
+ * @param {number} [intervalMs = 180000] - The interval in milliseconds on how frequent to generate pairing code (WhatsApp default to 3 minutes)
392
+ * @returns {Promise<string>} - Returns a pairing code in format "ABCDEFGH"
393
+ */
394
+ async requestPairingCode(phoneNumber, showNotification = true, intervalMs = 180000) {
395
+ return await this.pupPage.evaluate(async (phoneNumber, showNotification, intervalMs) => {
396
+ const getCode = async () => {
397
+ while (!window.AuthStore.PairingCodeLinkUtils) {
398
+ await new Promise(resolve => setTimeout(resolve, 250));
399
+ }
400
+ window.AuthStore.PairingCodeLinkUtils.setPairingType('ALT_DEVICE_LINKING');
401
+ await window.AuthStore.PairingCodeLinkUtils.initializeAltDeviceLinking();
402
+ return window.AuthStore.PairingCodeLinkUtils.startAltLinkingFlow(phoneNumber, showNotification);
403
+ };
404
+ if (window.codeInterval) {
405
+ clearInterval(window.codeInterval); // remove existing interval
406
+ }
407
+ window.codeInterval = setInterval(async () => {
408
+ if (window.AuthStore.AppState.state != 'UNPAIRED' && window.AuthStore.AppState.state != 'UNPAIRED_IDLE') {
409
+ clearInterval(window.codeInterval);
410
+ return;
411
+ }
412
+ window.onCodeReceivedEvent(await getCode());
413
+ }, intervalMs);
414
+ return window.onCodeReceivedEvent(await getCode());
415
+ }, phoneNumber, showNotification, intervalMs);
416
+ }
417
+
418
+ /**
419
+ * Attach event listeners to WA Web
420
+ * Private function
421
+ * @property {boolean} reinject is this a reinject?
422
+ */
423
+ async attachEventListeners() {
424
+ await exposeFunctionIfAbsent(this.pupPage, 'onAddMessageEvent', msg => {
425
+ if (msg.type === 'gp2') {
426
+ const notification = new GroupNotification(this, msg);
427
+ if (['add', 'invite', 'linked_group_join'].includes(msg.subtype)) {
428
+ /**
429
+ * Emitted when a user joins the chat via invite link or is added by an admin.
430
+ * @event Client#group_join
431
+ * @param {GroupNotification} notification GroupNotification with more information about the action
432
+ */
433
+ this.emit(Events.GROUP_JOIN, notification);
434
+ } else if (msg.subtype === 'remove' || msg.subtype === 'leave') {
435
+ /**
436
+ * Emitted when a user leaves the chat or is removed by an admin.
437
+ * @event Client#group_leave
438
+ * @param {GroupNotification} notification GroupNotification with more information about the action
439
+ */
440
+ this.emit(Events.GROUP_LEAVE, notification);
441
+ } else if (msg.subtype === 'promote' || msg.subtype === 'demote') {
442
+ /**
443
+ * Emitted when a current user is promoted to an admin or demoted to a regular user.
444
+ * @event Client#group_admin_changed
445
+ * @param {GroupNotification} notification GroupNotification with more information about the action
446
+ */
447
+ this.emit(Events.GROUP_ADMIN_CHANGED, notification);
448
+ } else if (msg.subtype === 'membership_approval_request') {
449
+ /**
450
+ * Emitted when some user requested to join the group
451
+ * that has the membership approval mode turned on
452
+ * @event Client#group_membership_request
453
+ * @param {GroupNotification} notification GroupNotification with more information about the action
454
+ * @param {string} notification.chatId The group ID the request was made for
455
+ * @param {string} notification.author The user ID that made a request
456
+ * @param {number} notification.timestamp The timestamp the request was made at
457
+ */
458
+ this.emit(Events.GROUP_MEMBERSHIP_REQUEST, notification);
459
+ } else {
460
+ /**
461
+ * Emitted when group settings are updated, such as subject, description or picture.
462
+ * @event Client#group_update
463
+ * @param {GroupNotification} notification GroupNotification with more information about the action
464
+ */
465
+ this.emit(Events.GROUP_UPDATE, notification);
466
+ }
467
+ return;
468
+ }
469
+
470
+ const message = new Message(this, msg);
471
+
472
+ /**
473
+ * Emitted when a new message is created, which may include the current user's own messages.
474
+ * @event Client#message_create
475
+ * @param {Message} message The message that was created
476
+ */
477
+ this.emit(Events.MESSAGE_CREATE, message);
478
+
479
+ if (msg.id.fromMe) return;
480
+
481
+ /**
482
+ * Emitted when a new message is received.
483
+ * @event Client#message
484
+ * @param {Message} message The message that was received
485
+ */
486
+ this.emit(Events.MESSAGE_RECEIVED, message);
487
+ });
488
+
489
+ let last_message;
490
+
491
+ await exposeFunctionIfAbsent(this.pupPage, 'onChangeMessageTypeEvent', (msg) => {
492
+
493
+ if (msg.type === 'revoked') {
494
+ const message = new Message(this, msg);
495
+ let revoked_msg;
496
+ if (last_message && msg.id.id === last_message.id.id) {
497
+ revoked_msg = new Message(this, last_message);
498
+
499
+ if (message.protocolMessageKey)
500
+ revoked_msg.id = { ...message.protocolMessageKey };
501
+ }
502
+
503
+ /**
504
+ * Emitted when a message is deleted for everyone in the chat.
505
+ * @event Client#message_revoke_everyone
506
+ * @param {Message} message The message that was revoked, in its current state. It will not contain the original message's data.
507
+ * @param {?Message} revoked_msg The message that was revoked, before it was revoked. It will contain the message's original data.
508
+ * Note that due to the way this data is captured, it may be possible that this param will be undefined.
509
+ */
510
+ this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg);
511
+ }
512
+
513
+ });
514
+
515
+ await exposeFunctionIfAbsent(this.pupPage, 'onChangeMessageEvent', (msg) => {
516
+
517
+ if (msg.type !== 'revoked') {
518
+ last_message = msg;
519
+ }
520
+
521
+ /**
522
+ * The event notification that is received when one of
523
+ * the group participants changes their phone number.
524
+ */
525
+ const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify';
526
+
527
+ /**
528
+ * The event notification that is received when one of
529
+ * the contacts changes their phone number.
530
+ */
531
+ const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number';
532
+
533
+ if (isParticipant || isContact) {
534
+ /** @type {GroupNotification} object does not provide enough information about this event, so a @type {Message} object is used. */
535
+ const message = new Message(this, msg);
536
+
537
+ const newId = isParticipant ? msg.recipients[0] : msg.to;
538
+ const oldId = isParticipant ? msg.author : msg.templateParams.find(id => id !== newId);
539
+
540
+ /**
541
+ * Emitted when a contact or a group participant changes their phone number.
542
+ * @event Client#contact_changed
543
+ * @param {Message} message Message with more information about the event.
544
+ * @param {String} oldId The user's id (an old one) who changed their phone number
545
+ * and who triggered the notification.
546
+ * @param {String} newId The user's new id after the change.
547
+ * @param {Boolean} isContact Indicates if a contact or a group participant changed their phone number.
548
+ */
549
+ this.emit(Events.CONTACT_CHANGED, message, oldId, newId, isContact);
550
+ }
551
+ });
552
+
553
+ await exposeFunctionIfAbsent(this.pupPage, 'onRemoveMessageEvent', (msg) => {
554
+
555
+ if (!msg.isNewMsg) return;
556
+
557
+ const message = new Message(this, msg);
558
+
559
+ /**
560
+ * Emitted when a message is deleted by the current user.
561
+ * @event Client#message_revoke_me
562
+ * @param {Message} message The message that was revoked
563
+ */
564
+ this.emit(Events.MESSAGE_REVOKED_ME, message);
565
+
566
+ });
567
+
568
+ await exposeFunctionIfAbsent(this.pupPage, 'onMessageAckEvent', (msg, ack) => {
569
+
570
+ const message = new Message(this, msg);
571
+
572
+ /**
573
+ * Emitted when an ack event occurrs on message type.
574
+ * @event Client#message_ack
575
+ * @param {Message} message The message that was affected
576
+ * @param {MessageAck} ack The new ACK value
577
+ */
578
+ this.emit(Events.MESSAGE_ACK, message, ack);
579
+
580
+ });
581
+
582
+ await exposeFunctionIfAbsent(this.pupPage, 'onChatUnreadCountEvent', async (data) =>{
583
+ const chat = await this.getChatById(data.id);
584
+
585
+ /**
586
+ * Emitted when the chat unread count changes
587
+ */
588
+ this.emit(Events.UNREAD_COUNT, chat);
589
+ });
590
+
591
+ await exposeFunctionIfAbsent(this.pupPage, 'onMessageMediaUploadedEvent', (msg) => {
592
+
593
+ const message = new Message(this, msg);
594
+
595
+ /**
596
+ * Emitted when media has been uploaded for a message sent by the client.
597
+ * @event Client#media_uploaded
598
+ * @param {Message} message The message with media that was uploaded
599
+ */
600
+ this.emit(Events.MEDIA_UPLOADED, message);
601
+ });
602
+
603
+ await exposeFunctionIfAbsent(this.pupPage, 'onAppStateChangedEvent', async (state) => {
604
+ /**
605
+ * Emitted when the connection state changes
606
+ * @event Client#change_state
607
+ * @param {WAState} state the new connection state
608
+ */
609
+ this.emit(Events.STATE_CHANGED, state);
610
+
611
+ const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT];
612
+
613
+ if (this.options.takeoverOnConflict) {
614
+ ACCEPTED_STATES.push(WAState.CONFLICT);
615
+
616
+ if (state === WAState.CONFLICT) {
617
+ setTimeout(() => {
618
+ this.pupPage.evaluate(() => window.Store.AppState.takeover());
619
+ }, this.options.takeoverTimeoutMs);
620
+ }
621
+ }
622
+
623
+ if (!ACCEPTED_STATES.includes(state)) {
624
+ /**
625
+ * Emitted when the client has been disconnected
626
+ * @event Client#disconnected
627
+ * @param {WAState|"LOGOUT"} reason reason that caused the disconnect
628
+ */
629
+ await this.authStrategy.disconnect();
630
+ this.emit(Events.DISCONNECTED, state);
631
+ this.destroy();
632
+ }
633
+ });
634
+
635
+ await exposeFunctionIfAbsent(this.pupPage, 'onBatteryStateChangedEvent', (state) => {
636
+ const { battery, plugged } = state;
637
+
638
+ if (battery === undefined) return;
639
+
640
+ /**
641
+ * Emitted when the battery percentage for the attached device changes. Will not be sent if using multi-device.
642
+ * @event Client#change_battery
643
+ * @param {object} batteryInfo
644
+ * @param {number} batteryInfo.battery - The current battery percentage
645
+ * @param {boolean} batteryInfo.plugged - Indicates if the phone is plugged in (true) or not (false)
646
+ * @deprecated
647
+ */
648
+ this.emit(Events.BATTERY_CHANGED, { battery, plugged });
649
+ });
650
+
651
+ await exposeFunctionIfAbsent(this.pupPage, 'onIncomingCall', (call) => {
652
+ /**
653
+ * Emitted when a call is received
654
+ * @event Client#incoming_call
655
+ * @param {object} call
656
+ * @param {number} call.id - Call id
657
+ * @param {string} call.peerJid - Who called
658
+ * @param {boolean} call.isVideo - if is video
659
+ * @param {boolean} call.isGroup - if is group
660
+ * @param {boolean} call.canHandleLocally - if we can handle in waweb
661
+ * @param {boolean} call.outgoing - if is outgoing
662
+ * @param {boolean} call.webClientShouldHandle - If Waweb should handle
663
+ * @param {object} call.participants - Participants
664
+ */
665
+ const cll = new Call(this, call);
666
+ this.emit(Events.INCOMING_CALL, cll);
667
+ });
668
+
669
+ await exposeFunctionIfAbsent(this.pupPage, 'onReaction', (reactions) => {
670
+ for (const reaction of reactions) {
671
+ /**
672
+ * Emitted when a reaction is sent, received, updated or removed
673
+ * @event Client#message_reaction
674
+ * @param {object} reaction
675
+ * @param {object} reaction.id - Reaction id
676
+ * @param {number} reaction.orphan - Orphan
677
+ * @param {?string} reaction.orphanReason - Orphan reason
678
+ * @param {number} reaction.timestamp - Timestamp
679
+ * @param {string} reaction.reaction - Reaction
680
+ * @param {boolean} reaction.read - Read
681
+ * @param {object} reaction.msgId - Parent message id
682
+ * @param {string} reaction.senderId - Sender id
683
+ * @param {?number} reaction.ack - Ack
684
+ */
685
+
686
+ this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction));
687
+ }
688
+ });
689
+
690
+ await exposeFunctionIfAbsent(this.pupPage, 'onRemoveChatEvent', async (chat) => {
691
+ const _chat = await this.getChatById(chat.id);
692
+
693
+ /**
694
+ * Emitted when a chat is removed
695
+ * @event Client#chat_removed
696
+ * @param {Chat} chat
697
+ */
698
+ this.emit(Events.CHAT_REMOVED, _chat);
699
+ });
700
+
701
+ await exposeFunctionIfAbsent(this.pupPage, 'onArchiveChatEvent', async (chat, currState, prevState) => {
702
+ const _chat = await this.getChatById(chat.id);
703
+
704
+ /**
705
+ * Emitted when a chat is archived/unarchived
706
+ * @event Client#chat_archived
707
+ * @param {Chat} chat
708
+ * @param {boolean} currState
709
+ * @param {boolean} prevState
710
+ */
711
+ this.emit(Events.CHAT_ARCHIVED, _chat, currState, prevState);
712
+ });
713
+
714
+ await exposeFunctionIfAbsent(this.pupPage, 'onEditMessageEvent', (msg, newBody, prevBody) => {
715
+
716
+ if(msg.type === 'revoked'){
717
+ return;
718
+ }
719
+ /**
720
+ * Emitted when messages are edited
721
+ * @event Client#message_edit
722
+ * @param {Message} message
723
+ * @param {string} newBody
724
+ * @param {string} prevBody
725
+ */
726
+ this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody);
727
+ });
728
+
729
+ await exposeFunctionIfAbsent(this.pupPage, 'onAddMessageCiphertextEvent', msg => {
730
+
731
+ /**
732
+ * Emitted when messages are edited
733
+ * @event Client#message_ciphertext
734
+ * @param {Message} message
735
+ */
736
+ this.emit(Events.MESSAGE_CIPHERTEXT, new Message(this, msg));
737
+ });
738
+
739
+ await exposeFunctionIfAbsent(this.pupPage, 'onPollVoteEvent', (votes) => {
740
+ for (const vote of votes) {
741
+ /**
742
+ * Emitted when some poll option is selected or deselected,
743
+ * shows a user's current selected option(s) on the poll
744
+ * @event Client#vote_update
745
+ */
746
+ this.emit(Events.VOTE_UPDATE, new PollVote(this, vote));
747
+ }
748
+ });
749
+
750
+ await this.pupPage.evaluate(() => {
751
+ window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(window.WWebJS.getMessageModel(msg)); });
752
+ window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(window.WWebJS.getMessageModel(msg)); });
753
+ window.Store.Msg.on('change:ack', (msg, ack) => { window.onMessageAckEvent(window.WWebJS.getMessageModel(msg), ack); });
754
+ window.Store.Msg.on('change:isUnsentMedia', (msg, unsent) => { if (msg.id.fromMe && !unsent) window.onMessageMediaUploadedEvent(window.WWebJS.getMessageModel(msg)); });
755
+ window.Store.Msg.on('remove', (msg) => { if (msg.isNewMsg) window.onRemoveMessageEvent(window.WWebJS.getMessageModel(msg)); });
756
+ window.Store.Msg.on('change:body change:caption', (msg, newBody, prevBody) => { window.onEditMessageEvent(window.WWebJS.getMessageModel(msg), newBody, prevBody); });
757
+ window.Store.AppState.on('change:state', (_AppState, state) => { window.onAppStateChangedEvent(state); });
758
+ window.Store.Conn.on('change:battery', (state) => { window.onBatteryStateChangedEvent(state); });
759
+ window.Store.Call.on('add', (call) => { window.onIncomingCall(call); });
760
+ window.Store.Chat.on('remove', async (chat) => { window.onRemoveChatEvent(await window.WWebJS.getChatModel(chat)); });
761
+ window.Store.Chat.on('change:archive', async (chat, currState, prevState) => { window.onArchiveChatEvent(await window.WWebJS.getChatModel(chat), currState, prevState); });
762
+ window.Store.Msg.on('add', (msg) => {
763
+ if (msg.isNewMsg) {
764
+ if(msg.type === 'ciphertext') {
765
+ // defer message event until ciphertext is resolved (type changed)
766
+ msg.once('change:type', (_msg) => window.onAddMessageEvent(window.WWebJS.getMessageModel(_msg)));
767
+ window.onAddMessageCiphertextEvent(window.WWebJS.getMessageModel(msg));
768
+ } else {
769
+ window.onAddMessageEvent(window.WWebJS.getMessageModel(msg));
770
+ }
771
+ }
772
+ });
773
+ window.Store.Chat.on('change:unreadCount', (chat) => {window.onChatUnreadCountEvent(chat);});
774
+
775
+ if (window.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.1014111620')) {
776
+ const module = window.Store.AddonReactionTable;
777
+ const ogMethod = module.bulkUpsert;
778
+ module.bulkUpsert = ((...args) => {
779
+ window.onReaction(args[0].map(reaction => {
780
+ const msgKey = reaction.id;
781
+ const parentMsgKey = reaction.reactionParentKey;
782
+ const timestamp = reaction.reactionTimestamp / 1000;
783
+ const sender = reaction.author ?? reaction.from;
784
+ const senderUserJid = sender._serialized;
785
+
786
+ return {...reaction, msgKey, parentMsgKey, senderUserJid, timestamp };
787
+ }));
788
+
789
+ return ogMethod(...args);
790
+ }).bind(module);
791
+
792
+ const pollVoteModule = window.Store.AddonPollVoteTable;
793
+ const ogPollVoteMethod = pollVoteModule.bulkUpsert;
794
+
795
+ pollVoteModule.bulkUpsert = (async (...args) => {
796
+ const votes = await Promise.all(args[0].map(async vote => {
797
+ const msgKey = vote.id;
798
+ const parentMsgKey = vote.pollUpdateParentKey;
799
+ const timestamp = vote.t / 1000;
800
+ const sender = vote.author ?? vote.from;
801
+ const senderUserJid = sender._serialized;
802
+
803
+ let parentMessage = window.Store.Msg.get(parentMsgKey._serialized);
804
+ if (!parentMessage) {
805
+ const fetched = await window.Store.Msg.getMessagesById([parentMsgKey._serialized]);
806
+ parentMessage = fetched?.messages?.[0] || null;
807
+ }
808
+
809
+ return {
810
+ ...vote,
811
+ msgKey,
812
+ sender,
813
+ parentMsgKey,
814
+ senderUserJid,
815
+ timestamp,
816
+ parentMessage
817
+ };
818
+ }));
819
+
820
+ window.onPollVoteEvent(votes);
821
+
822
+ return ogPollVoteMethod.apply(pollVoteModule, args);
823
+ }).bind(pollVoteModule);
824
+ } else {
825
+ const module = window.Store.createOrUpdateReactionsModule;
826
+ const ogMethod = module.createOrUpdateReactions;
827
+ module.createOrUpdateReactions = ((...args) => {
828
+ window.onReaction(args[0].map(reaction => {
829
+ const msgKey = window.Store.MsgKey.fromString(reaction.msgKey);
830
+ const parentMsgKey = window.Store.MsgKey.fromString(reaction.parentMsgKey);
831
+ const timestamp = reaction.timestamp / 1000;
832
+
833
+ return {...reaction, msgKey, parentMsgKey, timestamp };
834
+ }));
835
+
836
+ return ogMethod(...args);
837
+ }).bind(module);
838
+ }
839
+ });
840
+ }
841
+
842
+ async initWebVersionCache() {
843
+ const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache;
844
+ const webCache = WebCacheFactory.createWebCache(webCacheType, webCacheOptions);
845
+
846
+ const requestedVersion = this.options.webVersion;
847
+ const versionContent = await webCache.resolve(requestedVersion);
848
+
849
+ if(versionContent) {
850
+ await this.pupPage.setRequestInterception(true);
851
+ this.pupPage.on('request', async (req) => {
852
+ if(req.url() === WhatsWebURL) {
853
+ req.respond({
854
+ status: 200,
855
+ contentType: 'text/html',
856
+ body: versionContent
857
+ });
858
+ } else {
859
+ req.continue();
860
+ }
861
+ });
862
+ } else {
863
+ this.pupPage.on('response', async (res) => {
864
+ if(res.ok() && res.url() === WhatsWebURL) {
865
+ const indexHtml = await res.text();
866
+ this.currentIndexHtml = indexHtml;
867
+ }
868
+ });
869
+ }
870
+ }
871
+
872
+ /**
873
+ * Closes the client
874
+ */
875
+ async destroy() {
876
+ await this.pupBrowser.close();
877
+ await this.authStrategy.destroy();
878
+ }
879
+
880
+ /**
881
+ * Logs out the client, closing the current session
882
+ */
883
+ async logout() {
884
+ await this.pupPage.evaluate(() => {
885
+ if (window.Store && window.Store.AppState && typeof window.Store.AppState.logout === 'function') {
886
+ return window.Store.AppState.logout();
887
+ }
888
+ });
889
+ await this.pupBrowser.close();
890
+
891
+ let maxDelay = 0;
892
+ while (this.pupBrowser.isConnected() && (maxDelay < 10)) { // waits a maximum of 1 second before calling the AuthStrategy
893
+ await new Promise(resolve => setTimeout(resolve, 100));
894
+ maxDelay++;
895
+ }
896
+
897
+ await this.authStrategy.logout();
898
+ }
899
+
900
+ /**
901
+ * Returns the version of WhatsApp Web currently being run
902
+ * @returns {Promise<string>}
903
+ */
904
+ async getWWebVersion() {
905
+ return await this.pupPage.evaluate(() => {
906
+ return window.Debug.VERSION;
907
+ });
908
+ }
909
+
910
+ async setDeviceName(deviceName, browserName) {
911
+ (deviceName || browserName) && await this.pupPage.evaluate((deviceName, browserName) => {
912
+ const func = window.require('WAWebMiscBrowserUtils').info;
913
+ window.require('WAWebMiscBrowserUtils').info = () => {
914
+ return {
915
+ ...func(),
916
+ ...(deviceName ? { os: deviceName } : {}),
917
+ ...(browserName ? { name: browserName } : {})
918
+ };
919
+ };
920
+ }, deviceName, browserName);
921
+ }
922
+
923
+ /**
924
+ * Mark as seen for the Chat
925
+ * @param {string} chatId
926
+ * @returns {Promise<boolean>} result
927
+ *
928
+ */
929
+ async sendSeen(chatId) {
930
+ return await this.pupPage.evaluate(async (chatId) => {
931
+ return window.WWebJS.sendSeen(chatId);
932
+ }, chatId);
933
+ }
934
+
935
+ /**
936
+ * An object representing mentions of groups
937
+ * @typedef {Object} GroupMention
938
+ * @property {string} subject - The name of a group to mention (can be custom)
939
+ * @property {string} id - The group ID, e.g.: 'XXXXXXXXXX@g.us'
940
+ */
941
+
942
+ /**
943
+ * Message options.
944
+ * @typedef {Object} MessageSendOptions
945
+ * @property {boolean} [linkPreview=true] - Show links preview. Has no effect on multi-device accounts.
946
+ * @property {boolean} [sendAudioAsVoice=false] - Send audio as voice message with a generated waveform
947
+ * @property {boolean} [sendVideoAsGif=false] - Send video as gif
948
+ * @property {boolean} [sendMediaAsSticker=false] - Send media as a sticker
949
+ * @property {boolean} [sendMediaAsDocument=false] - Send media as a document
950
+ * @property {boolean} [sendMediaAsHd=false] - Send image as quality HD
951
+ * @property {boolean} [isViewOnce=false] - Send photo/video as a view once message
952
+ * @property {boolean} [parseVCards=true] - Automatically parse vCards and send them as contacts
953
+ * @property {string} [caption] - Image or video caption
954
+ * @property {string} [quotedMessageId] - Id of the message that is being quoted (or replied to)
955
+ * @property {GroupMention[]} [groupMentions] - An array of object that handle group mentions
956
+ * @property {string[]} [mentions] - User IDs to mention in the message
957
+ * @property {boolean} [sendSeen=true] - Mark the conversation as seen after sending the message
958
+ * @property {string} [invokedBotWid=undefined] - Bot Wid when doing a bot mention like @Meta AI
959
+ * @property {string} [stickerAuthor=undefined] - Sets the author of the sticker, (if sendMediaAsSticker is true).
960
+ * @property {string} [stickerName=undefined] - Sets the name of the sticker, (if sendMediaAsSticker is true).
961
+ * @property {string[]} [stickerCategories=undefined] - Sets the categories of the sticker, (if sendMediaAsSticker is true). Provide emoji char array, can be null.
962
+ * @property {boolean} [ignoreQuoteErrors = true] - Should the bot send a quoted message without the quoted message if it fails to get the quote?
963
+ * @property {boolean} [waitUntilMsgSent = false] - Should the bot wait for the message send result?
964
+ * @property {MessageMedia} [media] - Media to be sent
965
+ * @property {any} [extra] - Extra options
966
+ */
967
+
968
+ /**
969
+ * Send a message to a specific chatId
970
+ * @param {string} chatId
971
+ * @param {string|MessageMedia|Location|Poll|Contact|Array<Contact>|Buttons|List} content
972
+ * @param {MessageSendOptions} [options] - Options used when sending the message
973
+ *
974
+ * @returns {Promise<Message>} Message that was just sent
975
+ */
976
+ async sendMessage(chatId, content, options = {}) {
977
+ const isChannel = /@\w*newsletter\b/.test(chatId);
978
+ const isStatus = /@\w*broadcast\b/.test(chatId);
979
+
980
+ if (isChannel && [
981
+ options.sendMediaAsDocument, options.quotedMessageId,
982
+ options.parseVCards, options.isViewOnce,
983
+ content instanceof Location, content instanceof Contact,
984
+ content instanceof Buttons, content instanceof List,
985
+ Array.isArray(content) && content.length > 0 && content[0] instanceof Contact
986
+ ].includes(true)) {
987
+ 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.');
988
+ return null;
989
+
990
+ } else if (isStatus && [
991
+ options.sendMediaAsDocument, options.quotedMessageId,
992
+ options.parseVCards, options.isViewOnce, options.sendMediaAsSticker,
993
+ content instanceof Location, content instanceof Contact,
994
+ content instanceof Poll, content instanceof Buttons, content instanceof List,
995
+ Array.isArray(content) && content.length > 0 && content[0] instanceof Contact
996
+ ].includes(true)) {
997
+ console.warn('The message type is currently not supported for sending in status broadcast,\nthe supported message types are: text, image, gif, audio and video.');
998
+ return null;
999
+ }
1000
+
1001
+ if (options.mentions) {
1002
+ !Array.isArray(options.mentions) && (options.mentions = [options.mentions]);
1003
+ if (options.mentions.some((possiblyContact) => possiblyContact instanceof Contact)) {
1004
+ console.warn('Mentions with an array of Contact are now deprecated. See more at https://github.com/pedroslopez/whatsapp-web.js/pull/2166.');
1005
+ options.mentions = options.mentions.map((a) => a.id._serialized);
1006
+ }
1007
+ }
1008
+
1009
+ options.groupMentions && !Array.isArray(options.groupMentions) && (options.groupMentions = [options.groupMentions]);
1010
+
1011
+ let internalOptions = {
1012
+ linkPreview: options.linkPreview === false ? undefined : true,
1013
+ sendAudioAsVoice: options.sendAudioAsVoice,
1014
+ sendVideoAsGif: options.sendVideoAsGif,
1015
+ sendMediaAsSticker: options.sendMediaAsSticker,
1016
+ sendMediaAsDocument: options.sendMediaAsDocument,
1017
+ sendMediaAsHd: options.sendMediaAsHd,
1018
+ caption: options.caption,
1019
+ quotedMessageId: options.quotedMessageId,
1020
+ parseVCards: options.parseVCards !== false,
1021
+ mentionedJidList: options.mentions || [],
1022
+ groupMentions: options.groupMentions,
1023
+ invokedBotWid: options.invokedBotWid,
1024
+ ignoreQuoteErrors: options.ignoreQuoteErrors !== false,
1025
+ waitUntilMsgSent: options.waitUntilMsgSent || false,
1026
+ extraOptions: options.extra
1027
+ };
1028
+
1029
+ const sendSeen = options.sendSeen !== false;
1030
+
1031
+ if (content instanceof MessageMedia) {
1032
+ internalOptions.media = content;
1033
+ internalOptions.isViewOnce = options.isViewOnce,
1034
+ content = '';
1035
+ } else if (options.media instanceof MessageMedia) {
1036
+ internalOptions.media = options.media;
1037
+ internalOptions.caption = content;
1038
+ internalOptions.isViewOnce = options.isViewOnce,
1039
+ content = '';
1040
+ } else if (content instanceof Location) {
1041
+ internalOptions.location = content;
1042
+ content = '';
1043
+ } else if (content instanceof Poll) {
1044
+ internalOptions.poll = content;
1045
+ content = '';
1046
+ } else if (content instanceof ScheduledEvent) {
1047
+ internalOptions.event = content;
1048
+ content = '';
1049
+ } else if (content instanceof Contact) {
1050
+ internalOptions.contactCard = content.id._serialized;
1051
+ content = '';
1052
+ } else if (Array.isArray(content) && content.length > 0 && content[0] instanceof Contact) {
1053
+ internalOptions.contactCardList = content.map(contact => contact.id._serialized);
1054
+ content = '';
1055
+ } else if (content instanceof Buttons) {
1056
+ console.warn('Buttons are now deprecated. See more at https://www.youtube.com/watch?v=hv1R1rLeVVE.');
1057
+ if (content.type !== 'chat') { internalOptions.attachment = content.body; }
1058
+ internalOptions.buttons = content;
1059
+ content = '';
1060
+ } else if (content instanceof List) {
1061
+ console.warn('Lists are now deprecated. See more at https://www.youtube.com/watch?v=hv1R1rLeVVE.');
1062
+ internalOptions.list = content;
1063
+ content = '';
1064
+ }
1065
+
1066
+ if (internalOptions.sendMediaAsSticker && internalOptions.media) {
1067
+ internalOptions.media = await Util.formatToWebpSticker(
1068
+ internalOptions.media, {
1069
+ name: options.stickerName,
1070
+ author: options.stickerAuthor,
1071
+ categories: options.stickerCategories
1072
+ }, this.pupPage
1073
+ );
1074
+ }
1075
+
1076
+ const sentMsg = await this.pupPage.evaluate(async (chatId, content, options, sendSeen) => {
1077
+ const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1078
+
1079
+ if (!chat) return null;
1080
+
1081
+ if (sendSeen) {
1082
+ await window.WWebJS.sendSeen(chatId);
1083
+ }
1084
+
1085
+ const msg = await window.WWebJS.sendMessage(chat, content, options);
1086
+ return msg
1087
+ ? window.WWebJS.getMessageModel(msg)
1088
+ : undefined;
1089
+ }, chatId, content, internalOptions, sendSeen);
1090
+
1091
+ return sentMsg
1092
+ ? new Message(this, sentMsg)
1093
+ : undefined;
1094
+ }
1095
+
1096
+ /**
1097
+ * @typedef {Object} SendChannelAdminInviteOptions
1098
+ * @property {?string} comment The comment to be added to an invitation
1099
+ */
1100
+
1101
+ /**
1102
+ * Sends a channel admin invitation to a user, allowing them to become an admin of the channel
1103
+ * @param {string} chatId The ID of a user to send the channel admin invitation to
1104
+ * @param {string} channelId The ID of a channel for which the invitation is being sent
1105
+ * @param {SendChannelAdminInviteOptions} options
1106
+ * @returns {Promise<boolean>} Returns true if an invitation was sent successfully, false otherwise
1107
+ */
1108
+ async sendChannelAdminInvite(chatId, channelId, options = {}) {
1109
+ const response = await this.pupPage.evaluate(async (chatId, channelId, options) => {
1110
+ const channelWid = window.Store.WidFactory.createWid(channelId);
1111
+ const chatWid = window.Store.WidFactory.createWid(chatId);
1112
+ const chat = window.Store.Chat.get(chatWid) || (await window.Store.Chat.find(chatWid));
1113
+
1114
+ if (!chatWid.isUser()) {
1115
+ return false;
1116
+ }
1117
+
1118
+ return await window.Store.SendChannelMessage.sendNewsletterAdminInviteMessage(
1119
+ chat,
1120
+ {
1121
+ newsletterWid: channelWid,
1122
+ invitee: chatWid,
1123
+ inviteMessage: options.comment,
1124
+ base64Thumb: await window.WWebJS.getProfilePicThumbToBase64(channelWid)
1125
+ }
1126
+ );
1127
+ }, chatId, channelId, options);
1128
+
1129
+ return response.messageSendResult === 'OK';
1130
+ }
1131
+
1132
+ /**
1133
+ * Searches for messages
1134
+ * @param {string} query
1135
+ * @param {Object} [options]
1136
+ * @param {number} [options.page]
1137
+ * @param {number} [options.limit]
1138
+ * @param {string} [options.chatId]
1139
+ * @returns {Promise<Message[]>}
1140
+ */
1141
+ async searchMessages(query, options = {}) {
1142
+ const messages = await this.pupPage.evaluate(async (query, page, count, remote) => {
1143
+ const { messages } = await window.Store.Msg.search(query, page, count, remote);
1144
+ return messages.map(msg => window.WWebJS.getMessageModel(msg));
1145
+ }, query, options.page, options.limit, options.chatId);
1146
+
1147
+ return messages.map(msg => new Message(this, msg));
1148
+ }
1149
+
1150
+ /**
1151
+ * Get all current chat instances
1152
+ * @returns {Promise<Array<Chat>>}
1153
+ */
1154
+ async getChats() {
1155
+ const chats = await this.pupPage.evaluate(async () => {
1156
+ return await window.WWebJS.getChats();
1157
+ });
1158
+
1159
+ return chats.map(chat => ChatFactory.create(this, chat));
1160
+ }
1161
+
1162
+ /**
1163
+ * Gets all cached {@link Channel} instance
1164
+ * @returns {Promise<Array<Channel>>}
1165
+ */
1166
+ async getChannels() {
1167
+ const channels = await this.pupPage.evaluate(async () => {
1168
+ return await window.WWebJS.getChannels();
1169
+ });
1170
+
1171
+ return channels.map((channel) => ChatFactory.create(this, channel));
1172
+ }
1173
+
1174
+ /**
1175
+ * Gets chat or channel instance by ID
1176
+ * @param {string} chatId
1177
+ * @returns {Promise<Chat|Channel>}
1178
+ */
1179
+ async getChatById(chatId) {
1180
+ const chat = await this.pupPage.evaluate(async chatId => {
1181
+ return await window.WWebJS.getChat(chatId);
1182
+ }, chatId);
1183
+ return chat
1184
+ ? ChatFactory.create(this, chat)
1185
+ : undefined;
1186
+ }
1187
+
1188
+ /**
1189
+ * Gets a {@link Channel} instance by invite code
1190
+ * @param {string} inviteCode The code that comes after the 'https://whatsapp.com/channel/'
1191
+ * @returns {Promise<Channel>}
1192
+ */
1193
+ async getChannelByInviteCode(inviteCode) {
1194
+ const channel = await this.pupPage.evaluate(async (inviteCode) => {
1195
+ let channelMetadata;
1196
+ try {
1197
+ channelMetadata = await window.WWebJS.getChannelMetadata(inviteCode);
1198
+ } catch (err) {
1199
+ if (err.name === 'ServerStatusCodeError') return null;
1200
+ throw err;
1201
+ }
1202
+ return await window.WWebJS.getChat(channelMetadata.id);
1203
+ }, inviteCode);
1204
+
1205
+ return channel
1206
+ ? ChatFactory.create(this, channel)
1207
+ : undefined;
1208
+ }
1209
+
1210
+ /**
1211
+ * Get all current contact instances
1212
+ * @returns {Promise<Array<Contact>>}
1213
+ */
1214
+ async getContacts() {
1215
+ let contacts = await this.pupPage.evaluate(() => {
1216
+ return window.WWebJS.getContacts();
1217
+ });
1218
+
1219
+ return contacts.map(contact => ContactFactory.create(this, contact));
1220
+ }
1221
+
1222
+ /**
1223
+ * Get contact instance by ID
1224
+ * @param {string} contactId
1225
+ * @returns {Promise<Contact>}
1226
+ */
1227
+ async getContactById(contactId) {
1228
+ let contact = await this.pupPage.evaluate(contactId => {
1229
+ return window.WWebJS.getContact(contactId);
1230
+ }, contactId);
1231
+
1232
+ return ContactFactory.create(this, contact);
1233
+ }
1234
+
1235
+ /**
1236
+ * Get message by ID
1237
+ * @param {string} messageId
1238
+ * @returns {Promise<Message>}
1239
+ */
1240
+ async getMessageById(messageId) {
1241
+ const msg = await this.pupPage.evaluate(async messageId => {
1242
+ let msg = window.Store.Msg.get(messageId);
1243
+ if(msg) return window.WWebJS.getMessageModel(msg);
1244
+
1245
+ const params = messageId.split('_');
1246
+ if (params.length !== 3 && params.length !== 4) throw new Error('Invalid serialized message id specified');
1247
+
1248
+ let messagesObject = await window.Store.Msg.getMessagesById([messageId]);
1249
+ if (messagesObject && messagesObject.messages.length) msg = messagesObject.messages[0];
1250
+
1251
+ if(msg) return window.WWebJS.getMessageModel(msg);
1252
+ }, messageId);
1253
+
1254
+ if(msg) return new Message(this, msg);
1255
+ return null;
1256
+ }
1257
+
1258
+ /**
1259
+ * Gets instances of all pinned messages in a chat
1260
+ * @param {string} chatId The chat ID
1261
+ * @returns {Promise<Array<Message>>}
1262
+ */
1263
+ async getPinnedMessages(chatId) {
1264
+ const pinnedMsgs = await this.pupPage.evaluate(async (chatId) => {
1265
+ const chatWid = window.Store.WidFactory.createWid(chatId);
1266
+ const chat = window.Store.Chat.get(chatWid) ?? await window.Store.Chat.find(chatWid);
1267
+ if (!chat) return [];
1268
+
1269
+ const msgs = await window.Store.PinnedMsgUtils.getTable().equals(['chatId'], chatWid.toString());
1270
+
1271
+ const pinnedMsgs = (
1272
+ await Promise.all(
1273
+ msgs.filter(msg => msg.pinType == 1).map(async (msg) => {
1274
+ const res = await window.Store.Msg.getMessagesById([msg.parentMsgKey]);
1275
+ return res?.messages?.[0];
1276
+ })
1277
+ )
1278
+ ).filter(Boolean);
1279
+
1280
+ return !pinnedMsgs.length
1281
+ ? []
1282
+ : await Promise.all(pinnedMsgs.map((msg) => window.WWebJS.getMessageModel(msg)));
1283
+ }, chatId);
1284
+
1285
+ return pinnedMsgs.map((msg) => new Message(this, msg));
1286
+ }
1287
+
1288
+ /**
1289
+ * Returns an object with information about the invite code's group
1290
+ * @param {string} inviteCode
1291
+ * @returns {Promise<object>} Invite information
1292
+ */
1293
+ async getInviteInfo(inviteCode) {
1294
+ return await this.pupPage.evaluate(inviteCode => {
1295
+ return window.Store.GroupInvite.queryGroupInvite(inviteCode);
1296
+ }, inviteCode);
1297
+ }
1298
+
1299
+ /**
1300
+ * Accepts an invitation to join a group
1301
+ * @param {string} inviteCode Invitation code
1302
+ * @returns {Promise<string>} Id of the joined Chat
1303
+ */
1304
+ async acceptInvite(inviteCode) {
1305
+ const res = await this.pupPage.evaluate(async inviteCode => {
1306
+ return await window.Store.GroupInvite.joinGroupViaInvite(inviteCode);
1307
+ }, inviteCode);
1308
+
1309
+ return res.gid._serialized;
1310
+ }
1311
+
1312
+ /**
1313
+ * Accepts a channel admin invitation and promotes the current user to a channel admin
1314
+ * @param {string} channelId The channel ID to accept the admin invitation from
1315
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1316
+ */
1317
+ async acceptChannelAdminInvite(channelId) {
1318
+ return await this.pupPage.evaluate(async (channelId) => {
1319
+ try {
1320
+ await window.Store.ChannelUtils.acceptNewsletterAdminInvite(channelId);
1321
+ return true;
1322
+ } catch (err) {
1323
+ if (err.name === 'ServerStatusCodeError') return false;
1324
+ throw err;
1325
+ }
1326
+ }, channelId);
1327
+ }
1328
+
1329
+ /**
1330
+ * Revokes a channel admin invitation sent to a user by a channel owner
1331
+ * @param {string} channelId The channel ID an invitation belongs to
1332
+ * @param {string} userId The user ID the invitation was sent to
1333
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1334
+ */
1335
+ async revokeChannelAdminInvite(channelId, userId) {
1336
+ return await this.pupPage.evaluate(async (channelId, userId) => {
1337
+ try {
1338
+ const userWid = window.Store.WidFactory.createWid(userId);
1339
+ await window.Store.ChannelUtils.revokeNewsletterAdminInvite(channelId, userWid);
1340
+ return true;
1341
+ } catch (err) {
1342
+ if (err.name === 'ServerStatusCodeError') return false;
1343
+ throw err;
1344
+ }
1345
+ }, channelId, userId);
1346
+ }
1347
+
1348
+ /**
1349
+ * Demotes a channel admin to a regular subscriber (can be used also for self-demotion)
1350
+ * @param {string} channelId The channel ID to demote an admin in
1351
+ * @param {string} userId The user ID to demote
1352
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1353
+ */
1354
+ async demoteChannelAdmin(channelId, userId) {
1355
+ return await this.pupPage.evaluate(async (channelId, userId) => {
1356
+ try {
1357
+ const userWid = window.Store.WidFactory.createWid(userId);
1358
+ await window.Store.ChannelUtils.demoteNewsletterAdmin(channelId, userWid);
1359
+ return true;
1360
+ } catch (err) {
1361
+ if (err.name === 'ServerStatusCodeError') return false;
1362
+ throw err;
1363
+ }
1364
+ }, channelId, userId);
1365
+ }
1366
+
1367
+ /**
1368
+ * Accepts a private invitation to join a group
1369
+ * @param {object} inviteInfo Invite V4 Info
1370
+ * @returns {Promise<Object>}
1371
+ */
1372
+ async acceptGroupV4Invite(inviteInfo) {
1373
+ if (!inviteInfo.inviteCode) throw 'Invalid invite code, try passing the message.inviteV4 object';
1374
+ if (inviteInfo.inviteCodeExp == 0) throw 'Expired invite code';
1375
+ return this.pupPage.evaluate(async inviteInfo => {
1376
+ let { groupId, fromId, inviteCode, inviteCodeExp } = inviteInfo;
1377
+ let userWid = window.Store.WidFactory.createWid(fromId);
1378
+ return await window.Store.GroupInviteV4.joinGroupViaInviteV4(inviteCode, String(inviteCodeExp), groupId, userWid);
1379
+ }, inviteInfo);
1380
+ }
1381
+
1382
+ /**
1383
+ * Sets the current user's status message
1384
+ * @param {string} status New status message
1385
+ */
1386
+ async setStatus(status) {
1387
+ await this.pupPage.evaluate(async status => {
1388
+ return await window.Store.StatusUtils.setMyStatus(status);
1389
+ }, status);
1390
+ }
1391
+
1392
+ /**
1393
+ * Sets the current user's display name.
1394
+ * 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.
1395
+ * @param {string} displayName New display name
1396
+ * @returns {Promise<Boolean>}
1397
+ */
1398
+ async setDisplayName(displayName) {
1399
+ const couldSet = await this.pupPage.evaluate(async displayName => {
1400
+ if(!window.Store.Conn.canSetMyPushname()) return false;
1401
+ await window.Store.Settings.setPushname(displayName);
1402
+ return true;
1403
+ }, displayName);
1404
+
1405
+ return couldSet;
1406
+ }
1407
+
1408
+ /**
1409
+ * Gets the current connection state for the client
1410
+ * @returns {WAState}
1411
+ */
1412
+ async getState() {
1413
+ return await this.pupPage.evaluate(() => {
1414
+ if(!window.Store) return null;
1415
+ return window.Store.AppState.state;
1416
+ });
1417
+ }
1418
+
1419
+ /**
1420
+ * Marks the client as online
1421
+ */
1422
+ async sendPresenceAvailable() {
1423
+ return await this.pupPage.evaluate(() => {
1424
+ return window.Store.PresenceUtils.sendPresenceAvailable();
1425
+ });
1426
+ }
1427
+
1428
+ /**
1429
+ * Marks the client as unavailable
1430
+ */
1431
+ async sendPresenceUnavailable() {
1432
+ return await this.pupPage.evaluate(() => {
1433
+ return window.Store.PresenceUtils.sendPresenceUnavailable();
1434
+ });
1435
+ }
1436
+
1437
+ /**
1438
+ * Enables and returns the archive state of the Chat
1439
+ * @returns {boolean}
1440
+ */
1441
+ async archiveChat(chatId) {
1442
+ return await this.pupPage.evaluate(async chatId => {
1443
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1444
+ await window.Store.Cmd.archiveChat(chat, true);
1445
+ return true;
1446
+ }, chatId);
1447
+ }
1448
+
1449
+ /**
1450
+ * Changes and returns the archive state of the Chat
1451
+ * @returns {boolean}
1452
+ */
1453
+ async unarchiveChat(chatId) {
1454
+ return await this.pupPage.evaluate(async chatId => {
1455
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1456
+ await window.Store.Cmd.archiveChat(chat, false);
1457
+ return false;
1458
+ }, chatId);
1459
+ }
1460
+
1461
+ /**
1462
+ * Pins the Chat
1463
+ * @returns {Promise<boolean>} New pin state. Could be false if the max number of pinned chats was reached.
1464
+ */
1465
+ async pinChat(chatId) {
1466
+ return this.pupPage.evaluate(async chatId => {
1467
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1468
+ if (chat.pin) {
1469
+ return true;
1470
+ }
1471
+ const MAX_PIN_COUNT = 3;
1472
+ const chatModels = window.Store.Chat.getModelsArray();
1473
+ if (chatModels.length > MAX_PIN_COUNT) {
1474
+ let maxPinned = chatModels[MAX_PIN_COUNT - 1].pin;
1475
+ if (maxPinned) {
1476
+ return false;
1477
+ }
1478
+ }
1479
+ await window.Store.Cmd.pinChat(chat, true);
1480
+ return true;
1481
+ }, chatId);
1482
+ }
1483
+
1484
+ /**
1485
+ * Unpins the Chat
1486
+ * @returns {Promise<boolean>} New pin state
1487
+ */
1488
+ async unpinChat(chatId) {
1489
+ return this.pupPage.evaluate(async chatId => {
1490
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1491
+ if (!chat.pin) {
1492
+ return false;
1493
+ }
1494
+ await window.Store.Cmd.pinChat(chat, false);
1495
+ return false;
1496
+ }, chatId);
1497
+ }
1498
+
1499
+ /**
1500
+ * Mutes this chat forever, unless a date is specified
1501
+ * @param {string} chatId ID of the chat that will be muted
1502
+ * @param {?Date} unmuteDate Date when the chat will be unmuted, don't provide a value to mute forever
1503
+ * @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
1504
+ */
1505
+ async muteChat(chatId, unmuteDate) {
1506
+ unmuteDate = unmuteDate ? Math.floor(unmuteDate.getTime() / 1000) : -1;
1507
+ return this._muteUnmuteChat(chatId, 'MUTE', unmuteDate);
1508
+ }
1509
+
1510
+ /**
1511
+ * Unmutes the Chat
1512
+ * @param {string} chatId ID of the chat that will be unmuted
1513
+ * @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
1514
+ */
1515
+ async unmuteChat(chatId) {
1516
+ return this._muteUnmuteChat(chatId, 'UNMUTE');
1517
+ }
1518
+
1519
+ /**
1520
+ * Internal method to mute or unmute the chat
1521
+ * @param {string} chatId ID of the chat that will be muted/unmuted
1522
+ * @param {string} action The action: 'MUTE' or 'UNMUTE'
1523
+ * @param {number} unmuteDateTs Timestamp at which the chat will be unmuted
1524
+ * @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
1525
+ */
1526
+ async _muteUnmuteChat (chatId, action, unmuteDateTs) {
1527
+ return this.pupPage.evaluate(async (chatId, action, unmuteDateTs) => {
1528
+ const chat = window.Store.Chat.get(chatId) ?? await window.Store.Chat.find(chatId);
1529
+ action === 'MUTE'
1530
+ ? await chat.mute.mute({ expiration: unmuteDateTs, sendDevice: true })
1531
+ : await chat.mute.unmute({ sendDevice: true });
1532
+ return { isMuted: chat.mute.expiration !== 0, muteExpiration: chat.mute.expiration };
1533
+ }, chatId, action, unmuteDateTs || -1);
1534
+ }
1535
+
1536
+ /**
1537
+ * Mark the Chat as unread
1538
+ * @param {string} chatId ID of the chat that will be marked as unread
1539
+ */
1540
+ async markChatUnread(chatId) {
1541
+ await this.pupPage.evaluate(async chatId => {
1542
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1543
+ await window.Store.Cmd.markChatUnread(chat, true);
1544
+ }, chatId);
1545
+ }
1546
+
1547
+ /**
1548
+ * Returns the contact ID's profile picture URL, if privacy settings allow it
1549
+ * @param {string} contactId the whatsapp user's ID
1550
+ * @returns {Promise<string>}
1551
+ */
1552
+ async getProfilePicUrl(contactId) {
1553
+ const profilePic = await this.pupPage.evaluate(async contactId => {
1554
+ try {
1555
+ const chatWid = window.Store.WidFactory.createWid(contactId);
1556
+ return window.compareWwebVersions(window.Debug.VERSION, '<', '2.3000.0')
1557
+ ? await window.Store.ProfilePic.profilePicFind(chatWid)
1558
+ : await window.Store.ProfilePic.requestProfilePicFromServer(chatWid);
1559
+ } catch (err) {
1560
+ if(err.name === 'ServerStatusCodeError') return undefined;
1561
+ throw err;
1562
+ }
1563
+ }, contactId);
1564
+
1565
+ return profilePic ? profilePic.eurl : undefined;
1566
+ }
1567
+
1568
+ /**
1569
+ * Gets the Contact's common groups with you. Returns empty array if you don't have any common group.
1570
+ * @param {string} contactId the whatsapp user's ID (_serialized format)
1571
+ * @returns {Promise<WAWebJS.ChatId[]>}
1572
+ */
1573
+ async getCommonGroups(contactId) {
1574
+ const commonGroups = await this.pupPage.evaluate(async (contactId) => {
1575
+ let contact = window.Store.Contact.get(contactId);
1576
+ if (!contact) {
1577
+ const wid = window.Store.WidFactory.createWid(contactId);
1578
+ const chatConstructor = window.Store.Contact.getModelsArray().find(c=>!c.isGroup).constructor;
1579
+ contact = new chatConstructor({id: wid});
1580
+ }
1581
+
1582
+ if (contact.commonGroups) {
1583
+ return contact.commonGroups.serialize();
1584
+ }
1585
+ const status = await window.Store.findCommonGroups(contact);
1586
+ if (status) {
1587
+ return contact.commonGroups.serialize();
1588
+ }
1589
+ return [];
1590
+ }, contactId);
1591
+ const chats = [];
1592
+ for (const group of commonGroups) {
1593
+ chats.push(group.id);
1594
+ }
1595
+ return chats;
1596
+ }
1597
+
1598
+ /**
1599
+ * Force reset of connection state for the client
1600
+ */
1601
+ async resetState() {
1602
+ await this.pupPage.evaluate(() => {
1603
+ window.Store.AppState.reconnect();
1604
+ });
1605
+ }
1606
+
1607
+ /**
1608
+ * Check if a given ID is registered in whatsapp
1609
+ * @param {string} id the whatsapp user's ID
1610
+ * @returns {Promise<Boolean>}
1611
+ */
1612
+ async isRegisteredUser(id) {
1613
+ return Boolean(await this.getNumberId(id));
1614
+ }
1615
+
1616
+ /**
1617
+ * Get the registered WhatsApp ID for a number.
1618
+ * Will return null if the number is not registered on WhatsApp.
1619
+ * @param {string} number Number or ID ("@c.us" will be automatically appended if not specified)
1620
+ * @returns {Promise<Object|null>}
1621
+ */
1622
+ async getNumberId(number) {
1623
+ if (!number.endsWith('@c.us')) {
1624
+ number += '@c.us';
1625
+ }
1626
+
1627
+ return await this.pupPage.evaluate(async number => {
1628
+ const wid = window.Store.WidFactory.createWid(number);
1629
+ const result = await window.Store.QueryExist(wid);
1630
+ if (!result || result.wid === undefined) return null;
1631
+ return result.wid;
1632
+ }, number);
1633
+ }
1634
+
1635
+ /**
1636
+ * Get the formatted number of a WhatsApp ID.
1637
+ * @param {string} number Number or ID
1638
+ * @returns {Promise<string>}
1639
+ */
1640
+ async getFormattedNumber(number) {
1641
+ if (!number.endsWith('@s.whatsapp.net')) number = number.replace('c.us', 's.whatsapp.net');
1642
+ if (!number.includes('@s.whatsapp.net')) number = `${number}@s.whatsapp.net`;
1643
+
1644
+ return await this.pupPage.evaluate(async numberId => {
1645
+ return window.Store.NumberInfo.formattedPhoneNumber(numberId);
1646
+ }, number);
1647
+ }
1648
+
1649
+ /**
1650
+ * Get the country code of a WhatsApp ID.
1651
+ * @param {string} number Number or ID
1652
+ * @returns {Promise<string>}
1653
+ */
1654
+ async getCountryCode(number) {
1655
+ number = number.replace(' ', '').replace('+', '').replace('@c.us', '');
1656
+
1657
+ return await this.pupPage.evaluate(async numberId => {
1658
+ return window.Store.NumberInfo.findCC(numberId);
1659
+ }, number);
1660
+ }
1661
+
1662
+ /**
1663
+ * An object that represents the result for a participant added to a group
1664
+ * @typedef {Object} ParticipantResult
1665
+ * @property {number} statusCode The status code of the result
1666
+ * @property {string} message The result message
1667
+ * @property {boolean} isGroupCreator Indicates if the participant is a group creator
1668
+ * @property {boolean} isInviteV4Sent Indicates if the inviteV4 was sent to the participant
1669
+ */
1670
+
1671
+ /**
1672
+ * An object that handles the result for {@link createGroup} method
1673
+ * @typedef {Object} CreateGroupResult
1674
+ * @property {string} title A group title
1675
+ * @property {Object} gid An object that handles the newly created group ID
1676
+ * @property {string} gid.server
1677
+ * @property {string} gid.user
1678
+ * @property {string} gid._serialized
1679
+ * @property {Object.<string, ParticipantResult>} participants An object that handles the result value for each added to the group participant
1680
+ */
1681
+
1682
+ /**
1683
+ * An object that handles options for group creation
1684
+ * @typedef {Object} CreateGroupOptions
1685
+ * @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)
1686
+ * @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)
1687
+ * @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)
1688
+ * @property {string} [comment = ''] The comment to be added to an inviteV4 (empty string by default)
1689
+ * @property {boolean} [memberAddMode = false] If true, only admins can add members to the group (false by default)
1690
+ * @property {boolean} [membershipApprovalMode = false] If true, group admins will be required to approve anyone who wishes to join the group (false by default)
1691
+ * @property {boolean} [isRestrict = true] If true, only admins can change group group info (true by default)
1692
+ * @property {boolean} [isAnnounce = false] If true, only admins can send messages (false by default)
1693
+ */
1694
+
1695
+ /**
1696
+ * Creates a new group
1697
+ * @param {string} title Group title
1698
+ * @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
1699
+ * @param {CreateGroupOptions} options An object that handles options for group creation
1700
+ * @returns {Promise<CreateGroupResult|string>} Object with resulting data or an error message as a string
1701
+ */
1702
+ async createGroup(title, participants = [], options = {}) {
1703
+ !Array.isArray(participants) && (participants = [participants]);
1704
+ participants.map(p => (p instanceof Contact) ? p.id._serialized : p);
1705
+
1706
+ return await this.pupPage.evaluate(async (title, participants, options) => {
1707
+ const {
1708
+ messageTimer = 0,
1709
+ parentGroupId,
1710
+ autoSendInviteV4 = true,
1711
+ comment = '',
1712
+ } = options;
1713
+ const participantData = {}, participantWids = [], failedParticipants = [];
1714
+ let createGroupResult, parentGroupWid;
1715
+
1716
+ const addParticipantResultCodes = {
1717
+ default: 'An unknown error occupied while adding a participant',
1718
+ 200: 'The participant was added successfully',
1719
+ 403: 'The participant can be added by sending private invitation only',
1720
+ 404: 'The phone number is not registered on WhatsApp'
1721
+ };
1722
+
1723
+ for (const participant of participants) {
1724
+ const pWid = window.Store.WidFactory.createWid(participant);
1725
+ if ((await window.Store.QueryExist(pWid))?.wid) {
1726
+ participantWids.push({ phoneNumber: pWid });
1727
+ }
1728
+ else failedParticipants.push(participant);
1729
+ }
1730
+
1731
+ parentGroupId && (parentGroupWid = window.Store.WidFactory.createWid(parentGroupId));
1732
+
1733
+ try {
1734
+ createGroupResult = await window.Store.GroupUtils.createGroup(
1735
+ {
1736
+ 'addressingModeOverride': 'lid',
1737
+ 'memberAddMode': options.memberAddMode ?? false,
1738
+ 'membershipApprovalMode': options.membershipApprovalMode ?? false,
1739
+ 'announce': options.announce ?? false,
1740
+ 'restrict': options.isRestrict !== undefined ? !options.isRestrict : false,
1741
+ 'ephemeralDuration': messageTimer,
1742
+ 'parentGroupId': parentGroupWid,
1743
+ 'title': title,
1744
+ },
1745
+ participantWids
1746
+ );
1747
+ } catch (err) {
1748
+ return 'CreateGroupError: An unknown error occupied while creating a group';
1749
+ }
1750
+
1751
+ for (const participant of createGroupResult.participants) {
1752
+ let isInviteV4Sent = false;
1753
+ participant.wid.server == 'lid' && (participant.wid = window.Store.LidUtils.getPhoneNumber(participant.wid));
1754
+ const participantId = participant.wid._serialized;
1755
+ const statusCode = participant.error || 200;
1756
+
1757
+ if (autoSendInviteV4 && statusCode === 403) {
1758
+ window.Store.Contact.gadd(participant.wid, { silent: true });
1759
+ const addParticipantResult = await window.Store.GroupInviteV4.sendGroupInviteMessage(
1760
+ window.Store.Chat.get(participant.wid) || await window.Store.Chat.find(participant.wid),
1761
+ createGroupResult.wid._serialized,
1762
+ createGroupResult.subject,
1763
+ participant.invite_code,
1764
+ participant.invite_code_exp,
1765
+ comment,
1766
+ await window.WWebJS.getProfilePicThumbToBase64(createGroupResult.wid)
1767
+ );
1768
+ isInviteV4Sent = addParticipantResult.messageSendResult === 'OK';
1769
+ }
1770
+
1771
+ participantData[participantId] = {
1772
+ statusCode: statusCode,
1773
+ message: addParticipantResultCodes[statusCode] || addParticipantResultCodes.default,
1774
+ isGroupCreator: participant.type === 'superadmin',
1775
+ isInviteV4Sent: isInviteV4Sent
1776
+ };
1777
+ }
1778
+
1779
+ for (const f of failedParticipants) {
1780
+ participantData[f] = {
1781
+ statusCode: 404,
1782
+ message: addParticipantResultCodes[404],
1783
+ isGroupCreator: false,
1784
+ isInviteV4Sent: false
1785
+ };
1786
+ }
1787
+
1788
+ return { title: title, gid: createGroupResult.wid, participants: participantData };
1789
+ }, title, participants, options);
1790
+ }
1791
+
1792
+ /**
1793
+ * An object that handles the result for {@link createChannel} method
1794
+ * @typedef {Object} CreateChannelResult
1795
+ * @property {string} title A channel title
1796
+ * @property {ChatId} nid An object that handels the newly created channel ID
1797
+ * @property {string} nid.server 'newsletter'
1798
+ * @property {string} nid.user 'XXXXXXXXXX'
1799
+ * @property {string} nid._serialized 'XXXXXXXXXX@newsletter'
1800
+ * @property {string} inviteLink The channel invite link, starts with 'https://whatsapp.com/channel/'
1801
+ * @property {number} createdAtTs The timestamp the channel was created at
1802
+ */
1803
+
1804
+ /**
1805
+ * Options for the channel creation
1806
+ * @typedef {Object} CreateChannelOptions
1807
+ * @property {?string} description The channel description
1808
+ * @property {?MessageMedia} picture The channel profile picture
1809
+ */
1810
+
1811
+ /**
1812
+ * Creates a new channel
1813
+ * @param {string} title The channel name
1814
+ * @param {CreateChannelOptions} options
1815
+ * @returns {Promise<CreateChannelResult|string>} Returns an object that handles the result for the channel creation or an error message as a string
1816
+ */
1817
+ async createChannel(title, options = {}) {
1818
+ return await this.pupPage.evaluate(async (title, options) => {
1819
+ let response, { description = null, picture = null } = options;
1820
+
1821
+ if (!window.Store.ChannelUtils.isNewsletterCreationEnabled()) {
1822
+ return 'CreateChannelError: A channel creation is not enabled';
1823
+ }
1824
+
1825
+ if (picture) {
1826
+ picture = await window.WWebJS.cropAndResizeImage(picture, {
1827
+ asDataUrl: true,
1828
+ mimetype: 'image/jpeg',
1829
+ size: 640,
1830
+ quality: 1
1831
+ });
1832
+ }
1833
+
1834
+ try {
1835
+ response = await window.Store.ChannelUtils.createNewsletterQuery({
1836
+ name: title,
1837
+ description: description,
1838
+ picture: picture,
1839
+ });
1840
+ } catch (err) {
1841
+ if (err.name === 'ServerStatusCodeError') {
1842
+ return 'CreateChannelError: An error occupied while creating a channel';
1843
+ }
1844
+ throw err;
1845
+ }
1846
+
1847
+ return {
1848
+ title: title,
1849
+ nid: window.Store.JidToWid.newsletterJidToWid(response.idJid),
1850
+ inviteLink: `https://whatsapp.com/channel/${response.newsletterInviteLinkMetadataMixin.inviteCode}`,
1851
+ createdAtTs: response.newsletterCreationTimeMetadataMixin.creationTimeValue
1852
+ };
1853
+ }, title, options);
1854
+ }
1855
+
1856
+ /**
1857
+ * Subscribe to channel
1858
+ * @param {string} channelId The channel ID
1859
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1860
+ */
1861
+ async subscribeToChannel(channelId) {
1862
+ return await this.pupPage.evaluate(async (channelId) => {
1863
+ return await window.WWebJS.subscribeToUnsubscribeFromChannel(channelId, 'Subscribe');
1864
+ }, channelId);
1865
+ }
1866
+
1867
+ /**
1868
+ * Options for unsubscribe from a channel
1869
+ * @typedef {Object} UnsubscribeOptions
1870
+ * @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
1871
+ */
1872
+
1873
+ /**
1874
+ * Unsubscribe from channel
1875
+ * @param {string} channelId The channel ID
1876
+ * @param {UnsubscribeOptions} options
1877
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1878
+ */
1879
+ async unsubscribeFromChannel(channelId, options) {
1880
+ return await this.pupPage.evaluate(async (channelId, options) => {
1881
+ return await window.WWebJS.subscribeToUnsubscribeFromChannel(channelId, 'Unsubscribe', options);
1882
+ }, channelId, options);
1883
+ }
1884
+
1885
+ /**
1886
+ * Options for transferring a channel ownership to another user
1887
+ * @typedef {Object} TransferChannelOwnershipOptions
1888
+ * @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.
1889
+ */
1890
+
1891
+ /**
1892
+ * Transfers a channel ownership to another user.
1893
+ * Note: the user you are transferring the channel ownership to must be a channel admin.
1894
+ * @param {string} channelId
1895
+ * @param {string} newOwnerId
1896
+ * @param {TransferChannelOwnershipOptions} options
1897
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1898
+ */
1899
+ async transferChannelOwnership(channelId, newOwnerId, options = {}) {
1900
+ return await this.pupPage.evaluate(async (channelId, newOwnerId, options) => {
1901
+ const channel = await window.WWebJS.getChat(channelId, { getAsModel: false });
1902
+ const newOwner = window.Store.Contact.get(newOwnerId) || (await window.Store.Contact.find(newOwnerId));
1903
+ if (!channel.newsletterMetadata) {
1904
+ await window.Store.NewsletterMetadataCollection.update(channel.id);
1905
+ }
1906
+
1907
+ try {
1908
+ await window.Store.ChannelUtils.changeNewsletterOwnerAction(channel, newOwner);
1909
+
1910
+ if (options.shouldDismissSelfAsAdmin) {
1911
+ const meContact = window.Store.ContactCollection.getMeContact();
1912
+ meContact && (await window.Store.ChannelUtils.demoteNewsletterAdminAction(channel, meContact));
1913
+ }
1914
+ } catch (error) {
1915
+ return false;
1916
+ }
1917
+
1918
+ return true;
1919
+ }, channelId, newOwnerId, options);
1920
+ }
1921
+
1922
+ /**
1923
+ * Searches for channels based on search criteria, there are some notes:
1924
+ * 1. The method finds only channels you are not subscribed to currently
1925
+ * 2. If you have never been subscribed to a found channel
1926
+ * or you have unsubscribed from it with {@link UnsubscribeOptions.deleteLocalModels} set to 'true',
1927
+ * the lastMessage property of a found channel will be 'null'
1928
+ *
1929
+ * @param {Object} searchOptions Search options
1930
+ * @param {string} [searchOptions.searchText = ''] Text to search
1931
+ * @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
1932
+ * @param {boolean} [searchOptions.skipSubscribedNewsletters = false] If true, channels that user is subscribed to won't appear in found channels
1933
+ * @param {number} [searchOptions.view = 0] View type, makes sense only when the searchText is empty. Valid values to provide are:
1934
+ * 0 for RECOMMENDED channels
1935
+ * 1 for TRENDING channels
1936
+ * 2 for POPULAR channels
1937
+ * 3 for NEW channels
1938
+ * @param {number} [searchOptions.limit = 50] The limit of found channels to be appear in the returnig result
1939
+ * @returns {Promise<Array<Channel>>} Returns an array of Channel objects or an empty array if no channels were found
1940
+ */
1941
+ async searchChannels(searchOptions = {}) {
1942
+ return await this.pupPage.evaluate(async ({
1943
+ searchText = '',
1944
+ countryCodes = [window.Store.ChannelUtils.currentRegion],
1945
+ skipSubscribedNewsletters = false,
1946
+ view = 0,
1947
+ limit = 50
1948
+ }) => {
1949
+ searchText = searchText.trim();
1950
+ const currentRegion = window.Store.ChannelUtils.currentRegion;
1951
+ if (![0, 1, 2, 3].includes(view)) view = 0;
1952
+
1953
+ countryCodes = countryCodes.length === 1 && countryCodes[0] === currentRegion
1954
+ ? countryCodes
1955
+ : countryCodes.filter((code) => Object.keys(window.Store.ChannelUtils.countryCodesIso).includes(code));
1956
+
1957
+ const viewTypeMapping = {
1958
+ 0: 'RECOMMENDED',
1959
+ 1: 'TRENDING',
1960
+ 2: 'POPULAR',
1961
+ 3: 'NEW'
1962
+ };
1963
+
1964
+ searchOptions = {
1965
+ searchText: searchText,
1966
+ countryCodes: countryCodes,
1967
+ skipSubscribedNewsletters: skipSubscribedNewsletters,
1968
+ view: viewTypeMapping[view],
1969
+ categories: [],
1970
+ cursorToken: ''
1971
+ };
1972
+
1973
+ const originalFunction = window.Store.ChannelUtils.getNewsletterDirectoryPageSize;
1974
+ limit !== 50 && (window.Store.ChannelUtils.getNewsletterDirectoryPageSize = () => limit);
1975
+
1976
+ const channels = (await window.Store.ChannelUtils.fetchNewsletterDirectories(searchOptions)).newsletters;
1977
+
1978
+ limit !== 50 && (window.Store.ChannelUtils.getNewsletterDirectoryPageSize = originalFunction);
1979
+
1980
+ return channels
1981
+ ? await Promise.all(channels.map((channel) => window.WWebJS.getChatModel(channel, { isChannel: true })))
1982
+ : [];
1983
+ }, searchOptions);
1984
+ }
1985
+
1986
+ /**
1987
+ * Deletes the channel you created
1988
+ * @param {string} channelId The ID of a channel to delete
1989
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1990
+ */
1991
+ async deleteChannel(channelId) {
1992
+ return await this.client.pupPage.evaluate(async (channelId) => {
1993
+ const channel = await window.WWebJS.getChat(channelId, { getAsModel: false });
1994
+ if (!channel) return false;
1995
+ try {
1996
+ await window.Store.ChannelUtils.deleteNewsletterAction(channel);
1997
+ return true;
1998
+ } catch (err) {
1999
+ if (err.name === 'ServerStatusCodeError') return false;
2000
+ throw err;
2001
+ }
2002
+ }, channelId);
2003
+ }
2004
+
2005
+ /**
2006
+ * Get all current Labels
2007
+ * @returns {Promise<Array<Label>>}
2008
+ */
2009
+ async getLabels() {
2010
+ const labels = await this.pupPage.evaluate(async () => {
2011
+ return window.WWebJS.getLabels();
2012
+ });
2013
+
2014
+ return labels.map(data => new Label(this, data));
2015
+ }
2016
+
2017
+ /**
2018
+ * Get all current Broadcast
2019
+ * @returns {Promise<Array<Broadcast>>}
2020
+ */
2021
+ async getBroadcasts() {
2022
+ const broadcasts = await this.pupPage.evaluate(async () => {
2023
+ return window.WWebJS.getAllStatuses();
2024
+ });
2025
+ return broadcasts.map(data => new Broadcast(this, data));
2026
+ }
2027
+
2028
+ /**
2029
+ * Get broadcast instance by current user ID
2030
+ * @param {string} contactId
2031
+ * @returns {Promise<Broadcast>}
2032
+ */
2033
+ async getBroadcastById(contactId) {
2034
+ const broadcast = await this.pupPage.evaluate(async (userId) => {
2035
+ let status;
2036
+ try {
2037
+ status = window.Store.Status.get(userId);
2038
+ if (!status) {
2039
+ status = await window.Store.Status.find(userId);
2040
+ }
2041
+ } catch {
2042
+ status = null;
2043
+ }
2044
+
2045
+ if (status) return window.WWebJS.getStatusModel(status);
2046
+ }, contactId);
2047
+ return new Broadcast(this, broadcast);
2048
+ }
2049
+
2050
+ /**
2051
+ * Revoke current own status messages
2052
+ * @param {string} messageId
2053
+ * @returns {Promise<void>}
2054
+ */
2055
+ async revokeStatusMessage(messageId) {
2056
+ return await this.pupPage.evaluate(async (msgId) => {
2057
+ const status = window.Store.Status.getMyStatus();
2058
+ if (!status) return;
2059
+
2060
+ const msg =
2061
+ window.Store.Msg.get(msgId) || (await window.Store.Msg.getMessagesById([msgId]))?.messages?.[0];
2062
+ if (!msg) return;
2063
+
2064
+ if (!msg.id.fromMe || !msg.id.remote.isStatus())
2065
+ throw 'Invalid usage! Can only revoke the message its from own status broadcast';
2066
+
2067
+ return await window.Store.StatusUtils.sendStatusRevokeMsgAction(status, msg);
2068
+ }, messageId);
2069
+ }
2070
+
2071
+ /**
2072
+ * Get Label instance by ID
2073
+ * @param {string} labelId
2074
+ * @returns {Promise<Label>}
2075
+ */
2076
+ async getLabelById(labelId) {
2077
+ const label = await this.pupPage.evaluate(async (labelId) => {
2078
+ return window.WWebJS.getLabel(labelId);
2079
+ }, labelId);
2080
+
2081
+ return new Label(this, label);
2082
+ }
2083
+
2084
+ /**
2085
+ * Get all Labels assigned to a chat
2086
+ * @param {string} chatId
2087
+ * @returns {Promise<Array<Label>>}
2088
+ */
2089
+ async getChatLabels(chatId) {
2090
+ const labels = await this.pupPage.evaluate(async (chatId) => {
2091
+ return window.WWebJS.getChatLabels(chatId);
2092
+ }, chatId);
2093
+
2094
+ return labels.map(data => new Label(this, data));
2095
+ }
2096
+
2097
+ /**
2098
+ * Get all Chats for a specific Label
2099
+ * @param {string} labelId
2100
+ * @returns {Promise<Array<Chat>>}
2101
+ */
2102
+ async getChatsByLabelId(labelId) {
2103
+ const chatIds = await this.pupPage.evaluate(async (labelId) => {
2104
+ const label = window.Store.Label.get(labelId);
2105
+ const labelItems = label.labelItemCollection.getModelsArray();
2106
+ return labelItems.reduce((result, item) => {
2107
+ if (item.parentType === 'Chat') {
2108
+ result.push(item.parentId);
2109
+ }
2110
+ return result;
2111
+ }, []);
2112
+ }, labelId);
2113
+
2114
+ return Promise.all(chatIds.map(id => this.getChatById(id)));
2115
+ }
2116
+
2117
+ /**
2118
+ * Gets all blocked contacts by host account
2119
+ * @returns {Promise<Array<Contact>>}
2120
+ */
2121
+ async getBlockedContacts() {
2122
+ const blockedContacts = await this.pupPage.evaluate(() => {
2123
+ let chatIds = window.Store.Blocklist.getModelsArray().map(a => a.id._serialized);
2124
+ return Promise.all(chatIds.map(id => window.WWebJS.getContact(id)));
2125
+ });
2126
+
2127
+ return blockedContacts.map(contact => ContactFactory.create(this.client, contact));
2128
+ }
2129
+
2130
+ /**
2131
+ * Sets the current user's profile picture.
2132
+ * @param {MessageMedia} media
2133
+ * @returns {Promise<boolean>} Returns true if the picture was properly updated.
2134
+ */
2135
+ async setProfilePicture(media) {
2136
+ const success = await this.pupPage.evaluate((chatid, media) => {
2137
+ return window.WWebJS.setPicture(chatid, media);
2138
+ }, this.info.wid._serialized, media);
2139
+
2140
+ return success;
2141
+ }
2142
+
2143
+ /**
2144
+ * Deletes the current user's profile picture.
2145
+ * @returns {Promise<boolean>} Returns true if the picture was properly deleted.
2146
+ */
2147
+ async deleteProfilePicture() {
2148
+ const success = await this.pupPage.evaluate((chatid) => {
2149
+ return window.WWebJS.deletePicture(chatid);
2150
+ }, this.info.wid._serialized);
2151
+
2152
+ return success;
2153
+ }
2154
+
2155
+ /**
2156
+ * Change labels in chats
2157
+ * @param {Array<number|string>} labelIds
2158
+ * @param {Array<string>} chatIds
2159
+ * @returns {Promise<void>}
2160
+ */
2161
+ async addOrRemoveLabels(labelIds, chatIds) {
2162
+
2163
+ return this.pupPage.evaluate(async (labelIds, chatIds) => {
2164
+ if (['smba', 'smbi'].indexOf(window.Store.Conn.platform) === -1) {
2165
+ throw '[LT01] Only Whatsapp business';
2166
+ }
2167
+ const labels = window.WWebJS.getLabels().filter(e => labelIds.find(l => l == e.id) !== undefined);
2168
+ const chats = window.Store.Chat.filter(e => chatIds.includes(e.id._serialized));
2169
+
2170
+ let actions = labels.map(label => ({id: label.id, type: 'add'}));
2171
+
2172
+ chats.forEach(chat => {
2173
+ (chat.labels || []).forEach(n => {
2174
+ if (!actions.find(e => e.id == n)) {
2175
+ actions.push({id: n, type: 'remove'});
2176
+ }
2177
+ });
2178
+ });
2179
+
2180
+ return await window.Store.Label.addOrRemoveLabels(actions, chats);
2181
+ }, labelIds, chatIds);
2182
+ }
2183
+
2184
+ /**
2185
+ * An object that handles the information about the group membership request
2186
+ * @typedef {Object} GroupMembershipRequest
2187
+ * @property {Object} id The wid of a user who requests to enter the group
2188
+ * @property {Object} addedBy The wid of a user who created that request
2189
+ * @property {Object|null} parentGroupId The wid of a community parent group to which the current group is linked
2190
+ * @property {string} requestMethod The method used to create the request: NonAdminAdd/InviteLink/LinkedGroupJoin
2191
+ * @property {number} t The timestamp the request was created at
2192
+ */
2193
+
2194
+ /**
2195
+ * Gets an array of membership requests
2196
+ * @param {string} groupId The ID of a group to get membership requests for
2197
+ * @returns {Promise<Array<GroupMembershipRequest>>} An array of membership requests
2198
+ */
2199
+ async getGroupMembershipRequests(groupId) {
2200
+ return await this.pupPage.evaluate(async (groupId) => {
2201
+ const groupWid = window.Store.WidFactory.createWid(groupId);
2202
+ return await window.Store.MembershipRequestUtils.getMembershipApprovalRequests(groupWid);
2203
+ }, groupId);
2204
+ }
2205
+
2206
+ /**
2207
+ * An object that handles the result for membership request action
2208
+ * @typedef {Object} MembershipRequestActionResult
2209
+ * @property {string} requesterId User ID whos membership request was approved/rejected
2210
+ * @property {number|undefined} error An error code that occurred during the operation for the participant
2211
+ * @property {string} message A message with a result of membership request action
2212
+ */
2213
+
2214
+ /**
2215
+ * An object that handles options for {@link approveGroupMembershipRequests} and {@link rejectGroupMembershipRequests} methods
2216
+ * @typedef {Object} MembershipRequestActionOptions
2217
+ * @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
2218
+ * @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]
2219
+ */
2220
+
2221
+ /**
2222
+ * Approves membership requests if any
2223
+ * @param {string} groupId The group ID to get the membership request for
2224
+ * @param {MembershipRequestActionOptions} options Options for performing a membership request action
2225
+ * @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
2226
+ */
2227
+ async approveGroupMembershipRequests(groupId, options = {}) {
2228
+ return await this.pupPage.evaluate(async (groupId, options) => {
2229
+ const { requesterIds = null, sleep = [250, 500] } = options;
2230
+ return await window.WWebJS.membershipRequestAction(groupId, 'Approve', requesterIds, sleep);
2231
+ }, groupId, options);
2232
+ }
2233
+
2234
+ /**
2235
+ * Rejects membership requests if any
2236
+ * @param {string} groupId The group ID to get the membership request for
2237
+ * @param {MembershipRequestActionOptions} options Options for performing a membership request action
2238
+ * @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
2239
+ */
2240
+ async rejectGroupMembershipRequests(groupId, options = {}) {
2241
+ return await this.pupPage.evaluate(async (groupId, options) => {
2242
+ const { requesterIds = null, sleep = [250, 500] } = options;
2243
+ return await window.WWebJS.membershipRequestAction(groupId, 'Reject', requesterIds, sleep);
2244
+ }, groupId, options);
2245
+ }
2246
+
2247
+
2248
+ /**
2249
+ * Setting autoload download audio
2250
+ * @param {boolean} flag true/false
2251
+ */
2252
+ async setAutoDownloadAudio(flag) {
2253
+ await this.pupPage.evaluate(async flag => {
2254
+ const autoDownload = window.Store.Settings.getAutoDownloadAudio();
2255
+ if (autoDownload === flag) {
2256
+ return flag;
2257
+ }
2258
+ await window.Store.Settings.setAutoDownloadAudio(flag);
2259
+ return flag;
2260
+ }, flag);
2261
+ }
2262
+
2263
+ /**
2264
+ * Setting autoload download documents
2265
+ * @param {boolean} flag true/false
2266
+ */
2267
+ async setAutoDownloadDocuments(flag) {
2268
+ await this.pupPage.evaluate(async flag => {
2269
+ const autoDownload = window.Store.Settings.getAutoDownloadDocuments();
2270
+ if (autoDownload === flag) {
2271
+ return flag;
2272
+ }
2273
+ await window.Store.Settings.setAutoDownloadDocuments(flag);
2274
+ return flag;
2275
+ }, flag);
2276
+ }
2277
+
2278
+ /**
2279
+ * Setting autoload download photos
2280
+ * @param {boolean} flag true/false
2281
+ */
2282
+ async setAutoDownloadPhotos(flag) {
2283
+ await this.pupPage.evaluate(async flag => {
2284
+ const autoDownload = window.Store.Settings.getAutoDownloadPhotos();
2285
+ if (autoDownload === flag) {
2286
+ return flag;
2287
+ }
2288
+ await window.Store.Settings.setAutoDownloadPhotos(flag);
2289
+ return flag;
2290
+ }, flag);
2291
+ }
2292
+
2293
+ /**
2294
+ * Setting autoload download videos
2295
+ * @param {boolean} flag true/false
2296
+ */
2297
+ async setAutoDownloadVideos(flag) {
2298
+ await this.pupPage.evaluate(async flag => {
2299
+ const autoDownload = window.Store.Settings.getAutoDownloadVideos();
2300
+ if (autoDownload === flag) {
2301
+ return flag;
2302
+ }
2303
+ await window.Store.Settings.setAutoDownloadVideos(flag);
2304
+ return flag;
2305
+ }, flag);
2306
+ }
2307
+
2308
+ /**
2309
+ * Setting background synchronization.
2310
+ * NOTE: this action will take effect after you restart the client.
2311
+ * @param {boolean} flag true/false
2312
+ * @returns {Promise<boolean>}
2313
+ */
2314
+ async setBackgroundSync(flag) {
2315
+ return await this.pupPage.evaluate(async flag => {
2316
+ const backSync = window.Store.Settings.getGlobalOfflineNotifications();
2317
+ if (backSync === flag) {
2318
+ return flag;
2319
+ }
2320
+ await window.Store.Settings.setGlobalOfflineNotifications(flag);
2321
+ return flag;
2322
+ }, flag);
2323
+ }
2324
+
2325
+ /**
2326
+ * Get user device count by ID
2327
+ * Each WaWeb Connection counts as one device, and the phone (if exists) counts as one
2328
+ * So for a non-enterprise user with one WaWeb connection it should return "2"
2329
+ * @param {string} userId
2330
+ * @returns {Promise<number>}
2331
+ */
2332
+ async getContactDeviceCount(userId) {
2333
+ return await this.pupPage.evaluate(async (userId) => {
2334
+ const devices = await window.Store.DeviceList.getDeviceIds([window.Store.WidFactory.createWid(userId)]);
2335
+ if (devices && devices.length && devices[0] != null && typeof devices[0].devices == 'object') {
2336
+ return devices[0].devices.length;
2337
+ }
2338
+ return 0;
2339
+ }, userId);
2340
+ }
2341
+
2342
+ /**
2343
+ * Sync chat history conversation
2344
+ * @param {string} chatId
2345
+ * @return {Promise<boolean>} True if operation completed successfully, false otherwise.
2346
+ */
2347
+ async syncHistory(chatId) {
2348
+ return await this.pupPage.evaluate(async (chatId) => {
2349
+ const chatWid = window.Store.WidFactory.createWid(chatId);
2350
+ const chat = window.Store.Chat.get(chatWid) ?? (await window.Store.Chat.find(chatWid));
2351
+ if (chat?.endOfHistoryTransferType === 0) {
2352
+ await window.Store.HistorySync.sendPeerDataOperationRequest(3, {
2353
+ chatId: chat.id
2354
+ });
2355
+ return true;
2356
+ }
2357
+ return false;
2358
+ }, chatId);
2359
+ }
2360
+
2361
+ /**
2362
+ * Generates a WhatsApp call link (video call or voice call)
2363
+ * @param {Date} startTime The start time of the call
2364
+ * @param {string} callType The type of a WhatsApp call link to generate, valid values are: `video` | `voice`
2365
+ * @returns {Promise<string>} The WhatsApp call link (https://call.whatsapp.com/video/XxXxXxXxXxXxXx) or an empty string if a generation failed.
2366
+ */
2367
+ async createCallLink(startTime, callType) {
2368
+ if (!['video', 'voice'].includes(callType)) {
2369
+ throw new class CreateCallLinkError extends Error {
2370
+ constructor(m) { super(m); }
2371
+ }('Invalid \'callType\' parameter value is provided. Valid values are: \'voice\' | \'video\'.');
2372
+ }
2373
+
2374
+ startTime = Math.floor(startTime.getTime() / 1000);
2375
+
2376
+ return await this.pupPage.evaluate(async (startTimeTs, callType) => {
2377
+ const response = await window.Store.ScheduledEventMsgUtils.createEventCallLink(startTimeTs, callType);
2378
+ return response ?? '';
2379
+ }, startTime, callType);
2380
+ }
2381
+
2382
+ /**
2383
+ * Sends a response to the scheduled event message, indicating whether a user is going to attend the event or not
2384
+ * @param {number} response The response code to the scheduled event message. Valid values are: `0` for NONE response (removes a previous response) | `1` for GOING | `2` for NOT GOING | `3` for MAYBE going
2385
+ * @param {string} eventMessageId The scheduled event message ID
2386
+ * @returns {Promise<boolean>}
2387
+ */
2388
+ async sendResponseToScheduledEvent(response, eventMessageId) {
2389
+ if (![0, 1, 2, 3].includes(response)) return false;
2390
+
2391
+ return await this.pupPage.evaluate(async (response, msgId) => {
2392
+ const eventMsg = window.Store.Msg.get(msgId) || (await window.Store.Msg.getMessagesById([msgId]))?.messages?.[0];
2393
+ if (!eventMsg) return false;
2394
+
2395
+ await window.Store.ScheduledEventMsgUtils.sendEventResponseMsg(response, eventMsg);
2396
+ return true;
2397
+ }, response, eventMessageId);
2398
+ }
2399
+
2400
+ /**
2401
+ * Save new contact to user's addressbook or edit the existing one
2402
+ * @param {string} phoneNumber The contact's phone number in a format "17182222222", where "1" is a country code
2403
+ * @param {string} firstName
2404
+ * @param {string} lastName
2405
+ * @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
2406
+ * @returns {Promise<void>}
2407
+ */
2408
+ async saveOrEditAddressbookContact(phoneNumber, firstName, lastName, syncToAddressbook = false)
2409
+ {
2410
+ return await this.pupPage.evaluate(async (phoneNumber, firstName, lastName, syncToAddressbook) => {
2411
+ return await window.Store.AddressbookContactUtils.saveContactAction(
2412
+ phoneNumber,
2413
+ phoneNumber,
2414
+ null,
2415
+ null,
2416
+ firstName,
2417
+ lastName,
2418
+ syncToAddressbook
2419
+ );
2420
+ }, phoneNumber, firstName, lastName, syncToAddressbook);
2421
+ }
2422
+
2423
+ /**
2424
+ * Deletes the contact from user's addressbook
2425
+ * @param {string} phoneNumber The contact's phone number in a format "17182222222", where "1" is a country code
2426
+ * @returns {Promise<void>}
2427
+ */
2428
+ async deleteAddressbookContact(phoneNumber)
2429
+ {
2430
+ return await this.pupPage.evaluate(async (phoneNumber) => {
2431
+ return await window.Store.AddressbookContactUtils.deleteContactAction(phoneNumber);
2432
+ }, phoneNumber);
2433
+ }
2434
+
2435
+ /**
2436
+ * Get lid and phone number for multiple users
2437
+ * @param {string[]} userIds - Array of user IDs
2438
+ * @returns {Promise<Array<{ lid: string, pn: string }>>}
2439
+ */
2440
+ async getContactLidAndPhone(userIds) {
2441
+ return await this.pupPage.evaluate(async (userIds) => {
2442
+ if (!Array.isArray(userIds)) userIds = [userIds];
2443
+
2444
+ return await Promise.all(userIds.map(async (userId) => {
2445
+ const { lid, phone } = await window.WWebJS.enforceLidAndPnRetrieval(userId);
2446
+
2447
+ return {
2448
+ lid: lid?._serialized,
2449
+ pn: phone?._serialized
2450
+ };
2451
+ }));
2452
+ }, userIds);
2453
+ }
2454
+
2455
+ /**
2456
+ * Add or edit a customer note
2457
+ * @see https://faq.whatsapp.com/1433099287594476
2458
+ * @param {string} userId The ID of a customer to add a note to
2459
+ * @param {string} note The note to add
2460
+ * @returns {Promise<void>}
2461
+ */
2462
+ async addOrEditCustomerNote(userId, note) {
2463
+ return await this.pupPage.evaluate(async (userId, note) => {
2464
+ if (!window.Store.BusinessGatingUtils.smbNotesV1Enabled()) return;
2465
+
2466
+ return window.Store.CustomerNoteUtils.noteAddAction(
2467
+ 'unstructured',
2468
+ window.Store.WidToJid.widToUserJid(window.Store.WidFactory.createWid(userId)),
2469
+ note
2470
+ );
2471
+ }, userId, note);
2472
+ }
2473
+
2474
+ /**
2475
+ * Get a customer note
2476
+ * @see https://faq.whatsapp.com/1433099287594476
2477
+ * @param {string} userId The ID of a customer to get a note from
2478
+ * @returns {Promise<{
2479
+ * chatId: string,
2480
+ * content: string,
2481
+ * createdAt: number,
2482
+ * id: string,
2483
+ * modifiedAt: number,
2484
+ * type: string
2485
+ * }>}
2486
+ */
2487
+ async getCustomerNote(userId) {
2488
+ return await this.pupPage.evaluate(async (userId) => {
2489
+ if (!window.Store.BusinessGatingUtils.smbNotesV1Enabled()) return null;
2490
+
2491
+ const note = await window.Store.CustomerNoteUtils.retrieveOnlyNoteForChatJid(
2492
+ window.Store.WidToJid.widToUserJid(window.Store.WidFactory.createWid(userId))
2493
+ );
2494
+
2495
+ let serialized = note?.serialize();
2496
+
2497
+ if (!serialized) return null;
2498
+
2499
+ serialized.chatId = window.Store.JidToWid.userJidToUserWid(serialized.chatJid)._serialized;
2500
+ delete serialized.chatJid;
2501
+
2502
+ return serialized;
2503
+ }, userId);
2504
+ }
2505
+
2506
+ /**
2507
+ * Get Poll Votes
2508
+ * @param {string} messageId
2509
+ * @return {Promise<Array<PollVote>>}
2510
+ */
2511
+ async getPollVotes(messageId) {
2512
+ const msg = await this.getMessageById(messageId);
2513
+ if (!msg) return [];
2514
+ if (msg.type != MessageTypes.POLL_CREATION) throw 'Invalid usage! Can only be used with a pollCreation message';
2515
+
2516
+ const pollVotes = await this.pupPage.evaluate( async (msg) => {
2517
+ const msgKey = window.Store.MsgKey.fromString(msg.id._serialized);
2518
+ let pollVotes = await window.Store.PollsVotesSchema.getTable().equals(['parentMsgKey'], msgKey.toString());
2519
+
2520
+ return pollVotes.map(item => {
2521
+ const typedArray = new Uint8Array(item.selectedOptionLocalIds);
2522
+ return {
2523
+ ...item,
2524
+ selectedOptionLocalIds: Array.from(typedArray)
2525
+ };
2526
+ });
2527
+ }, msg);
2528
+
2529
+ return pollVotes.map((pollVote) => new PollVote(this.client, {...pollVote, parentMessage: msg}));
2530
+ }
2531
+ }
2532
+
2533
+ module.exports = Client;