@another-trial/whatsapp-web.js 1.34.1 → 1.35.0-alpha.2

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 +699 -690
  6. package/index.d.ts +2248 -2202
  7. package/index.js +35 -35
  8. package/package.json +59 -59
  9. package/shell.js +36 -36
  10. package/src/Client.js +2447 -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 +208 -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 +780 -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 +257 -233
  49. package/src/util/Injected/Utils.js +1168 -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,2447 @@
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 {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 = (
1229
+ await Promise.all(
1230
+ msgs.filter(msg => msg.pinType == 1).map(async (msg) => {
1231
+ const res = await window.Store.Msg.getMessagesById([msg.parentMsgKey]);
1232
+ return res?.messages?.[0];
1233
+ })
1234
+ )
1235
+ ).filter(Boolean);
1236
+
1237
+ return !pinnedMsgs.length
1238
+ ? []
1239
+ : await Promise.all(pinnedMsgs.map((msg) => window.WWebJS.getMessageModel(msg)));
1240
+ }, chatId);
1241
+
1242
+ return pinnedMsgs.map((msg) => new Message(this, msg));
1243
+ }
1244
+
1245
+ /**
1246
+ * Returns an object with information about the invite code's group
1247
+ * @param {string} inviteCode
1248
+ * @returns {Promise<object>} Invite information
1249
+ */
1250
+ async getInviteInfo(inviteCode) {
1251
+ return await this.pupPage.evaluate(inviteCode => {
1252
+ return window.Store.GroupInvite.queryGroupInvite(inviteCode);
1253
+ }, inviteCode);
1254
+ }
1255
+
1256
+ /**
1257
+ * Accepts an invitation to join a group
1258
+ * @param {string} inviteCode Invitation code
1259
+ * @returns {Promise<string>} Id of the joined Chat
1260
+ */
1261
+ async acceptInvite(inviteCode) {
1262
+ const res = await this.pupPage.evaluate(async inviteCode => {
1263
+ return await window.Store.GroupInvite.joinGroupViaInvite(inviteCode);
1264
+ }, inviteCode);
1265
+
1266
+ return res.gid._serialized;
1267
+ }
1268
+
1269
+ /**
1270
+ * Accepts a channel admin invitation and promotes the current user to a channel admin
1271
+ * @param {string} channelId The channel ID to accept the admin invitation from
1272
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1273
+ */
1274
+ async acceptChannelAdminInvite(channelId) {
1275
+ return await this.pupPage.evaluate(async (channelId) => {
1276
+ try {
1277
+ await window.Store.ChannelUtils.acceptNewsletterAdminInvite(channelId);
1278
+ return true;
1279
+ } catch (err) {
1280
+ if (err.name === 'ServerStatusCodeError') return false;
1281
+ throw err;
1282
+ }
1283
+ }, channelId);
1284
+ }
1285
+
1286
+ /**
1287
+ * Revokes a channel admin invitation sent to a user by a channel owner
1288
+ * @param {string} channelId The channel ID an invitation belongs to
1289
+ * @param {string} userId The user ID the invitation was sent to
1290
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1291
+ */
1292
+ async revokeChannelAdminInvite(channelId, userId) {
1293
+ return await this.pupPage.evaluate(async (channelId, userId) => {
1294
+ try {
1295
+ const userWid = window.Store.WidFactory.createWid(userId);
1296
+ await window.Store.ChannelUtils.revokeNewsletterAdminInvite(channelId, userWid);
1297
+ return true;
1298
+ } catch (err) {
1299
+ if (err.name === 'ServerStatusCodeError') return false;
1300
+ throw err;
1301
+ }
1302
+ }, channelId, userId);
1303
+ }
1304
+
1305
+ /**
1306
+ * Demotes a channel admin to a regular subscriber (can be used also for self-demotion)
1307
+ * @param {string} channelId The channel ID to demote an admin in
1308
+ * @param {string} userId The user ID to demote
1309
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1310
+ */
1311
+ async demoteChannelAdmin(channelId, userId) {
1312
+ return await this.pupPage.evaluate(async (channelId, userId) => {
1313
+ try {
1314
+ const userWid = window.Store.WidFactory.createWid(userId);
1315
+ await window.Store.ChannelUtils.demoteNewsletterAdmin(channelId, userWid);
1316
+ return true;
1317
+ } catch (err) {
1318
+ if (err.name === 'ServerStatusCodeError') return false;
1319
+ throw err;
1320
+ }
1321
+ }, channelId, userId);
1322
+ }
1323
+
1324
+ /**
1325
+ * Accepts a private invitation to join a group
1326
+ * @param {object} inviteInfo Invite V4 Info
1327
+ * @returns {Promise<Object>}
1328
+ */
1329
+ async acceptGroupV4Invite(inviteInfo) {
1330
+ if (!inviteInfo.inviteCode) throw 'Invalid invite code, try passing the message.inviteV4 object';
1331
+ if (inviteInfo.inviteCodeExp == 0) throw 'Expired invite code';
1332
+ return this.pupPage.evaluate(async inviteInfo => {
1333
+ let { groupId, fromId, inviteCode, inviteCodeExp } = inviteInfo;
1334
+ let userWid = window.Store.WidFactory.createWid(fromId);
1335
+ return await window.Store.GroupInviteV4.joinGroupViaInviteV4(inviteCode, String(inviteCodeExp), groupId, userWid);
1336
+ }, inviteInfo);
1337
+ }
1338
+
1339
+ /**
1340
+ * Sets the current user's status message
1341
+ * @param {string} status New status message
1342
+ */
1343
+ async setStatus(status) {
1344
+ await this.pupPage.evaluate(async status => {
1345
+ return await window.Store.StatusUtils.setMyStatus(status);
1346
+ }, status);
1347
+ }
1348
+
1349
+ /**
1350
+ * Sets the current user's display name.
1351
+ * 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.
1352
+ * @param {string} displayName New display name
1353
+ * @returns {Promise<Boolean>}
1354
+ */
1355
+ async setDisplayName(displayName) {
1356
+ const couldSet = await this.pupPage.evaluate(async displayName => {
1357
+ if(!window.Store.Conn.canSetMyPushname()) return false;
1358
+ await window.Store.Settings.setPushname(displayName);
1359
+ return true;
1360
+ }, displayName);
1361
+
1362
+ return couldSet;
1363
+ }
1364
+
1365
+ /**
1366
+ * Gets the current connection state for the client
1367
+ * @returns {WAState}
1368
+ */
1369
+ async getState() {
1370
+ return await this.pupPage.evaluate(() => {
1371
+ if(!window.Store) return null;
1372
+ return window.Store.AppState.state;
1373
+ });
1374
+ }
1375
+
1376
+ /**
1377
+ * Marks the client as online
1378
+ */
1379
+ async sendPresenceAvailable() {
1380
+ return await this.pupPage.evaluate(() => {
1381
+ return window.Store.PresenceUtils.sendPresenceAvailable();
1382
+ });
1383
+ }
1384
+
1385
+ /**
1386
+ * Marks the client as unavailable
1387
+ */
1388
+ async sendPresenceUnavailable() {
1389
+ return await this.pupPage.evaluate(() => {
1390
+ return window.Store.PresenceUtils.sendPresenceUnavailable();
1391
+ });
1392
+ }
1393
+
1394
+ /**
1395
+ * Enables and returns the archive state of the Chat
1396
+ * @returns {boolean}
1397
+ */
1398
+ async archiveChat(chatId) {
1399
+ return await this.pupPage.evaluate(async chatId => {
1400
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1401
+ await window.Store.Cmd.archiveChat(chat, true);
1402
+ return true;
1403
+ }, chatId);
1404
+ }
1405
+
1406
+ /**
1407
+ * Changes and returns the archive state of the Chat
1408
+ * @returns {boolean}
1409
+ */
1410
+ async unarchiveChat(chatId) {
1411
+ return await this.pupPage.evaluate(async chatId => {
1412
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1413
+ await window.Store.Cmd.archiveChat(chat, false);
1414
+ return false;
1415
+ }, chatId);
1416
+ }
1417
+
1418
+ /**
1419
+ * Pins the Chat
1420
+ * @returns {Promise<boolean>} New pin state. Could be false if the max number of pinned chats was reached.
1421
+ */
1422
+ async pinChat(chatId) {
1423
+ return this.pupPage.evaluate(async chatId => {
1424
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1425
+ if (chat.pin) {
1426
+ return true;
1427
+ }
1428
+ const MAX_PIN_COUNT = 3;
1429
+ const chatModels = window.Store.Chat.getModelsArray();
1430
+ if (chatModels.length > MAX_PIN_COUNT) {
1431
+ let maxPinned = chatModels[MAX_PIN_COUNT - 1].pin;
1432
+ if (maxPinned) {
1433
+ return false;
1434
+ }
1435
+ }
1436
+ await window.Store.Cmd.pinChat(chat, true);
1437
+ return true;
1438
+ }, chatId);
1439
+ }
1440
+
1441
+ /**
1442
+ * Unpins the Chat
1443
+ * @returns {Promise<boolean>} New pin state
1444
+ */
1445
+ async unpinChat(chatId) {
1446
+ return this.pupPage.evaluate(async chatId => {
1447
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1448
+ if (!chat.pin) {
1449
+ return false;
1450
+ }
1451
+ await window.Store.Cmd.pinChat(chat, false);
1452
+ return false;
1453
+ }, chatId);
1454
+ }
1455
+
1456
+ /**
1457
+ * Mutes this chat forever, unless a date is specified
1458
+ * @param {string} chatId ID of the chat that will be muted
1459
+ * @param {?Date} unmuteDate Date when the chat will be unmuted, don't provide a value to mute forever
1460
+ * @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
1461
+ */
1462
+ async muteChat(chatId, unmuteDate) {
1463
+ unmuteDate = unmuteDate ? Math.floor(unmuteDate.getTime() / 1000) : -1;
1464
+ return this._muteUnmuteChat(chatId, 'MUTE', unmuteDate);
1465
+ }
1466
+
1467
+ /**
1468
+ * Unmutes the Chat
1469
+ * @param {string} chatId ID of the chat that will be unmuted
1470
+ * @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
1471
+ */
1472
+ async unmuteChat(chatId) {
1473
+ return this._muteUnmuteChat(chatId, 'UNMUTE');
1474
+ }
1475
+
1476
+ /**
1477
+ * Internal method to mute or unmute the chat
1478
+ * @param {string} chatId ID of the chat that will be muted/unmuted
1479
+ * @param {string} action The action: 'MUTE' or 'UNMUTE'
1480
+ * @param {number} unmuteDateTs Timestamp at which the chat will be unmuted
1481
+ * @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
1482
+ */
1483
+ async _muteUnmuteChat (chatId, action, unmuteDateTs) {
1484
+ return this.pupPage.evaluate(async (chatId, action, unmuteDateTs) => {
1485
+ const chat = window.Store.Chat.get(chatId) ?? await window.Store.Chat.find(chatId);
1486
+ action === 'MUTE'
1487
+ ? await chat.mute.mute({ expiration: unmuteDateTs, sendDevice: true })
1488
+ : await chat.mute.unmute({ sendDevice: true });
1489
+ return { isMuted: chat.mute.expiration !== 0, muteExpiration: chat.mute.expiration };
1490
+ }, chatId, action, unmuteDateTs || -1);
1491
+ }
1492
+
1493
+ /**
1494
+ * Mark the Chat as unread
1495
+ * @param {string} chatId ID of the chat that will be marked as unread
1496
+ */
1497
+ async markChatUnread(chatId) {
1498
+ await this.pupPage.evaluate(async chatId => {
1499
+ let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
1500
+ await window.Store.Cmd.markChatUnread(chat, true);
1501
+ }, chatId);
1502
+ }
1503
+
1504
+ /**
1505
+ * Returns the contact ID's profile picture URL, if privacy settings allow it
1506
+ * @param {string} contactId the whatsapp user's ID
1507
+ * @returns {Promise<string>}
1508
+ */
1509
+ async getProfilePicUrl(contactId) {
1510
+ const profilePic = await this.pupPage.evaluate(async contactId => {
1511
+ try {
1512
+ const chatWid = window.Store.WidFactory.createWid(contactId);
1513
+ return window.compareWwebVersions(window.Debug.VERSION, '<', '2.3000.0')
1514
+ ? await window.Store.ProfilePic.profilePicFind(chatWid)
1515
+ : await window.Store.ProfilePic.requestProfilePicFromServer(chatWid);
1516
+ } catch (err) {
1517
+ if(err.name === 'ServerStatusCodeError') return undefined;
1518
+ throw err;
1519
+ }
1520
+ }, contactId);
1521
+
1522
+ return profilePic ? profilePic.eurl : undefined;
1523
+ }
1524
+
1525
+ /**
1526
+ * Gets the Contact's common groups with you. Returns empty array if you don't have any common group.
1527
+ * @param {string} contactId the whatsapp user's ID (_serialized format)
1528
+ * @returns {Promise<WAWebJS.ChatId[]>}
1529
+ */
1530
+ async getCommonGroups(contactId) {
1531
+ const commonGroups = await this.pupPage.evaluate(async (contactId) => {
1532
+ let contact = window.Store.Contact.get(contactId);
1533
+ if (!contact) {
1534
+ const wid = window.Store.WidFactory.createWid(contactId);
1535
+ const chatConstructor = window.Store.Contact.getModelsArray().find(c=>!c.isGroup).constructor;
1536
+ contact = new chatConstructor({id: wid});
1537
+ }
1538
+
1539
+ if (contact.commonGroups) {
1540
+ return contact.commonGroups.serialize();
1541
+ }
1542
+ const status = await window.Store.findCommonGroups(contact);
1543
+ if (status) {
1544
+ return contact.commonGroups.serialize();
1545
+ }
1546
+ return [];
1547
+ }, contactId);
1548
+ const chats = [];
1549
+ for (const group of commonGroups) {
1550
+ chats.push(group.id);
1551
+ }
1552
+ return chats;
1553
+ }
1554
+
1555
+ /**
1556
+ * Force reset of connection state for the client
1557
+ */
1558
+ async resetState() {
1559
+ await this.pupPage.evaluate(() => {
1560
+ window.Store.AppState.reconnect();
1561
+ });
1562
+ }
1563
+
1564
+ /**
1565
+ * Check if a given ID is registered in whatsapp
1566
+ * @param {string} id the whatsapp user's ID
1567
+ * @returns {Promise<Boolean>}
1568
+ */
1569
+ async isRegisteredUser(id) {
1570
+ return Boolean(await this.getNumberId(id));
1571
+ }
1572
+
1573
+ /**
1574
+ * Get the registered WhatsApp ID for a number.
1575
+ * Will return null if the number is not registered on WhatsApp.
1576
+ * @param {string} number Number or ID ("@c.us" will be automatically appended if not specified)
1577
+ * @returns {Promise<Object|null>}
1578
+ */
1579
+ async getNumberId(number) {
1580
+ if (!number.endsWith('@c.us')) {
1581
+ number += '@c.us';
1582
+ }
1583
+
1584
+ return await this.pupPage.evaluate(async number => {
1585
+ const wid = window.Store.WidFactory.createWid(number);
1586
+ const result = await window.Store.QueryExist(wid);
1587
+ if (!result || result.wid === undefined) return null;
1588
+ return result.wid;
1589
+ }, number);
1590
+ }
1591
+
1592
+ /**
1593
+ * Get the formatted number of a WhatsApp ID.
1594
+ * @param {string} number Number or ID
1595
+ * @returns {Promise<string>}
1596
+ */
1597
+ async getFormattedNumber(number) {
1598
+ if (!number.endsWith('@s.whatsapp.net')) number = number.replace('c.us', 's.whatsapp.net');
1599
+ if (!number.includes('@s.whatsapp.net')) number = `${number}@s.whatsapp.net`;
1600
+
1601
+ return await this.pupPage.evaluate(async numberId => {
1602
+ return window.Store.NumberInfo.formattedPhoneNumber(numberId);
1603
+ }, number);
1604
+ }
1605
+
1606
+ /**
1607
+ * Get the country code of a WhatsApp ID.
1608
+ * @param {string} number Number or ID
1609
+ * @returns {Promise<string>}
1610
+ */
1611
+ async getCountryCode(number) {
1612
+ number = number.replace(' ', '').replace('+', '').replace('@c.us', '');
1613
+
1614
+ return await this.pupPage.evaluate(async numberId => {
1615
+ return window.Store.NumberInfo.findCC(numberId);
1616
+ }, number);
1617
+ }
1618
+
1619
+ /**
1620
+ * An object that represents the result for a participant added to a group
1621
+ * @typedef {Object} ParticipantResult
1622
+ * @property {number} statusCode The status code of the result
1623
+ * @property {string} message The result message
1624
+ * @property {boolean} isGroupCreator Indicates if the participant is a group creator
1625
+ * @property {boolean} isInviteV4Sent Indicates if the inviteV4 was sent to the participant
1626
+ */
1627
+
1628
+ /**
1629
+ * An object that handles the result for {@link createGroup} method
1630
+ * @typedef {Object} CreateGroupResult
1631
+ * @property {string} title A group title
1632
+ * @property {Object} gid An object that handles the newly created group ID
1633
+ * @property {string} gid.server
1634
+ * @property {string} gid.user
1635
+ * @property {string} gid._serialized
1636
+ * @property {Object.<string, ParticipantResult>} participants An object that handles the result value for each added to the group participant
1637
+ */
1638
+
1639
+ /**
1640
+ * An object that handles options for group creation
1641
+ * @typedef {Object} CreateGroupOptions
1642
+ * @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)
1643
+ * @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)
1644
+ * @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)
1645
+ * @property {string} [comment = ''] The comment to be added to an inviteV4 (empty string by default)
1646
+ * @property {boolean} [memberAddMode = false] If true, only admins can add members to the group (false by default)
1647
+ * @property {boolean} [membershipApprovalMode = false] If true, group admins will be required to approve anyone who wishes to join the group (false by default)
1648
+ * @property {boolean} [isRestrict = true] If true, only admins can change group group info (true by default)
1649
+ * @property {boolean} [isAnnounce = false] If true, only admins can send messages (false by default)
1650
+ */
1651
+
1652
+ /**
1653
+ * Creates a new group
1654
+ * @param {string} title Group title
1655
+ * @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
1656
+ * @param {CreateGroupOptions} options An object that handles options for group creation
1657
+ * @returns {Promise<CreateGroupResult|string>} Object with resulting data or an error message as a string
1658
+ */
1659
+ async createGroup(title, participants = [], options = {}) {
1660
+ !Array.isArray(participants) && (participants = [participants]);
1661
+ participants.map(p => (p instanceof Contact) ? p.id._serialized : p);
1662
+
1663
+ return await this.pupPage.evaluate(async (title, participants, options) => {
1664
+ const {
1665
+ messageTimer = 0,
1666
+ parentGroupId,
1667
+ autoSendInviteV4 = true,
1668
+ comment = '',
1669
+ } = options;
1670
+ const participantData = {}, participantWids = [], failedParticipants = [];
1671
+ let createGroupResult, parentGroupWid;
1672
+
1673
+ const addParticipantResultCodes = {
1674
+ default: 'An unknown error occupied while adding a participant',
1675
+ 200: 'The participant was added successfully',
1676
+ 403: 'The participant can be added by sending private invitation only',
1677
+ 404: 'The phone number is not registered on WhatsApp'
1678
+ };
1679
+
1680
+ for (const participant of participants) {
1681
+ const pWid = window.Store.WidFactory.createWid(participant);
1682
+ if ((await window.Store.QueryExist(pWid))?.wid) {
1683
+ participantWids.push({ phoneNumber: pWid });
1684
+ }
1685
+ else failedParticipants.push(participant);
1686
+ }
1687
+
1688
+ parentGroupId && (parentGroupWid = window.Store.WidFactory.createWid(parentGroupId));
1689
+
1690
+ try {
1691
+ createGroupResult = await window.Store.GroupUtils.createGroup(
1692
+ {
1693
+ 'addressingModeOverride': 'lid',
1694
+ 'memberAddMode': options.memberAddMode ?? false,
1695
+ 'membershipApprovalMode': options.membershipApprovalMode ?? false,
1696
+ 'announce': options.announce ?? false,
1697
+ 'restrict': options.isRestrict !== undefined ? !options.isRestrict : false,
1698
+ 'ephemeralDuration': messageTimer,
1699
+ 'parentGroupId': parentGroupWid,
1700
+ 'title': title,
1701
+ },
1702
+ participantWids
1703
+ );
1704
+ } catch (err) {
1705
+ return 'CreateGroupError: An unknown error occupied while creating a group';
1706
+ }
1707
+
1708
+ for (const participant of createGroupResult.participants) {
1709
+ let isInviteV4Sent = false;
1710
+ participant.wid.server == 'lid' && (participant.wid = window.Store.LidUtils.getPhoneNumber(participant.wid));
1711
+ const participantId = participant.wid._serialized;
1712
+ const statusCode = participant.error || 200;
1713
+
1714
+ if (autoSendInviteV4 && statusCode === 403) {
1715
+ window.Store.Contact.gadd(participant.wid, { silent: true });
1716
+ const addParticipantResult = await window.Store.GroupInviteV4.sendGroupInviteMessage(
1717
+ window.Store.Chat.get(participant.wid) || await window.Store.Chat.find(participant.wid),
1718
+ createGroupResult.wid._serialized,
1719
+ createGroupResult.subject,
1720
+ participant.invite_code,
1721
+ participant.invite_code_exp,
1722
+ comment,
1723
+ await window.WWebJS.getProfilePicThumbToBase64(createGroupResult.wid)
1724
+ );
1725
+ isInviteV4Sent = addParticipantResult.messageSendResult === 'OK';
1726
+ }
1727
+
1728
+ participantData[participantId] = {
1729
+ statusCode: statusCode,
1730
+ message: addParticipantResultCodes[statusCode] || addParticipantResultCodes.default,
1731
+ isGroupCreator: participant.type === 'superadmin',
1732
+ isInviteV4Sent: isInviteV4Sent
1733
+ };
1734
+ }
1735
+
1736
+ for (const f of failedParticipants) {
1737
+ participantData[f] = {
1738
+ statusCode: 404,
1739
+ message: addParticipantResultCodes[404],
1740
+ isGroupCreator: false,
1741
+ isInviteV4Sent: false
1742
+ };
1743
+ }
1744
+
1745
+ return { title: title, gid: createGroupResult.wid, participants: participantData };
1746
+ }, title, participants, options);
1747
+ }
1748
+
1749
+ /**
1750
+ * An object that handles the result for {@link createChannel} method
1751
+ * @typedef {Object} CreateChannelResult
1752
+ * @property {string} title A channel title
1753
+ * @property {ChatId} nid An object that handels the newly created channel ID
1754
+ * @property {string} nid.server 'newsletter'
1755
+ * @property {string} nid.user 'XXXXXXXXXX'
1756
+ * @property {string} nid._serialized 'XXXXXXXXXX@newsletter'
1757
+ * @property {string} inviteLink The channel invite link, starts with 'https://whatsapp.com/channel/'
1758
+ * @property {number} createdAtTs The timestamp the channel was created at
1759
+ */
1760
+
1761
+ /**
1762
+ * Options for the channel creation
1763
+ * @typedef {Object} CreateChannelOptions
1764
+ * @property {?string} description The channel description
1765
+ * @property {?MessageMedia} picture The channel profile picture
1766
+ */
1767
+
1768
+ /**
1769
+ * Creates a new channel
1770
+ * @param {string} title The channel name
1771
+ * @param {CreateChannelOptions} options
1772
+ * @returns {Promise<CreateChannelResult|string>} Returns an object that handles the result for the channel creation or an error message as a string
1773
+ */
1774
+ async createChannel(title, options = {}) {
1775
+ return await this.pupPage.evaluate(async (title, options) => {
1776
+ let response, { description = null, picture = null } = options;
1777
+
1778
+ if (!window.Store.ChannelUtils.isNewsletterCreationEnabled()) {
1779
+ return 'CreateChannelError: A channel creation is not enabled';
1780
+ }
1781
+
1782
+ if (picture) {
1783
+ picture = await window.WWebJS.cropAndResizeImage(picture, {
1784
+ asDataUrl: true,
1785
+ mimetype: 'image/jpeg',
1786
+ size: 640,
1787
+ quality: 1
1788
+ });
1789
+ }
1790
+
1791
+ try {
1792
+ response = await window.Store.ChannelUtils.createNewsletterQuery({
1793
+ name: title,
1794
+ description: description,
1795
+ picture: picture,
1796
+ });
1797
+ } catch (err) {
1798
+ if (err.name === 'ServerStatusCodeError') {
1799
+ return 'CreateChannelError: An error occupied while creating a channel';
1800
+ }
1801
+ throw err;
1802
+ }
1803
+
1804
+ return {
1805
+ title: title,
1806
+ nid: window.Store.JidToWid.newsletterJidToWid(response.idJid),
1807
+ inviteLink: `https://whatsapp.com/channel/${response.newsletterInviteLinkMetadataMixin.inviteCode}`,
1808
+ createdAtTs: response.newsletterCreationTimeMetadataMixin.creationTimeValue
1809
+ };
1810
+ }, title, options);
1811
+ }
1812
+
1813
+ /**
1814
+ * Subscribe to channel
1815
+ * @param {string} channelId The channel ID
1816
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1817
+ */
1818
+ async subscribeToChannel(channelId) {
1819
+ return await this.pupPage.evaluate(async (channelId) => {
1820
+ return await window.WWebJS.subscribeToUnsubscribeFromChannel(channelId, 'Subscribe');
1821
+ }, channelId);
1822
+ }
1823
+
1824
+ /**
1825
+ * Options for unsubscribe from a channel
1826
+ * @typedef {Object} UnsubscribeOptions
1827
+ * @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
1828
+ */
1829
+
1830
+ /**
1831
+ * Unsubscribe from channel
1832
+ * @param {string} channelId The channel ID
1833
+ * @param {UnsubscribeOptions} options
1834
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1835
+ */
1836
+ async unsubscribeFromChannel(channelId, options) {
1837
+ return await this.pupPage.evaluate(async (channelId, options) => {
1838
+ return await window.WWebJS.subscribeToUnsubscribeFromChannel(channelId, 'Unsubscribe', options);
1839
+ }, channelId, options);
1840
+ }
1841
+
1842
+ /**
1843
+ * Options for transferring a channel ownership to another user
1844
+ * @typedef {Object} TransferChannelOwnershipOptions
1845
+ * @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.
1846
+ */
1847
+
1848
+ /**
1849
+ * Transfers a channel ownership to another user.
1850
+ * Note: the user you are transferring the channel ownership to must be a channel admin.
1851
+ * @param {string} channelId
1852
+ * @param {string} newOwnerId
1853
+ * @param {TransferChannelOwnershipOptions} options
1854
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1855
+ */
1856
+ async transferChannelOwnership(channelId, newOwnerId, options = {}) {
1857
+ return await this.pupPage.evaluate(async (channelId, newOwnerId, options) => {
1858
+ const channel = await window.WWebJS.getChat(channelId, { getAsModel: false });
1859
+ const newOwner = window.Store.Contact.get(newOwnerId) || (await window.Store.Contact.find(newOwnerId));
1860
+ if (!channel.newsletterMetadata) {
1861
+ await window.Store.NewsletterMetadataCollection.update(channel.id);
1862
+ }
1863
+
1864
+ try {
1865
+ await window.Store.ChannelUtils.changeNewsletterOwnerAction(channel, newOwner);
1866
+
1867
+ if (options.shouldDismissSelfAsAdmin) {
1868
+ const meContact = window.Store.ContactCollection.getMeContact();
1869
+ meContact && (await window.Store.ChannelUtils.demoteNewsletterAdminAction(channel, meContact));
1870
+ }
1871
+ } catch (error) {
1872
+ return false;
1873
+ }
1874
+
1875
+ return true;
1876
+ }, channelId, newOwnerId, options);
1877
+ }
1878
+
1879
+ /**
1880
+ * Searches for channels based on search criteria, there are some notes:
1881
+ * 1. The method finds only channels you are not subscribed to currently
1882
+ * 2. If you have never been subscribed to a found channel
1883
+ * or you have unsubscribed from it with {@link UnsubscribeOptions.deleteLocalModels} set to 'true',
1884
+ * the lastMessage property of a found channel will be 'null'
1885
+ *
1886
+ * @param {Object} searchOptions Search options
1887
+ * @param {string} [searchOptions.searchText = ''] Text to search
1888
+ * @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
1889
+ * @param {boolean} [searchOptions.skipSubscribedNewsletters = false] If true, channels that user is subscribed to won't appear in found channels
1890
+ * @param {number} [searchOptions.view = 0] View type, makes sense only when the searchText is empty. Valid values to provide are:
1891
+ * 0 for RECOMMENDED channels
1892
+ * 1 for TRENDING channels
1893
+ * 2 for POPULAR channels
1894
+ * 3 for NEW channels
1895
+ * @param {number} [searchOptions.limit = 50] The limit of found channels to be appear in the returnig result
1896
+ * @returns {Promise<Array<Channel>|[]>} Returns an array of Channel objects or an empty array if no channels were found
1897
+ */
1898
+ async searchChannels(searchOptions = {}) {
1899
+ return await this.pupPage.evaluate(async ({
1900
+ searchText = '',
1901
+ countryCodes = [window.Store.ChannelUtils.currentRegion],
1902
+ skipSubscribedNewsletters = false,
1903
+ view = 0,
1904
+ limit = 50
1905
+ }) => {
1906
+ searchText = searchText.trim();
1907
+ const currentRegion = window.Store.ChannelUtils.currentRegion;
1908
+ if (![0, 1, 2, 3].includes(view)) view = 0;
1909
+
1910
+ countryCodes = countryCodes.length === 1 && countryCodes[0] === currentRegion
1911
+ ? countryCodes
1912
+ : countryCodes.filter((code) => Object.keys(window.Store.ChannelUtils.countryCodesIso).includes(code));
1913
+
1914
+ const viewTypeMapping = {
1915
+ 0: 'RECOMMENDED',
1916
+ 1: 'TRENDING',
1917
+ 2: 'POPULAR',
1918
+ 3: 'NEW'
1919
+ };
1920
+
1921
+ searchOptions = {
1922
+ searchText: searchText,
1923
+ countryCodes: countryCodes,
1924
+ skipSubscribedNewsletters: skipSubscribedNewsletters,
1925
+ view: viewTypeMapping[view],
1926
+ categories: [],
1927
+ cursorToken: ''
1928
+ };
1929
+
1930
+ const originalFunction = window.Store.ChannelUtils.getNewsletterDirectoryPageSize;
1931
+ limit !== 50 && (window.Store.ChannelUtils.getNewsletterDirectoryPageSize = () => limit);
1932
+
1933
+ const channels = (await window.Store.ChannelUtils.fetchNewsletterDirectories(searchOptions)).newsletters;
1934
+
1935
+ limit !== 50 && (window.Store.ChannelUtils.getNewsletterDirectoryPageSize = originalFunction);
1936
+
1937
+ return channels
1938
+ ? await Promise.all(channels.map((channel) => window.WWebJS.getChatModel(channel, { isChannel: true })))
1939
+ : [];
1940
+ }, searchOptions);
1941
+ }
1942
+
1943
+ /**
1944
+ * Deletes the channel you created
1945
+ * @param {string} channelId The ID of a channel to delete
1946
+ * @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
1947
+ */
1948
+ async deleteChannel(channelId) {
1949
+ return await this.client.pupPage.evaluate(async (channelId) => {
1950
+ const channel = await window.WWebJS.getChat(channelId, { getAsModel: false });
1951
+ if (!channel) return false;
1952
+ try {
1953
+ await window.Store.ChannelUtils.deleteNewsletterAction(channel);
1954
+ return true;
1955
+ } catch (err) {
1956
+ if (err.name === 'ServerStatusCodeError') return false;
1957
+ throw err;
1958
+ }
1959
+ }, channelId);
1960
+ }
1961
+
1962
+ /**
1963
+ * Get all current Labels
1964
+ * @returns {Promise<Array<Label>>}
1965
+ */
1966
+ async getLabels() {
1967
+ const labels = await this.pupPage.evaluate(async () => {
1968
+ return window.WWebJS.getLabels();
1969
+ });
1970
+
1971
+ return labels.map(data => new Label(this, data));
1972
+ }
1973
+
1974
+ /**
1975
+ * Get all current Broadcast
1976
+ * @returns {Promise<Array<Broadcast>>}
1977
+ */
1978
+ async getBroadcasts() {
1979
+ const broadcasts = await this.pupPage.evaluate(async () => {
1980
+ return window.WWebJS.getAllStatuses();
1981
+ });
1982
+ return broadcasts.map(data => new Broadcast(this, data));
1983
+ }
1984
+
1985
+ /**
1986
+ * Get Label instance by ID
1987
+ * @param {string} labelId
1988
+ * @returns {Promise<Label>}
1989
+ */
1990
+ async getLabelById(labelId) {
1991
+ const label = await this.pupPage.evaluate(async (labelId) => {
1992
+ return window.WWebJS.getLabel(labelId);
1993
+ }, labelId);
1994
+
1995
+ return new Label(this, label);
1996
+ }
1997
+
1998
+ /**
1999
+ * Get all Labels assigned to a chat
2000
+ * @param {string} chatId
2001
+ * @returns {Promise<Array<Label>>}
2002
+ */
2003
+ async getChatLabels(chatId) {
2004
+ const labels = await this.pupPage.evaluate(async (chatId) => {
2005
+ return window.WWebJS.getChatLabels(chatId);
2006
+ }, chatId);
2007
+
2008
+ return labels.map(data => new Label(this, data));
2009
+ }
2010
+
2011
+ /**
2012
+ * Get all Chats for a specific Label
2013
+ * @param {string} labelId
2014
+ * @returns {Promise<Array<Chat>>}
2015
+ */
2016
+ async getChatsByLabelId(labelId) {
2017
+ const chatIds = await this.pupPage.evaluate(async (labelId) => {
2018
+ const label = window.Store.Label.get(labelId);
2019
+ const labelItems = label.labelItemCollection.getModelsArray();
2020
+ return labelItems.reduce((result, item) => {
2021
+ if (item.parentType === 'Chat') {
2022
+ result.push(item.parentId);
2023
+ }
2024
+ return result;
2025
+ }, []);
2026
+ }, labelId);
2027
+
2028
+ return Promise.all(chatIds.map(id => this.getChatById(id)));
2029
+ }
2030
+
2031
+ /**
2032
+ * Gets all blocked contacts by host account
2033
+ * @returns {Promise<Array<Contact>>}
2034
+ */
2035
+ async getBlockedContacts() {
2036
+ const blockedContacts = await this.pupPage.evaluate(() => {
2037
+ let chatIds = window.Store.Blocklist.getModelsArray().map(a => a.id._serialized);
2038
+ return Promise.all(chatIds.map(id => window.WWebJS.getContact(id)));
2039
+ });
2040
+
2041
+ return blockedContacts.map(contact => ContactFactory.create(this.client, contact));
2042
+ }
2043
+
2044
+ /**
2045
+ * Sets the current user's profile picture.
2046
+ * @param {MessageMedia} media
2047
+ * @returns {Promise<boolean>} Returns true if the picture was properly updated.
2048
+ */
2049
+ async setProfilePicture(media) {
2050
+ const success = await this.pupPage.evaluate((chatid, media) => {
2051
+ return window.WWebJS.setPicture(chatid, media);
2052
+ }, this.info.wid._serialized, media);
2053
+
2054
+ return success;
2055
+ }
2056
+
2057
+ /**
2058
+ * Deletes the current user's profile picture.
2059
+ * @returns {Promise<boolean>} Returns true if the picture was properly deleted.
2060
+ */
2061
+ async deleteProfilePicture() {
2062
+ const success = await this.pupPage.evaluate((chatid) => {
2063
+ return window.WWebJS.deletePicture(chatid);
2064
+ }, this.info.wid._serialized);
2065
+
2066
+ return success;
2067
+ }
2068
+
2069
+ /**
2070
+ * Change labels in chats
2071
+ * @param {Array<number|string>} labelIds
2072
+ * @param {Array<string>} chatIds
2073
+ * @returns {Promise<void>}
2074
+ */
2075
+ async addOrRemoveLabels(labelIds, chatIds) {
2076
+
2077
+ return this.pupPage.evaluate(async (labelIds, chatIds) => {
2078
+ if (['smba', 'smbi'].indexOf(window.Store.Conn.platform) === -1) {
2079
+ throw '[LT01] Only Whatsapp business';
2080
+ }
2081
+ const labels = window.WWebJS.getLabels().filter(e => labelIds.find(l => l == e.id) !== undefined);
2082
+ const chats = window.Store.Chat.filter(e => chatIds.includes(e.id._serialized));
2083
+
2084
+ let actions = labels.map(label => ({id: label.id, type: 'add'}));
2085
+
2086
+ chats.forEach(chat => {
2087
+ (chat.labels || []).forEach(n => {
2088
+ if (!actions.find(e => e.id == n)) {
2089
+ actions.push({id: n, type: 'remove'});
2090
+ }
2091
+ });
2092
+ });
2093
+
2094
+ return await window.Store.Label.addOrRemoveLabels(actions, chats);
2095
+ }, labelIds, chatIds);
2096
+ }
2097
+
2098
+ /**
2099
+ * An object that handles the information about the group membership request
2100
+ * @typedef {Object} GroupMembershipRequest
2101
+ * @property {Object} id The wid of a user who requests to enter the group
2102
+ * @property {Object} addedBy The wid of a user who created that request
2103
+ * @property {Object|null} parentGroupId The wid of a community parent group to which the current group is linked
2104
+ * @property {string} requestMethod The method used to create the request: NonAdminAdd/InviteLink/LinkedGroupJoin
2105
+ * @property {number} t The timestamp the request was created at
2106
+ */
2107
+
2108
+ /**
2109
+ * Gets an array of membership requests
2110
+ * @param {string} groupId The ID of a group to get membership requests for
2111
+ * @returns {Promise<Array<GroupMembershipRequest>>} An array of membership requests
2112
+ */
2113
+ async getGroupMembershipRequests(groupId) {
2114
+ return await this.pupPage.evaluate(async (groupId) => {
2115
+ const groupWid = window.Store.WidFactory.createWid(groupId);
2116
+ return await window.Store.MembershipRequestUtils.getMembershipApprovalRequests(groupWid);
2117
+ }, groupId);
2118
+ }
2119
+
2120
+ /**
2121
+ * An object that handles the result for membership request action
2122
+ * @typedef {Object} MembershipRequestActionResult
2123
+ * @property {string} requesterId User ID whos membership request was approved/rejected
2124
+ * @property {number|undefined} error An error code that occurred during the operation for the participant
2125
+ * @property {string} message A message with a result of membership request action
2126
+ */
2127
+
2128
+ /**
2129
+ * An object that handles options for {@link approveGroupMembershipRequests} and {@link rejectGroupMembershipRequests} methods
2130
+ * @typedef {Object} MembershipRequestActionOptions
2131
+ * @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
2132
+ * @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]
2133
+ */
2134
+
2135
+ /**
2136
+ * Approves membership requests if any
2137
+ * @param {string} groupId The group ID to get the membership request for
2138
+ * @param {MembershipRequestActionOptions} options Options for performing a membership request action
2139
+ * @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
2140
+ */
2141
+ async approveGroupMembershipRequests(groupId, options = {}) {
2142
+ return await this.pupPage.evaluate(async (groupId, options) => {
2143
+ const { requesterIds = null, sleep = [250, 500] } = options;
2144
+ return await window.WWebJS.membershipRequestAction(groupId, 'Approve', requesterIds, sleep);
2145
+ }, groupId, options);
2146
+ }
2147
+
2148
+ /**
2149
+ * Rejects membership requests if any
2150
+ * @param {string} groupId The group ID to get the membership request for
2151
+ * @param {MembershipRequestActionOptions} options Options for performing a membership request action
2152
+ * @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
2153
+ */
2154
+ async rejectGroupMembershipRequests(groupId, options = {}) {
2155
+ return await this.pupPage.evaluate(async (groupId, options) => {
2156
+ const { requesterIds = null, sleep = [250, 500] } = options;
2157
+ return await window.WWebJS.membershipRequestAction(groupId, 'Reject', requesterIds, sleep);
2158
+ }, groupId, options);
2159
+ }
2160
+
2161
+
2162
+ /**
2163
+ * Setting autoload download audio
2164
+ * @param {boolean} flag true/false
2165
+ */
2166
+ async setAutoDownloadAudio(flag) {
2167
+ await this.pupPage.evaluate(async flag => {
2168
+ const autoDownload = window.Store.Settings.getAutoDownloadAudio();
2169
+ if (autoDownload === flag) {
2170
+ return flag;
2171
+ }
2172
+ await window.Store.Settings.setAutoDownloadAudio(flag);
2173
+ return flag;
2174
+ }, flag);
2175
+ }
2176
+
2177
+ /**
2178
+ * Setting autoload download documents
2179
+ * @param {boolean} flag true/false
2180
+ */
2181
+ async setAutoDownloadDocuments(flag) {
2182
+ await this.pupPage.evaluate(async flag => {
2183
+ const autoDownload = window.Store.Settings.getAutoDownloadDocuments();
2184
+ if (autoDownload === flag) {
2185
+ return flag;
2186
+ }
2187
+ await window.Store.Settings.setAutoDownloadDocuments(flag);
2188
+ return flag;
2189
+ }, flag);
2190
+ }
2191
+
2192
+ /**
2193
+ * Setting autoload download photos
2194
+ * @param {boolean} flag true/false
2195
+ */
2196
+ async setAutoDownloadPhotos(flag) {
2197
+ await this.pupPage.evaluate(async flag => {
2198
+ const autoDownload = window.Store.Settings.getAutoDownloadPhotos();
2199
+ if (autoDownload === flag) {
2200
+ return flag;
2201
+ }
2202
+ await window.Store.Settings.setAutoDownloadPhotos(flag);
2203
+ return flag;
2204
+ }, flag);
2205
+ }
2206
+
2207
+ /**
2208
+ * Setting autoload download videos
2209
+ * @param {boolean} flag true/false
2210
+ */
2211
+ async setAutoDownloadVideos(flag) {
2212
+ await this.pupPage.evaluate(async flag => {
2213
+ const autoDownload = window.Store.Settings.getAutoDownloadVideos();
2214
+ if (autoDownload === flag) {
2215
+ return flag;
2216
+ }
2217
+ await window.Store.Settings.setAutoDownloadVideos(flag);
2218
+ return flag;
2219
+ }, flag);
2220
+ }
2221
+
2222
+ /**
2223
+ * Setting background synchronization.
2224
+ * NOTE: this action will take effect after you restart the client.
2225
+ * @param {boolean} flag true/false
2226
+ * @returns {Promise<boolean>}
2227
+ */
2228
+ async setBackgroundSync(flag) {
2229
+ return await this.pupPage.evaluate(async flag => {
2230
+ const backSync = window.Store.Settings.getGlobalOfflineNotifications();
2231
+ if (backSync === flag) {
2232
+ return flag;
2233
+ }
2234
+ await window.Store.Settings.setGlobalOfflineNotifications(flag);
2235
+ return flag;
2236
+ }, flag);
2237
+ }
2238
+
2239
+ /**
2240
+ * Get user device count by ID
2241
+ * Each WaWeb Connection counts as one device, and the phone (if exists) counts as one
2242
+ * So for a non-enterprise user with one WaWeb connection it should return "2"
2243
+ * @param {string} userId
2244
+ * @returns {Promise<number>}
2245
+ */
2246
+ async getContactDeviceCount(userId) {
2247
+ return await this.pupPage.evaluate(async (userId) => {
2248
+ const devices = await window.Store.DeviceList.getDeviceIds([window.Store.WidFactory.createWid(userId)]);
2249
+ if (devices && devices.length && devices[0] != null && typeof devices[0].devices == 'object') {
2250
+ return devices[0].devices.length;
2251
+ }
2252
+ return 0;
2253
+ }, userId);
2254
+ }
2255
+
2256
+ /**
2257
+ * Sync chat history conversation
2258
+ * @param {string} chatId
2259
+ * @return {Promise<boolean>} True if operation completed successfully, false otherwise.
2260
+ */
2261
+ async syncHistory(chatId) {
2262
+ return await this.pupPage.evaluate(async (chatId) => {
2263
+ const chatWid = window.Store.WidFactory.createWid(chatId);
2264
+ const chat = window.Store.Chat.get(chatWid) ?? (await window.Store.Chat.find(chatWid));
2265
+ if (chat?.endOfHistoryTransferType === 0) {
2266
+ await window.Store.HistorySync.sendPeerDataOperationRequest(3, {
2267
+ chatId: chat.id
2268
+ });
2269
+ return true;
2270
+ }
2271
+ return false;
2272
+ }, chatId);
2273
+ }
2274
+
2275
+ /**
2276
+ * Generates a WhatsApp call link (video call or voice call)
2277
+ * @param {Date} startTime The start time of the call
2278
+ * @param {string} callType The type of a WhatsApp call link to generate, valid values are: `video` | `voice`
2279
+ * @returns {Promise<string>} The WhatsApp call link (https://call.whatsapp.com/video/XxXxXxXxXxXxXx) or an empty string if a generation failed.
2280
+ */
2281
+ async createCallLink(startTime, callType) {
2282
+ if (!['video', 'voice'].includes(callType)) {
2283
+ throw new class CreateCallLinkError extends Error {
2284
+ constructor(m) { super(m); }
2285
+ }('Invalid \'callType\' parameter value is provided. Valid values are: \'voice\' | \'video\'.');
2286
+ }
2287
+
2288
+ startTime = Math.floor(startTime.getTime() / 1000);
2289
+
2290
+ return await this.pupPage.evaluate(async (startTimeTs, callType) => {
2291
+ const response = await window.Store.ScheduledEventMsgUtils.createEventCallLink(startTimeTs, callType);
2292
+ return response ?? '';
2293
+ }, startTime, callType);
2294
+ }
2295
+
2296
+ /**
2297
+ * Sends a response to the scheduled event message, indicating whether a user is going to attend the event or not
2298
+ * @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
2299
+ * @param {string} eventMessageId The scheduled event message ID
2300
+ * @returns {Promise<boolean>}
2301
+ */
2302
+ async sendResponseToScheduledEvent(response, eventMessageId) {
2303
+ if (![0, 1, 2, 3].includes(response)) return false;
2304
+
2305
+ return await this.pupPage.evaluate(async (response, msgId) => {
2306
+ const eventMsg = window.Store.Msg.get(msgId) || (await window.Store.Msg.getMessagesById([msgId]))?.messages?.[0];
2307
+ if (!eventMsg) return false;
2308
+
2309
+ await window.Store.ScheduledEventMsgUtils.sendEventResponseMsg(response, eventMsg);
2310
+ return true;
2311
+ }, response, eventMessageId);
2312
+ }
2313
+
2314
+ /**
2315
+ * Save new contact to user's addressbook or edit the existing one
2316
+ * @param {string} phoneNumber The contact's phone number in a format "17182222222", where "1" is a country code
2317
+ * @param {string} firstName
2318
+ * @param {string} lastName
2319
+ * @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
2320
+ * @returns {Promise<void>}
2321
+ */
2322
+ async saveOrEditAddressbookContact(phoneNumber, firstName, lastName, syncToAddressbook = false)
2323
+ {
2324
+ return await this.pupPage.evaluate(async (phoneNumber, firstName, lastName, syncToAddressbook) => {
2325
+ return await window.Store.AddressbookContactUtils.saveContactAction(
2326
+ phoneNumber,
2327
+ phoneNumber,
2328
+ null,
2329
+ null,
2330
+ firstName,
2331
+ lastName,
2332
+ syncToAddressbook
2333
+ );
2334
+ }, phoneNumber, firstName, lastName, syncToAddressbook);
2335
+ }
2336
+
2337
+ /**
2338
+ * Deletes the contact from user's addressbook
2339
+ * @param {string} phoneNumber The contact's phone number in a format "17182222222", where "1" is a country code
2340
+ * @returns {Promise<void>}
2341
+ */
2342
+ async deleteAddressbookContact(phoneNumber)
2343
+ {
2344
+ return await this.pupPage.evaluate(async (phoneNumber) => {
2345
+ return await window.Store.AddressbookContactUtils.deleteContactAction(phoneNumber);
2346
+ }, phoneNumber);
2347
+ }
2348
+
2349
+ /**
2350
+ * Get lid and phone number for multiple users
2351
+ * @param {string[]} userIds - Array of user IDs
2352
+ * @returns {Promise<Array<{ lid: string, pn: string }>>}
2353
+ */
2354
+ async getContactLidAndPhone(userIds) {
2355
+ return await this.pupPage.evaluate(async (userIds) => {
2356
+ if (!Array.isArray(userIds)) userIds = [userIds];
2357
+
2358
+ return await Promise.all(userIds.map(async (userId) => {
2359
+ const { lid, phone } = await window.WWebJS.enforceLidAndPnRetrieval(userId);
2360
+
2361
+ return {
2362
+ lid: lid?._serialized,
2363
+ pn: phone?._serialized
2364
+ };
2365
+ }));
2366
+ }, userIds);
2367
+ }
2368
+
2369
+ /**
2370
+ * Add or edit a customer note
2371
+ * @see https://faq.whatsapp.com/1433099287594476
2372
+ * @param {string} userId The ID of a customer to add a note to
2373
+ * @param {string} note The note to add
2374
+ * @returns {Promise<void>}
2375
+ */
2376
+ async addOrEditCustomerNote(userId, note) {
2377
+ return await this.pupPage.evaluate(async (userId, note) => {
2378
+ if (!window.Store.BusinessGatingUtils.smbNotesV1Enabled()) return;
2379
+
2380
+ return window.Store.CustomerNoteUtils.noteAddAction(
2381
+ 'unstructured',
2382
+ window.Store.WidToJid.widToUserJid(window.Store.WidFactory.createWid(userId)),
2383
+ note
2384
+ );
2385
+ }, userId, note);
2386
+ }
2387
+
2388
+ /**
2389
+ * Get a customer note
2390
+ * @see https://faq.whatsapp.com/1433099287594476
2391
+ * @param {string} userId The ID of a customer to get a note from
2392
+ * @returns {Promise<{
2393
+ * chatId: string,
2394
+ * content: string,
2395
+ * createdAt: number,
2396
+ * id: string,
2397
+ * modifiedAt: number,
2398
+ * type: string
2399
+ * }>}
2400
+ */
2401
+ async getCustomerNote(userId) {
2402
+ return await this.pupPage.evaluate(async (userId) => {
2403
+ if (!window.Store.BusinessGatingUtils.smbNotesV1Enabled()) return null;
2404
+
2405
+ const note = await window.Store.CustomerNoteUtils.retrieveOnlyNoteForChatJid(
2406
+ window.Store.WidToJid.widToUserJid(window.Store.WidFactory.createWid(userId))
2407
+ );
2408
+
2409
+ let serialized = note?.serialize();
2410
+
2411
+ if (!serialized) return null;
2412
+
2413
+ serialized.chatId = window.Store.JidToWid.userJidToUserWid(serialized.chatJid)._serialized;
2414
+ delete serialized.chatJid;
2415
+
2416
+ return serialized;
2417
+ }, userId);
2418
+ }
2419
+
2420
+ /**
2421
+ * Get Poll Votes
2422
+ * @param {string} messageId
2423
+ * @return {Promise<Array<PollVote>>}
2424
+ */
2425
+ async getPollVotes(messageId) {
2426
+ const msg = await this.getMessageById(messageId);
2427
+ if (!msg) return [];
2428
+ if (msg.type != MessageTypes.POLL_CREATION) throw 'Invalid usage! Can only be used with a pollCreation message';
2429
+
2430
+ const pollVotes = await this.pupPage.evaluate( async (msg) => {
2431
+ const msgKey = window.Store.MsgKey.fromString(msg.id._serialized);
2432
+ let pollVotes = await window.Store.PollsVotesSchema.getTable().equals(['parentMsgKey'], msgKey.toString());
2433
+
2434
+ return pollVotes.map(item => {
2435
+ const typedArray = new Uint8Array(item.selectedOptionLocalIds);
2436
+ return {
2437
+ ...item,
2438
+ selectedOptionLocalIds: Array.from(typedArray)
2439
+ };
2440
+ });
2441
+ }, msg);
2442
+
2443
+ return pollVotes.map((pollVote) => new PollVote(this.client, {...pollVote, parentMessage: msg}));
2444
+ }
2445
+ }
2446
+
2447
+ module.exports = Client;