whatsapp-web.js 1.20.0 → 1.21.1-alpha.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 CHANGED
@@ -1,4 +1,4 @@
1
- [![npm](https://img.shields.io/npm/v/whatsapp-web.js.svg)](https://www.npmjs.com/package/whatsapp-web.js) [![Depfu](https://badges.depfu.com/badges/4a65a0de96ece65fdf39e294e0c8dcba/overview.svg)](https://depfu.com/github/pedroslopez/whatsapp-web.js?project_id=9765) ![WhatsApp_Web 2.2306.7](https://img.shields.io/badge/WhatsApp_Web-2.2306.7-brightgreen.svg) [![Discord Chat](https://img.shields.io/discord/698610475432411196.svg?logo=discord)](https://discord.gg/H7DqQs4)
1
+ [![npm](https://img.shields.io/npm/v/whatsapp-web.js.svg)](https://www.npmjs.com/package/whatsapp-web.js) [![Depfu](https://badges.depfu.com/badges/4a65a0de96ece65fdf39e294e0c8dcba/overview.svg)](https://depfu.com/github/pedroslopez/whatsapp-web.js?project_id=9765) ![WhatsApp_Web 2.2322.15](https://img.shields.io/badge/WhatsApp_Web-2.2322.15-brightgreen.svg) [![Discord Chat](https://img.shields.io/discord/698610475432411196.svg?logo=discord)](https://discord.gg/H7DqQs4)
2
2
 
3
3
  # whatsapp-web.js
4
4
  A WhatsApp API client that connects through the WhatsApp Web browser app
package/example.js CHANGED
@@ -140,6 +140,12 @@ client.on('message', async msg => {
140
140
  const attachmentData = await quotedMsg.downloadMedia();
141
141
  client.sendMessage(msg.from, attachmentData, { caption: 'Here\'s your requested media.' });
142
142
  }
143
+ } else if (msg.body === '!isviewonce' && msg.hasQuotedMsg) {
144
+ const quotedMsg = await msg.getQuotedMessage();
145
+ if (quotedMsg.hasMedia) {
146
+ const media = await quotedMsg.downloadMedia();
147
+ await client.sendMessage(msg.from, media, { isViewOnce: true });
148
+ }
143
149
  } else if (msg.body === '!location') {
144
150
  msg.reply(new Location(37.422, -122.084, 'Googleplex\nGoogle Headquarters'));
145
151
  } else if (msg.location) {
@@ -201,6 +207,27 @@ client.on('message', async msg => {
201
207
  client.sendMessage(msg.from, list);
202
208
  } else if (msg.body === '!reaction') {
203
209
  msg.react('👍');
210
+ } else if (msg.body === '!edit') {
211
+ if (msg.hasQuotedMsg) {
212
+ const quotedMsg = await msg.getQuotedMessage();
213
+ if (quotedMsg.fromMe) {
214
+ quotedMsg.edit(msg.body.replace('!edit', ''));
215
+ } else {
216
+ msg.reply('I can only edit my own messages');
217
+ }
218
+ }
219
+ } else if (msg.body === '!updatelabels') {
220
+ const chat = await msg.getChat();
221
+ await chat.changeLabels([0, 1]);
222
+ } else if (msg.body === '!addlabels') {
223
+ const chat = await msg.getChat();
224
+ let labels = (await chat.getLabels()).map(l => l.id);
225
+ labels.push('0');
226
+ labels.push('1');
227
+ await chat.changeLabels(labels);
228
+ } else if (msg.body === '!removelabels') {
229
+ const chat = await msg.getChat();
230
+ await chat.changeLabels([]);
204
231
  }
205
232
  });
206
233
 
@@ -286,28 +313,28 @@ client.on('contact_changed', async (message, oldId, newId, isContact) => {
286
313
  `Their new phone number is ${newId.slice(0, -5)}.\n`);
287
314
 
288
315
  /**
289
- * Information about the {@name message}:
316
+ * Information about the @param {message}:
290
317
  *
291
318
  * 1. If a notification was emitted due to a group participant changing their phone number:
292
- * {@name message.author} is a participant's id before the change.
293
- * {@name message.recipients[0]} is a participant's id after the change (a new one).
319
+ * @param {message.author} is a participant's id before the change.
320
+ * @param {message.recipients[0]} is a participant's id after the change (a new one).
294
321
  *
295
322
  * 1.1 If the contact who changed their number WAS in the current user's contact list at the time of the change:
296
- * {@name message.to} is a group chat id the event was emitted in.
297
- * {@name message.from} is a current user's id that got an notification message in the group.
298
- * Also the {@name message.fromMe} is TRUE.
323
+ * @param {message.to} is a group chat id the event was emitted in.
324
+ * @param {message.from} is a current user's id that got an notification message in the group.
325
+ * Also the @param {message.fromMe} is TRUE.
299
326
  *
300
327
  * 1.2 Otherwise:
301
- * {@name message.from} is a group chat id the event was emitted in.
302
- * {@name message.to} is @type {undefined}.
303
- * Also {@name message.fromMe} is FALSE.
328
+ * @param {message.from} is a group chat id the event was emitted in.
329
+ * @param {message.to} is @type {undefined}.
330
+ * Also @param {message.fromMe} is FALSE.
304
331
  *
305
332
  * 2. If a notification was emitted due to a contact changing their phone number:
306
- * {@name message.templateParams} is an array of two user's ids:
333
+ * @param {message.templateParams} is an array of two user's ids:
307
334
  * the old (before the change) and a new one, stored in alphabetical order.
308
- * {@name message.from} is a current user's id that has a chat with a user,
335
+ * @param {message.from} is a current user's id that has a chat with a user,
309
336
  * whos phone number was changed.
310
- * {@name message.to} is a user's id (after the change), the current user has a chat with.
337
+ * @param {message.to} is a user's id (after the change), the current user has a chat with.
311
338
  */
312
339
  });
313
340
 
package/index.d.ts CHANGED
@@ -60,6 +60,9 @@ declare namespace WAWebJS {
60
60
  /** Get contact instance by ID */
61
61
  getContactById(contactId: string): Promise<Contact>
62
62
 
63
+ /** Get message by ID */
64
+ getMessageById(messageId: string): Promise<Message>
65
+
63
66
  /** Get all current contact instances */
64
67
  getContacts(): Promise<Contact[]>
65
68
 
@@ -71,6 +74,9 @@ declare namespace WAWebJS {
71
74
 
72
75
  /** Get all current Labels */
73
76
  getLabels(): Promise<Label[]>
77
+
78
+ /** Change labels in chats */
79
+ addOrRemoveLabels(labelIds: Array<number|string>, chatIds: Array<string>): Promise<void>
74
80
 
75
81
  /** Get Label instance by ID */
76
82
  getLabelById(labelId: string): Promise<Label>
@@ -242,6 +248,16 @@ declare namespace WAWebJS {
242
248
  ack: MessageAck
243
249
  ) => void): this
244
250
 
251
+ /** Emitted when an ack event occurrs on message type */
252
+ on(event: 'message_edit', listener: (
253
+ /** The message that was affected */
254
+ message: Message,
255
+ /** New text message */
256
+ newBody: String,
257
+ /** Prev text message */
258
+ prevBody: String
259
+ ) => void): this
260
+
245
261
  /** Emitted when a chat unread count changes */
246
262
  on(event: 'unread_count', listener: (
247
263
  /** The chat that was affected */
@@ -365,6 +381,10 @@ declare namespace WAWebJS {
365
381
  puppeteer?: puppeteer.PuppeteerNodeLaunchOptions & puppeteer.ConnectOptions
366
382
  /** Determines how to save and restore sessions. Will use LegacySessionAuth if options.session is set. Otherwise, NoAuth will be used. */
367
383
  authStrategy?: AuthStrategy,
384
+ /** The version of WhatsApp Web to use. Use options.webVersionCache to configure how the version is retrieved. */
385
+ webVersion?: string,
386
+ /** Determines how to retrieve the WhatsApp Web version specified in options.webVersion. */
387
+ webVersionCache?: WebCacheOptions,
368
388
  /** How many times should the qrcode be refreshed before giving up
369
389
  * @default 0 (disabled) */
370
390
  qrMaxRetries?: number,
@@ -392,6 +412,24 @@ declare namespace WAWebJS {
392
412
  proxyAuthentication?: {username: string, password: string} | undefined
393
413
  }
394
414
 
415
+ export interface LocalWebCacheOptions {
416
+ type: 'local',
417
+ path?: string,
418
+ strict?: boolean
419
+ }
420
+
421
+ export interface RemoteWebCacheOptions {
422
+ type: 'remote',
423
+ remotePath: string,
424
+ strict?: boolean
425
+ }
426
+
427
+ export interface NoWebCacheOptions {
428
+ type: 'none'
429
+ }
430
+
431
+ export type WebCacheOptions = NoWebCacheOptions | LocalWebCacheOptions | RemoteWebCacheOptions;
432
+
395
433
  /**
396
434
  * Base class which all authentication strategies extend
397
435
  */
@@ -545,6 +583,7 @@ declare namespace WAWebJS {
545
583
  MESSAGE_REVOKED_EVERYONE = 'message_revoke_everyone',
546
584
  MESSAGE_REVOKED_ME = 'message_revoke_me',
547
585
  MESSAGE_ACK = 'message_ack',
586
+ MESSAGE_EDIT = 'message_edit',
548
587
  MEDIA_UPLOADED = 'media_uploaded',
549
588
  CONTACT_CHANGED = 'contact_changed',
550
589
  GROUP_JOIN = 'group_join',
@@ -767,6 +806,10 @@ declare namespace WAWebJS {
767
806
  businessOwnerJid?: string,
768
807
  /** Product JID */
769
808
  productId?: string,
809
+ /** Last edit time */
810
+ latestEditSenderTimestampMs?: number,
811
+ /** Last edit message author */
812
+ latestEditMsgKey?: MessageId,
770
813
  /** Message buttons */
771
814
  dynamicReplyButtons?: object,
772
815
  /** Selected button ID */
@@ -824,6 +867,8 @@ declare namespace WAWebJS {
824
867
  * Gets the reactions associated with the given message
825
868
  */
826
869
  getReactions: () => Promise<ReactionList[]>,
870
+ /** Edits the current message */
871
+ edit: (content: MessageContent, options?: MessageEditOptions) => Promise<Message | null>,
827
872
  }
828
873
 
829
874
  /** ID that represents a message */
@@ -867,6 +912,8 @@ declare namespace WAWebJS {
867
912
  sendMediaAsSticker?: boolean
868
913
  /** Send media as document */
869
914
  sendMediaAsDocument?: boolean
915
+ /** Send photo/video as a view once message */
916
+ isViewOnce?: boolean
870
917
  /** Automatically parse vCards and send them as contacts */
871
918
  parseVCards?: boolean
872
919
  /** Image or videos caption */
@@ -889,6 +936,16 @@ declare namespace WAWebJS {
889
936
  stickerCategories?: string[]
890
937
  }
891
938
 
939
+ /** Options for editing a message */
940
+ export interface MessageEditOptions {
941
+ /** Show links preview. Has no effect on multi-device accounts. */
942
+ linkPreview?: boolean
943
+ /** Contacts that are being mentioned in the message */
944
+ mentions?: Contact[]
945
+ /** Extra options */
946
+ extra?: any
947
+ }
948
+
892
949
  export interface MediaFromURLOptions {
893
950
  client?: Client
894
951
  filename?: string
@@ -1113,6 +1170,8 @@ declare namespace WAWebJS {
1113
1170
  markUnread: () => Promise<void>
1114
1171
  /** Returns array of all Labels assigned to this Chat */
1115
1172
  getLabels: () => Promise<Label[]>
1173
+ /** Add or remove labels to this Chat */
1174
+ changeLabels: (labelIds: Array<string | number>) => Promise<void>
1116
1175
  }
1117
1176
 
1118
1177
  export interface MessageSearchOptions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whatsapp-web.js",
3
- "version": "1.20.0",
3
+ "version": "1.21.1-alpha.0",
4
4
  "description": "Library for interacting with the WhatsApp Web API ",
5
5
  "main": "./index.js",
6
6
  "typings": "./index.d.ts",
package/src/Client.js CHANGED
@@ -10,6 +10,7 @@ const { WhatsWebURL, DefaultOptions, Events, WAState } = require('./util/Constan
10
10
  const { ExposeStore, LoadUtils } = require('./util/Injected');
11
11
  const ChatFactory = require('./factories/ChatFactory');
12
12
  const ContactFactory = require('./factories/ContactFactory');
13
+ const WebCacheFactory = require('./webCache/WebCacheFactory');
13
14
  const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List, Reaction, Chat } = require('./structures');
14
15
  const LegacySessionAuth = require('./authStrategies/LegacySessionAuth');
15
16
  const NoAuth = require('./authStrategies/NoAuth');
@@ -19,6 +20,8 @@ const NoAuth = require('./authStrategies/NoAuth');
19
20
  * @extends {EventEmitter}
20
21
  * @param {object} options - Client options
21
22
  * @param {AuthStrategy} options.authStrategy - Determines how to save and restore sessions. Will use LegacySessionAuth if options.session is set. Otherwise, NoAuth will be used.
23
+ * @param {string} options.webVersion - The version of WhatsApp Web to use. Use options.webVersionCache to configure how the version is retrieved.
24
+ * @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.
22
25
  * @param {number} options.authTimeoutMs - Timeout for authentication selector in puppeteer
23
26
  * @param {object} options.puppeteer - Puppeteer launch options. View docs here: https://github.com/puppeteer/puppeteer/
24
27
  * @param {number} options.qrMaxRetries - How many times should the qrcode be refreshed before giving up
@@ -115,6 +118,7 @@ class Client extends EventEmitter {
115
118
  this.pupPage = page;
116
119
 
117
120
  await this.authStrategy.afterBrowserInitialized();
121
+ await this.initWebVersionCache();
118
122
 
119
123
  await page.goto(WhatsWebURL, {
120
124
  waitUntil: 'load',
@@ -234,9 +238,9 @@ class Client extends EventEmitter {
234
238
  // Listens to qr token change
235
239
  if (mut.type === 'attributes' && mut.attributeName === 'data-ref') {
236
240
  window.qrChanged(mut.target.dataset.ref);
237
- } else
241
+ }
238
242
  // Listens to retry button, when found, click it
239
- if (mut.type === 'childList') {
243
+ else if (mut.type === 'childList') {
240
244
  const retry_button = document.querySelector(selectors.QR_RETRY_BUTTON);
241
245
  if (retry_button) retry_button.click();
242
246
  }
@@ -310,7 +314,7 @@ class Client extends EventEmitter {
310
314
  await page.exposeFunction('onAddMessageEvent', msg => {
311
315
  if (msg.type === 'gp2') {
312
316
  const notification = new GroupNotification(this, msg);
313
- if (msg.subtype === 'add' || msg.subtype === 'invite') {
317
+ if (['add', 'invite', 'linked_group_join'].includes(msg.subtype)) {
314
318
  /**
315
319
  * Emitted when a user joins the chat via invite link or is added by an admin.
316
320
  * @event Client#group_join
@@ -392,18 +396,18 @@ class Client extends EventEmitter {
392
396
 
393
397
  /**
394
398
  * The event notification that is received when one of
395
- * the group participants changes thier phone number.
399
+ * the group participants changes their phone number.
396
400
  */
397
401
  const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify';
398
402
 
399
403
  /**
400
404
  * The event notification that is received when one of
401
- * the contacts changes thier phone number.
405
+ * the contacts changes their phone number.
402
406
  */
403
407
  const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number';
404
408
 
405
409
  if (isParticipant || isContact) {
406
- /** {@link GroupNotification} object does not provide enough information about this event, so a {@link Message} object is used. */
410
+ /** @type {GroupNotification} object does not provide enough information about this event, so a @type {Message} object is used. */
407
411
  const message = new Message(this, msg);
408
412
 
409
413
  const newId = isParticipant ? msg.recipients[0] : msg.to;
@@ -580,12 +584,28 @@ class Client extends EventEmitter {
580
584
  this.emit(Events.CHAT_ARCHIVED, new Chat(this, chat), currState, prevState);
581
585
  });
582
586
 
587
+ await page.exposeFunction('onEditMessageEvent', (msg, newBody, prevBody) => {
588
+
589
+ if(msg.type === 'revoked'){
590
+ return;
591
+ }
592
+ /**
593
+ * Emitted when messages are edited
594
+ * @event Client#message_edit
595
+ * @param {Message} message
596
+ * @param {string} newBody
597
+ * @param {string} prevBody
598
+ */
599
+ this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody);
600
+ });
601
+
583
602
  await page.evaluate(() => {
584
603
  window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(window.WWebJS.getMessageModel(msg)); });
585
604
  window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(window.WWebJS.getMessageModel(msg)); });
586
605
  window.Store.Msg.on('change:ack', (msg, ack) => { window.onMessageAckEvent(window.WWebJS.getMessageModel(msg), ack); });
587
606
  window.Store.Msg.on('change:isUnsentMedia', (msg, unsent) => { if (msg.id.fromMe && !unsent) window.onMessageMediaUploadedEvent(window.WWebJS.getMessageModel(msg)); });
588
607
  window.Store.Msg.on('remove', (msg) => { if (msg.isNewMsg) window.onRemoveMessageEvent(window.WWebJS.getMessageModel(msg)); });
608
+ window.Store.Msg.on('change:body', (msg, newBody, prevBody) => { window.onEditMessageEvent(window.WWebJS.getMessageModel(msg), newBody, prevBody); });
589
609
  window.Store.AppState.on('change:state', (_AppState, state) => { window.onAppStateChangedEvent(state); });
590
610
  window.Store.Conn.on('change:battery', (state) => { window.onBatteryStateChangedEvent(state); });
591
611
  window.Store.Call.on('add', (call) => { window.onIncomingCall(call); });
@@ -638,6 +658,35 @@ class Client extends EventEmitter {
638
658
  });
639
659
  }
640
660
 
661
+ async initWebVersionCache() {
662
+ const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache;
663
+ const webCache = WebCacheFactory.createWebCache(webCacheType, webCacheOptions);
664
+
665
+ const requestedVersion = this.options.webVersion;
666
+ const versionContent = await webCache.resolve(requestedVersion);
667
+
668
+ if(versionContent) {
669
+ await this.pupPage.setRequestInterception(true);
670
+ this.pupPage.on('request', async (req) => {
671
+ if(req.url() === WhatsWebURL) {
672
+ req.respond({
673
+ status: 200,
674
+ contentType: 'text/html',
675
+ body: versionContent
676
+ });
677
+ } else {
678
+ req.continue();
679
+ }
680
+ });
681
+ } else {
682
+ this.pupPage.on('response', async (res) => {
683
+ if(res.ok() && res.url() === WhatsWebURL) {
684
+ await webCache.persist(await res.text());
685
+ }
686
+ });
687
+ }
688
+ }
689
+
641
690
  /**
642
691
  * Closes the client
643
692
  */
@@ -689,6 +738,7 @@ class Client extends EventEmitter {
689
738
  * @property {boolean} [sendVideoAsGif=false] - Send video as gif
690
739
  * @property {boolean} [sendMediaAsSticker=false] - Send media as a sticker
691
740
  * @property {boolean} [sendMediaAsDocument=false] - Send media as a document
741
+ * @property {boolean} [isViewOnce=false] - Send photo/video as a view once message
692
742
  * @property {boolean} [parseVCards=true] - Automatically parse vCards and send them as contacts
693
743
  * @property {string} [caption] - Image or video caption
694
744
  * @property {string} [quotedMessageId] - Id of the message that is being quoted (or replied to)
@@ -699,7 +749,7 @@ class Client extends EventEmitter {
699
749
  * @property {string[]} [stickerCategories=undefined] - Sets the categories of the sticker, (if sendMediaAsSticker is true). Provide emoji char array, can be null.
700
750
  * @property {MessageMedia} [media] - Media to be sent
701
751
  */
702
-
752
+
703
753
  /**
704
754
  * Send a message to a specific chatId
705
755
  * @param {string} chatId
@@ -709,6 +759,10 @@ class Client extends EventEmitter {
709
759
  * @returns {Promise<Message>} Message that was just sent
710
760
  */
711
761
  async sendMessage(chatId, content, options = {}) {
762
+ if (options.mentions && options.mentions.some(possiblyContact => possiblyContact instanceof Contact)) {
763
+ console.warn('Mentions with an array of Contact are now deprecated. See more at https://github.com/pedroslopez/whatsapp-web.js/pull/2166.');
764
+ options.mentions = options.mentions.map(a => a.id._serialized);
765
+ }
712
766
  let internalOptions = {
713
767
  linkPreview: options.linkPreview === false ? undefined : true,
714
768
  sendAudioAsVoice: options.sendAudioAsVoice,
@@ -718,7 +772,7 @@ class Client extends EventEmitter {
718
772
  caption: options.caption,
719
773
  quotedMessageId: options.quotedMessageId,
720
774
  parseVCards: options.parseVCards === false ? false : true,
721
- mentionedJidList: Array.isArray(options.mentions) ? options.mentions.map(contact => contact.id._serialized) : [],
775
+ mentionedJidList: Array.isArray(options.mentions) ? options.mentions : [],
722
776
  extraOptions: options.extra
723
777
  };
724
778
 
@@ -726,10 +780,12 @@ class Client extends EventEmitter {
726
780
 
727
781
  if (content instanceof MessageMedia) {
728
782
  internalOptions.attachment = content;
783
+ internalOptions.isViewOnce = options.isViewOnce,
729
784
  content = '';
730
785
  } else if (options.media instanceof MessageMedia) {
731
786
  internalOptions.attachment = options.media;
732
787
  internalOptions.caption = content;
788
+ internalOptions.isViewOnce = options.isViewOnce,
733
789
  content = '';
734
790
  } else if (content instanceof Location) {
735
791
  internalOptions.location = content;
@@ -748,7 +804,7 @@ class Client extends EventEmitter {
748
804
  internalOptions.list = content;
749
805
  content = '';
750
806
  }
751
-
807
+
752
808
  if (internalOptions.sendMediaAsSticker && internalOptions.attachment) {
753
809
  internalOptions.attachment = await Util.formatToWebpSticker(
754
810
  internalOptions.attachment, {
@@ -774,7 +830,7 @@ class Client extends EventEmitter {
774
830
 
775
831
  return new Message(this, newMessage);
776
832
  }
777
-
833
+
778
834
  /**
779
835
  * Searches for messages
780
836
  * @param {string} query
@@ -842,6 +898,24 @@ class Client extends EventEmitter {
842
898
 
843
899
  return ContactFactory.create(this, contact);
844
900
  }
901
+
902
+ async getMessageById(messageId) {
903
+ const msg = await this.pupPage.evaluate(async messageId => {
904
+ let msg = window.Store.Msg.get(messageId);
905
+ if(msg) return window.WWebJS.getMessageModel(msg);
906
+
907
+ const params = messageId.split('_');
908
+ if(params.length !== 3) throw new Error('Invalid serialized message id specified');
909
+
910
+ let messagesObject = await window.Store.Msg.getMessagesById([messageId]);
911
+ if (messagesObject && messagesObject.messages.length) msg = messagesObject.messages[0];
912
+
913
+ if(msg) return window.WWebJS.getMessageModel(msg);
914
+ }, messageId);
915
+
916
+ if(msg) return new Message(this, msg);
917
+ return null;
918
+ }
845
919
 
846
920
  /**
847
921
  * Returns an object with information about the invite code's group
@@ -877,7 +951,8 @@ class Client extends EventEmitter {
877
951
  if (inviteInfo.inviteCodeExp == 0) throw 'Expired invite code';
878
952
  return this.pupPage.evaluate(async inviteInfo => {
879
953
  let { groupId, fromId, inviteCode, inviteCodeExp } = inviteInfo;
880
- return await window.Store.JoinInviteV4.sendJoinGroupViaInviteV4(inviteCode, String(inviteCodeExp), groupId, fromId);
954
+ let userWid = window.Store.WidFactory.createWid(fromId);
955
+ return await window.Store.JoinInviteV4.joinGroupViaInviteV4(inviteCode, String(inviteCodeExp), groupId, userWid);
881
956
  }, inviteInfo);
882
957
  }
883
958
 
@@ -1279,6 +1354,35 @@ class Client extends EventEmitter {
1279
1354
 
1280
1355
  return success;
1281
1356
  }
1357
+
1358
+ /**
1359
+ * Change labels in chats
1360
+ * @param {Array<number|string>} labelIds
1361
+ * @param {Array<string>} chatIds
1362
+ * @returns {Promise<void>}
1363
+ */
1364
+ async addOrRemoveLabels(labelIds, chatIds) {
1365
+
1366
+ return this.pupPage.evaluate(async (labelIds, chatIds) => {
1367
+ if (['smba', 'smbi'].indexOf(window.Store.Conn.platform) === -1) {
1368
+ throw '[LT01] Only Whatsapp business';
1369
+ }
1370
+ const labels = window.WWebJS.getLabels().filter(e => labelIds.find(l => l == e.id) !== undefined);
1371
+ const chats = window.Store.Chat.filter(e => chatIds.includes(e.id._serialized));
1372
+
1373
+ let actions = labels.map(label => ({id: label.id, type: 'add'}));
1374
+
1375
+ chats.forEach(chat => {
1376
+ (chat.labels || []).forEach(n => {
1377
+ if (!actions.find(e => e.id == n)) {
1378
+ actions.push({id: n, type: 'remove'});
1379
+ }
1380
+ });
1381
+ });
1382
+
1383
+ return await window.Store.Label.addOrRemoveLabels(actions, chats);
1384
+ }, labelIds, chatIds);
1385
+ }
1282
1386
  }
1283
1387
 
1284
1388
  module.exports = Client;
@@ -261,6 +261,15 @@ class Chat extends Base {
261
261
  async getLabels() {
262
262
  return this.client.getChatLabels(this.id._serialized);
263
263
  }
264
+
265
+ /**
266
+ * Add or remove labels to this Chat
267
+ * @param {Array<number|string>} labelIds
268
+ * @returns {Promise<void>}
269
+ */
270
+ async changeLabels(labelIds) {
271
+ return this.client.addOrRemoveLabels(labelIds, [this.id._serialized]);
272
+ }
264
273
  }
265
274
 
266
275
  module.exports = Chat;
@@ -156,9 +156,10 @@ class Contact extends Base {
156
156
 
157
157
  await this.client.pupPage.evaluate(async (contactId) => {
158
158
  const contact = window.Store.Contact.get(contactId);
159
- await window.Store.BlockContact.blockContact(contact);
159
+ await window.Store.BlockContact.blockContact({contact});
160
160
  }, this.id._serialized);
161
161
 
162
+ this.isBlocked = true;
162
163
  return true;
163
164
  }
164
165
 
@@ -174,6 +175,7 @@ class Contact extends Base {
174
175
  await window.Store.BlockContact.unblockContact(contact);
175
176
  }, this.id._serialized);
176
177
 
178
+ this.isBlocked = false;
177
179
  return true;
178
180
  }
179
181
 
@@ -7,6 +7,7 @@ const Order = require('./Order');
7
7
  const Payment = require('./Payment');
8
8
  const Reaction = require('./Reaction');
9
9
  const {MessageTypes} = require('../util/Constants');
10
+ const {Contact} = require('./Contact');
10
11
 
11
12
  /**
12
13
  * Represents a Message on WhatsApp
@@ -167,8 +168,8 @@ class Message extends Base {
167
168
  inviteCodeExp: data.inviteCodeExp,
168
169
  groupId: data.inviteGrp,
169
170
  groupName: data.inviteGrpName,
170
- fromId: data.from._serialized,
171
- toId: data.to._serialized
171
+ fromId: data.from?._serialized ? data.from._serialized : data.from,
172
+ toId: data.to?._serialized ? data.to._serialized : data.to
172
173
  } : undefined;
173
174
 
174
175
  /**
@@ -224,6 +225,16 @@ class Message extends Base {
224
225
  this.productId = data.productId;
225
226
  }
226
227
 
228
+ /** Last edit time */
229
+ if (data.latestEditSenderTimestampMs) {
230
+ this.latestEditSenderTimestampMs = data.latestEditSenderTimestampMs;
231
+ }
232
+
233
+ /** Last edit message author */
234
+ if (data.latestEditMsgKey) {
235
+ this.latestEditMsgKey = data.latestEditMsgKey;
236
+ }
237
+
227
238
  /**
228
239
  * Links included in the message.
229
240
  * @type {Array<{link: string, isSuspicious: boolean}>}
@@ -576,6 +587,42 @@ class Message extends Base {
576
587
  return reaction;
577
588
  });
578
589
  }
590
+
591
+ /**
592
+ * Edits the current message.
593
+ * @param {string} content
594
+ * @param {MessageEditOptions} [options] - Options used when editing the message
595
+ * @returns {Promise<?Message>}
596
+ */
597
+ async edit(content, options = {}) {
598
+ if (options.mentions && options.mentions.some(possiblyContact => possiblyContact instanceof Contact)) {
599
+ options.mentions = options.mentions.map(a => a.id._serialized);
600
+ }
601
+ let internalOptions = {
602
+ linkPreview: options.linkPreview === false ? undefined : true,
603
+ mentionedJidList: Array.isArray(options.mentions) ? options.mentions : [],
604
+ extraOptions: options.extra
605
+ };
606
+
607
+ if (!this.fromMe) {
608
+ return null;
609
+ }
610
+ const messageEdit = await this.client.pupPage.evaluate(async (msgId, message, options) => {
611
+ let msg = window.Store.Msg.get(msgId);
612
+ if (!msg) return null;
613
+
614
+ let catEdit = (msg.type === 'chat' && window.Store.MsgActionChecks.canEditText(msg));
615
+ if (catEdit) {
616
+ const msgEdit = await window.WWebJS.editMessage(msg, message, options);
617
+ return msgEdit.serialize();
618
+ }
619
+ return null;
620
+ }, this.id._serialized, content, internalOptions);
621
+ if (messageEdit) {
622
+ return new Message(this.client, messageEdit);
623
+ }
624
+ return null;
625
+ }
579
626
  }
580
627
 
581
628
  module.exports = Message;
@@ -7,6 +7,10 @@ exports.DefaultOptions = {
7
7
  headless: true,
8
8
  defaultViewport: null
9
9
  },
10
+ webVersion: '2.2322.15',
11
+ webVersionCache: {
12
+ type: 'local',
13
+ },
10
14
  authTimeoutMs: 0,
11
15
  qrMaxRetries: 0,
12
16
  takeoverOnConflict: false,
@@ -44,6 +48,7 @@ exports.Events = {
44
48
  MESSAGE_REVOKED_EVERYONE: 'message_revoke_everyone',
45
49
  MESSAGE_REVOKED_ME: 'message_revoke_me',
46
50
  MESSAGE_ACK: 'message_ack',
51
+ MESSAGE_EDIT: 'message_edit',
47
52
  UNREAD_COUNT: 'unread_count',
48
53
  MESSAGE_REACTION: 'message_reaction',
49
54
  MEDIA_UPLOADED: 'media_uploaded',
@@ -31,8 +31,11 @@ exports.ExposeStore = (moduleRaidStr) => {
31
31
  window.Store.SendClear = window.mR.findModule('sendClear')[0];
32
32
  window.Store.SendDelete = window.mR.findModule('sendDelete')[0];
33
33
  window.Store.SendMessage = window.mR.findModule('addAndSendMsgToChat')[0];
34
+ window.Store.EditMessage = window.mR.findModule('addAndSendMessageEdit')[0];
34
35
  window.Store.SendSeen = window.mR.findModule('sendSeen')[0];
35
36
  window.Store.User = window.mR.findModule('getMaybeMeUser')[0];
37
+ window.Store.ContactMethods = window.mR.findModule('getUserid')[0];
38
+ window.Store.BusinessProfileCollection = window.mR.findModule('BusinessProfileCollection')[0].BusinessProfileCollection;
36
39
  window.Store.UploadUtils = window.mR.findModule((module) => (module.default && module.default.encryptAndUpload) ? module.default : null)[0].default;
37
40
  window.Store.UserConstructor = window.mR.findModule((module) => (module.default && module.default.prototype && module.default.prototype.isServer && module.default.prototype.isUser) ? module.default : null)[0].default;
38
41
  window.Store.Validators = window.mR.findModule('findLinks')[0];
@@ -42,7 +45,7 @@ exports.ExposeStore = (moduleRaidStr) => {
42
45
  window.Store.PresenceUtils = window.mR.findModule('sendPresenceAvailable')[0];
43
46
  window.Store.ChatState = window.mR.findModule('sendChatStateComposing')[0];
44
47
  window.Store.GroupParticipants = window.mR.findModule('promoteParticipants')[0];
45
- window.Store.JoinInviteV4 = window.mR.findModule('sendJoinGroupViaInviteV4')[0];
48
+ window.Store.JoinInviteV4 = window.mR.findModule('queryGroupInviteV4')[0];
46
49
  window.Store.findCommonGroups = window.mR.findModule('findCommonGroups')[0].findCommonGroups;
47
50
  window.Store.StatusUtils = window.mR.findModule('setMyStatus')[0];
48
51
  window.Store.ConversationMsgs = window.mR.findModule('loadEarlierMsgs')[0];
@@ -119,8 +122,12 @@ exports.LoadUtils = () => {
119
122
  forceDocument: options.sendMediaAsDocument,
120
123
  forceGif: options.sendVideoAsGif
121
124
  });
122
-
125
+
126
+ if (options.caption){
127
+ attOptions.caption = options.caption;
128
+ }
123
129
  content = options.sendMediaAsSticker ? undefined : attOptions.preview;
130
+ attOptions.isViewOnce = options.isViewOnce;
124
131
 
125
132
  delete options.attachment;
126
133
  delete options.sendMediaAsSticker;
@@ -226,8 +233,8 @@ exports.LoadUtils = () => {
226
233
  }
227
234
 
228
235
  let listOptions = {};
229
- if(options.list){
230
- if(window.Store.Conn.platform === 'smba' || window.Store.Conn.platform === 'smbi'){
236
+ if (options.list) {
237
+ if (window.Store.Conn.platform === 'smba' || window.Store.Conn.platform === 'smbi') {
231
238
  throw '[LT01] Whatsapp business can\'t send this yet';
232
239
  }
233
240
  listOptions = {
@@ -286,6 +293,40 @@ exports.LoadUtils = () => {
286
293
  await window.Store.SendMessage.addAndSendMsgToChat(chat, message);
287
294
  return window.Store.Msg.get(newMsgId._serialized);
288
295
  };
296
+
297
+ window.WWebJS.editMessage = async (msg, content, options = {}) => {
298
+
299
+ const extraOptions = options.extraOptions || {};
300
+ delete options.extraOptions;
301
+
302
+ if (options.mentionedJidList) {
303
+ options.mentionedJidList = options.mentionedJidList.map(cId => window.Store.Contact.get(cId).id);
304
+ }
305
+
306
+ if (options.linkPreview) {
307
+ options.linkPreview = null;
308
+
309
+ // Not supported yet by WhatsApp Web on MD
310
+ if(!window.Store.MDBackend) {
311
+ const link = window.Store.Validators.findLink(content);
312
+ if (link) {
313
+ const preview = await window.Store.Wap.queryLinkPreview(link.url);
314
+ preview.preview = true;
315
+ preview.subtype = 'url';
316
+ options = { ...options, ...preview };
317
+ }
318
+ }
319
+ }
320
+
321
+
322
+ const internalOptions = {
323
+ ...options,
324
+ ...extraOptions
325
+ };
326
+
327
+ await window.Store.EditMessage.sendMessageEdit(msg, content, internalOptions);
328
+ return window.Store.Msg.get(msg.id._serialized);
329
+ };
289
330
 
290
331
  window.WWebJS.toStickerData = async (mediaInfo) => {
291
332
  if (mediaInfo.mimetype == 'image/webp') return mediaInfo;
@@ -434,7 +475,7 @@ exports.LoadUtils = () => {
434
475
 
435
476
  res.lastMessage = null;
436
477
  if (res.msgs && res.msgs.length) {
437
- const lastMessage = window.Store.Msg.get(chat.lastReceivedKey._serialized);
478
+ const lastMessage = chat.lastReceivedKey ? window.Store.Msg.get(chat.lastReceivedKey._serialized) : null;
438
479
  if (lastMessage) {
439
480
  res.lastMessage = window.WWebJS.getMessageModel(lastMessage);
440
481
  }
@@ -462,19 +503,56 @@ exports.LoadUtils = () => {
462
503
 
463
504
  window.WWebJS.getContactModel = contact => {
464
505
  let res = contact.serialize();
465
- res.isBusiness = contact.isBusiness;
506
+ res.isBusiness = contact.isBusiness === undefined ? false : contact.isBusiness;
466
507
 
467
508
  if (contact.businessProfile) {
468
509
  res.businessProfile = contact.businessProfile.serialize();
469
510
  }
470
511
 
471
- res.isMe = contact.isMe;
472
- res.isUser = contact.isUser;
473
- res.isGroup = contact.isGroup;
474
- res.isWAContact = contact.isWAContact;
475
- res.isMyContact = contact.isMyContact;
512
+ // TODO: remove useOldImplementation and its checks once all clients are updated to >= v2.2327.4
513
+ const useOldImplementation
514
+ = window.WWebJS.compareWwebVersions(window.Debug.VERSION, '<', '2.2327.4');
515
+
516
+ res.isMe = useOldImplementation
517
+ ? contact.isMe
518
+ : window.Store.ContactMethods.getIsMe(contact);
519
+ res.isUser = useOldImplementation
520
+ ? contact.isUser
521
+ : window.Store.ContactMethods.getIsUser(contact);
522
+ res.isGroup = useOldImplementation
523
+ ? contact.isGroup
524
+ : window.Store.ContactMethods.getIsGroup(contact);
525
+ res.isWAContact = useOldImplementation
526
+ ? contact.isWAContact
527
+ : window.Store.ContactMethods.getIsWAContact(contact);
528
+ res.isMyContact = useOldImplementation
529
+ ? contact.isMyContact
530
+ : window.Store.ContactMethods.getIsMyContact(contact);
476
531
  res.isBlocked = contact.isContactBlocked;
477
- res.userid = contact.userid;
532
+ res.userid = useOldImplementation
533
+ ? contact.userid
534
+ : window.Store.ContactMethods.getUserid(contact);
535
+ res.isEnterprise = useOldImplementation
536
+ ? contact.isEnterprise
537
+ : window.Store.ContactMethods.getIsEnterprise(contact);
538
+ res.verifiedName = useOldImplementation
539
+ ? contact.verifiedName
540
+ : window.Store.ContactMethods.getVerifiedName(contact);
541
+ res.verifiedLevel = useOldImplementation
542
+ ? contact.verifiedLevel
543
+ : window.Store.ContactMethods.getVerifiedLevel(contact);
544
+ res.statusMute = useOldImplementation
545
+ ? contact.statusMute
546
+ : window.Store.ContactMethods.getStatusMute(contact);
547
+ res.name = useOldImplementation
548
+ ? contact.name
549
+ : window.Store.ContactMethods.getName(contact);
550
+ res.shortName = useOldImplementation
551
+ ? contact.shortName
552
+ : window.Store.ContactMethods.getShortName(contact);
553
+ res.pushname = useOldImplementation
554
+ ? contact.pushname
555
+ : window.Store.ContactMethods.getPushname(contact);
478
556
 
479
557
  return res;
480
558
  };
@@ -482,6 +560,8 @@ exports.LoadUtils = () => {
482
560
  window.WWebJS.getContact = async contactId => {
483
561
  const wid = window.Store.WidFactory.createWid(contactId);
484
562
  const contact = await window.Store.Contact.find(wid);
563
+ const bizProfile = await window.Store.BusinessProfileCollection.fetchBizProfile(wid);
564
+ bizProfile.profileOptions && (contact.businessProfile = bizProfile);
485
565
  return window.WWebJS.getContactModel(contact);
486
566
  };
487
567
 
@@ -707,4 +787,46 @@ exports.LoadUtils = () => {
707
787
  throw err;
708
788
  }
709
789
  };
790
+
791
+ /**
792
+ * Inner function that compares between two WWeb versions. Its purpose is to help the developer to choose the correct code implementation depending on the comparison value and the WWeb version.
793
+ * @param {string} lOperand The left operand for the WWeb version string to compare with
794
+ * @param {string} operator The comparison operator
795
+ * @param {string} rOperand The right operand for the WWeb version string to compare with
796
+ * @returns {boolean} Boolean value that indicates the result of the comparison
797
+ */
798
+ window.WWebJS.compareWwebVersions = (lOperand, operator, rOperand) => {
799
+ if (!['>', '>=', '<', '<=', '='].includes(operator)) {
800
+ throw class _ extends Error {
801
+ constructor(m) { super(m); this.name = 'CompareWwebVersionsError'; }
802
+ }('Invalid comparison operator is provided');
803
+
804
+ }
805
+ if (typeof lOperand !== 'string' || typeof rOperand !== 'string') {
806
+ throw class _ extends Error {
807
+ constructor(m) { super(m); this.name = 'CompareWwebVersionsError'; }
808
+ }('A non-string WWeb version type is provided');
809
+ }
810
+
811
+ lOperand = lOperand.replace(/-beta$/, '');
812
+ rOperand = rOperand.replace(/-beta$/, '');
813
+
814
+ while (lOperand.length !== rOperand.length) {
815
+ lOperand.length > rOperand.length
816
+ ? rOperand = rOperand.concat('0')
817
+ : lOperand = lOperand.concat('0');
818
+ }
819
+
820
+ lOperand = Number(lOperand.replace(/\./g, ''));
821
+ rOperand = Number(rOperand.replace(/\./g, ''));
822
+
823
+ return (
824
+ operator === '>' ? lOperand > rOperand :
825
+ operator === '>=' ? lOperand >= rOperand :
826
+ operator === '<' ? lOperand < rOperand :
827
+ operator === '<=' ? lOperand <= rOperand :
828
+ operator === '=' ? lOperand === rOperand :
829
+ false
830
+ );
831
+ };
710
832
  };
@@ -0,0 +1,43 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ const { WebCache, VersionResolveError } = require('./WebCache');
5
+
6
+ /**
7
+ * LocalWebCache - Fetches a WhatsApp Web version from a local file store
8
+ * @param {object} options - options
9
+ * @param {string} options.path - Path to the directory where cached versions are saved, default is: "./.wwebjs_cache/"
10
+ * @param {boolean} options.strict - If true, will throw an error if the requested version can't be fetched. If false, will resolve to the latest version.
11
+ */
12
+ class LocalWebCache extends WebCache {
13
+ constructor(options = {}) {
14
+ super();
15
+
16
+ this.path = options.path || './.wwebjs_cache/';
17
+ this.strict = options.strict || false;
18
+ }
19
+
20
+ async resolve(version) {
21
+ const filePath = path.join(this.path, `${version}.html`);
22
+
23
+ try {
24
+ return fs.readFileSync(filePath, 'utf-8');
25
+ }
26
+ catch (err) {
27
+ if (this.strict) throw new VersionResolveError(`Couldn't load version ${version} from the cache`);
28
+ return null;
29
+ }
30
+ }
31
+
32
+ async persist(indexHtml) {
33
+ // extract version from index (e.g. manifest-2.2206.9.json -> 2.2206.9)
34
+ const version = indexHtml.match(/manifest-([\d\\.]+)\.json/)[1];
35
+ if(!version) return;
36
+
37
+ const filePath = path.join(this.path, `${version}.html`);
38
+ fs.mkdirSync(this.path, { recursive: true });
39
+ fs.writeFileSync(filePath, indexHtml);
40
+ }
41
+ }
42
+
43
+ module.exports = LocalWebCache;
@@ -0,0 +1,40 @@
1
+ const fetch = require('node-fetch');
2
+ const { WebCache, VersionResolveError } = require('./WebCache');
3
+
4
+ /**
5
+ * RemoteWebCache - Fetches a WhatsApp Web version index from a remote server
6
+ * @param {object} options - options
7
+ * @param {string} options.remotePath - Endpoint that should be used to fetch the version index. Use {version} as a placeholder for the version number.
8
+ * @param {boolean} options.strict - If true, will throw an error if the requested version can't be fetched. If false, will resolve to the latest version. Defaults to false.
9
+ */
10
+ class RemoteWebCache extends WebCache {
11
+ constructor(options = {}) {
12
+ super();
13
+
14
+ if (!options.remotePath) throw new Error('webVersionCache.remotePath is required when using the remote cache');
15
+ this.remotePath = options.remotePath;
16
+ this.strict = options.strict || false;
17
+ }
18
+
19
+ async resolve(version) {
20
+ const remotePath = this.remotePath.replace('{version}', version);
21
+
22
+ try {
23
+ const cachedRes = await fetch(remotePath);
24
+ if (cachedRes.ok) {
25
+ return cachedRes.text();
26
+ }
27
+ } catch (err) {
28
+ console.error(`Error fetching version ${version} from remote`, err);
29
+ }
30
+
31
+ if (this.strict) throw new VersionResolveError(`Couldn't load version ${version} from the archive`);
32
+ return null;
33
+ }
34
+
35
+ async persist() {
36
+ // Nothing to do here
37
+ }
38
+ }
39
+
40
+ module.exports = RemoteWebCache;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Default implementation of a web version cache that does nothing.
3
+ */
4
+ class WebCache {
5
+ async resolve() { return null; }
6
+ async persist() { }
7
+ }
8
+
9
+ class VersionResolveError extends Error { }
10
+
11
+ module.exports = {
12
+ WebCache,
13
+ VersionResolveError
14
+ };
@@ -0,0 +1,20 @@
1
+ const RemoteWebCache = require('./RemoteWebCache');
2
+ const LocalWebCache = require('./LocalWebCache');
3
+ const { WebCache } = require('./WebCache');
4
+
5
+ const createWebCache = (type, options) => {
6
+ switch (type) {
7
+ case 'remote':
8
+ return new RemoteWebCache(options);
9
+ case 'local':
10
+ return new LocalWebCache(options);
11
+ case 'none':
12
+ return new WebCache();
13
+ default:
14
+ throw new Error(`Invalid WebCache type ${type}`);
15
+ }
16
+ };
17
+
18
+ module.exports = {
19
+ createWebCache,
20
+ };