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