whatsapp-web.js 1.16.7 → 1.18.0-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.2220.8](https://img.shields.io/badge/WhatsApp_Web-2.2220.8-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.2224.8](https://img.shields.io/badge/WhatsApp_Web-2.2224.8-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
@@ -80,6 +80,7 @@ For more information on saving and restoring sessions, check out the available [
80
80
  | Get contact info | ✅ |
81
81
  | Get profile pictures | ✅ |
82
82
  | Set user status message | ✅ |
83
+ | React to messages | ✅ |
83
84
 
84
85
  Something missing? Make an issue and let us know!
85
86
 
package/example.js CHANGED
@@ -1,4 +1,4 @@
1
- const { Client, Location, List, Buttons, LocalAuth } = require('./index');
1
+ const { Client, Location, List, Buttons, LocalAuth} = require('./index');
2
2
 
3
3
  const client = new Client({
4
4
  authStrategy: new LocalAuth(),
@@ -7,6 +7,10 @@ const client = new Client({
7
7
 
8
8
  client.initialize();
9
9
 
10
+ client.on('loading_screen', (percent, message) => {
11
+ console.log('LOADING SCREEN', percent, message);
12
+ });
13
+
10
14
  client.on('qr', (qr) => {
11
15
  // NOTE: This event will not be fired if a session is specified.
12
16
  console.log('QR RECEIVED', qr);
@@ -191,6 +195,8 @@ client.on('message', async msg => {
191
195
  let sections = [{title:'sectionTitle',rows:[{title:'ListItem1', description: 'desc'},{title:'ListItem2'}]}];
192
196
  let list = new List('List body','btnText',sections,'Title','footer');
193
197
  client.sendMessage(msg.from, list);
198
+ } else if (msg.body === '!reaction') {
199
+ msg.react('👍');
194
200
  }
195
201
  });
196
202
 
package/index.d.ts CHANGED
@@ -114,7 +114,7 @@ declare namespace WAWebJS {
114
114
 
115
115
  /** Send a message to a specific chatId */
116
116
  sendMessage(chatId: string, content: MessageContent, options?: MessageSendOptions): Promise<Message>
117
-
117
+
118
118
  /** Searches for messages */
119
119
  searchMessages(query: string, options?: { chatId?: string, page?: number, limit?: number }): Promise<Message[]>
120
120
 
@@ -141,7 +141,7 @@ declare namespace WAWebJS {
141
141
  * @param displayName New display name
142
142
  */
143
143
  setDisplayName(displayName: string): Promise<boolean>
144
-
144
+
145
145
  /** Changes and returns the archive state of the Chat */
146
146
  unarchiveChat(chatId: string): Promise<boolean>
147
147
 
@@ -241,6 +241,15 @@ declare namespace WAWebJS {
241
241
  message: Message
242
242
  ) => void): this
243
243
 
244
+ /** Emitted when a reaction is sent, received, updated or removed */
245
+ on(event: 'message_reaction', listener: (
246
+ /** The reaction object */
247
+ reaction: Reaction
248
+ ) => void): this
249
+
250
+ /** Emitted when loading screen is appearing */
251
+ on(event: 'loading_screen', listener: (percent: string, message: string) => void): this
252
+
244
253
  /** Emitted when the QR code is received */
245
254
  on(event: 'qr', listener: (
246
255
  /** qr code string
@@ -256,6 +265,9 @@ declare namespace WAWebJS {
256
265
 
257
266
  /** Emitted when the client has initialized and is ready to receive messages */
258
267
  on(event: 'ready', listener: () => void): this
268
+
269
+ /** Emitted when the RemoteAuth session is saved successfully on the external Database */
270
+ on(event: 'remote_session_saved', listener: () => void): this
259
271
  }
260
272
 
261
273
  /** Current connection information */
@@ -345,6 +357,9 @@ declare namespace WAWebJS {
345
357
  failureEventPayload?: any
346
358
  }>;
347
359
  getAuthEventPayload: () => Promise<any>;
360
+ afterAuthReady: () => Promise<void>;
361
+ disconnect: () => Promise<void>;
362
+ destroy: () => Promise<void>;
348
363
  logout: () => Promise<void>;
349
364
  }
350
365
 
@@ -365,6 +380,30 @@ declare namespace WAWebJS {
365
380
  dataPath?: string
366
381
  })
367
382
  }
383
+
384
+ /**
385
+ * Remote-based authentication
386
+ */
387
+ export class RemoteAuth extends AuthStrategy {
388
+ public clientId?: string;
389
+ public dataPath?: string;
390
+ constructor(options?: {
391
+ store: Store,
392
+ clientId?: string,
393
+ dataPath?: string,
394
+ backupSyncIntervalMs: number
395
+ })
396
+ }
397
+
398
+ /**
399
+ * Remote store interface
400
+ */
401
+ export interface Store {
402
+ sessionExists: ({session: string}) => Promise<boolean> | boolean,
403
+ delete: ({session: string}) => Promise<any> | any,
404
+ save: ({session: string}) => Promise<any> | any,
405
+ extract: ({session: string, path: string}) => Promise<any> | any,
406
+ }
368
407
 
369
408
  /**
370
409
  * Legacy session auth strategy
@@ -463,9 +502,11 @@ declare namespace WAWebJS {
463
502
  GROUP_LEAVE = 'group_leave',
464
503
  GROUP_UPDATE = 'group_update',
465
504
  QR_RECEIVED = 'qr',
505
+ LOADING_SCREEN = 'loading_screen',
466
506
  DISCONNECTED = 'disconnected',
467
507
  STATE_CHANGED = 'change_state',
468
508
  BATTERY_CHANGED = 'change_battery',
509
+ REMOTE_SESSION_SAVED = 'remote_session_saved'
469
510
  }
470
511
 
471
512
  /** Group notification types */
@@ -604,6 +645,8 @@ declare namespace WAWebJS {
604
645
  ack: MessageAck,
605
646
  /** If the message was sent to a group, this field will contain the user that sent the message. */
606
647
  author?: string,
648
+ /** String that represents from which device type the message was sent */
649
+ deviceType: string,
607
650
  /** Message content */
608
651
  body: string,
609
652
  /** Indicates if the message was a broadcast */
@@ -687,7 +730,7 @@ declare namespace WAWebJS {
687
730
  acceptGroupV4Invite: () => Promise<{status: number}>,
688
731
  /** Deletes the message from the chat */
689
732
  delete: (everyone?: boolean) => Promise<void>,
690
- /** Downloads and returns the attatched message media */
733
+ /** Downloads and returns the attached message media */
691
734
  downloadMedia: () => Promise<MessageMedia>,
692
735
  /** Returns the Chat this message was sent in */
693
736
  getChat: () => Promise<Chat>,
@@ -703,15 +746,17 @@ declare namespace WAWebJS {
703
746
  * If not, it will send the message in the same Chat as the original message was sent.
704
747
  */
705
748
  reply: (content: MessageContent, chatId?: string, options?: MessageSendOptions) => Promise<Message>,
749
+ /** React to this message with an emoji*/
750
+ react: (reaction: string) => Promise<void>,
706
751
  /**
707
- * Forwards this message to another chat
752
+ * Forwards this message to another chat (that you chatted before, otherwise it will fail)
708
753
  */
709
754
  forward: (chat: Chat | string) => Promise<void>,
710
755
  /** Star this message */
711
756
  star: () => Promise<void>,
712
757
  /** Unstar this message */
713
758
  unstar: () => Promise<void>,
714
- /** Get information about message delivery statuso */
759
+ /** Get information about message delivery status */
715
760
  getInfo: () => Promise<MessageInfo | null>,
716
761
  /**
717
762
  * Gets the order associated with a given message
@@ -801,13 +846,16 @@ declare namespace WAWebJS {
801
846
  data: string
802
847
  /** Document file name. Value can be null */
803
848
  filename?: string | null
849
+ /** Document file size in bytes. Value can be null. */
850
+ filesize?: number | null
804
851
 
805
852
  /**
806
853
  * @param {string} mimetype MIME type of the attachment
807
854
  * @param {string} data Base64-encoded data of the file
808
855
  * @param {?string} filename Document file name. Value can be null
856
+ * @param {?number} filesize Document file size in bytes. Value can be null.
809
857
  */
810
- constructor(mimetype: string, data: string, filename?: string | null)
858
+ constructor(mimetype: string, data: string, filename?: string | null, filesize?: number | null)
811
859
 
812
860
  /** Creates a MessageMedia instance from a local file path */
813
861
  static fromFilePath: (filePath: string) => MessageMedia
@@ -816,7 +864,7 @@ declare namespace WAWebJS {
816
864
  static fromUrl: (url: string, options?: MediaFromURLOptions) => Promise<MessageMedia>
817
865
  }
818
866
 
819
- export type MessageContent = string | MessageMedia | Location | Contact | Contact[] | List | Buttons
867
+ export type MessageContent = string | MessageMedia | Location | Contact | Contact[] | List | Buttons
820
868
 
821
869
  /**
822
870
  * Represents a Contact on WhatsApp
@@ -1014,6 +1062,10 @@ declare namespace WAWebJS {
1014
1062
  * Set this to Infinity to load all messages.
1015
1063
  */
1016
1064
  limit?: number
1065
+ /**
1066
+ * Return only messages from the bot number or vise versa. To get all messages, leave the option undefined.
1067
+ */
1068
+ fromMe?: boolean
1017
1069
  }
1018
1070
 
1019
1071
  /**
@@ -1287,7 +1339,7 @@ declare namespace WAWebJS {
1287
1339
  constructor(body: string, buttonText: string, sections: Array<any>, title?: string | null, footer?: string | null)
1288
1340
  }
1289
1341
 
1290
- /** Message type buttons */
1342
+ /** Message type Buttons */
1291
1343
  export class Buttons {
1292
1344
  body: string | MessageMedia
1293
1345
  buttons: Array<{ buttonId: string; buttonText: {displayText: string}; type: number }>
@@ -1296,6 +1348,19 @@ declare namespace WAWebJS {
1296
1348
 
1297
1349
  constructor(body: string, buttons: Array<{ id?: string; body: string }>, title?: string | null, footer?: string | null)
1298
1350
  }
1351
+
1352
+ /** Message type Reaction */
1353
+ export class Reaction {
1354
+ id: MessageId
1355
+ orphan: number
1356
+ orphanReason?: string
1357
+ timestamp: number
1358
+ reaction: string
1359
+ read: boolean
1360
+ msgId: MessageId
1361
+ senderId: string
1362
+ ack?: number
1363
+ }
1299
1364
  }
1300
1365
 
1301
1366
  export = WAWebJS
package/index.js CHANGED
@@ -25,6 +25,7 @@ module.exports = {
25
25
  // Auth Strategies
26
26
  NoAuth: require('./src/authStrategies/NoAuth'),
27
27
  LocalAuth: require('./src/authStrategies/LocalAuth'),
28
+ RemoteAuth: require('./src/authStrategies/RemoteAuth'),
28
29
  LegacySessionAuth: require('./src/authStrategies/LegacySessionAuth'),
29
30
 
30
31
  ...Constants
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whatsapp-web.js",
3
- "version": "1.16.7",
3
+ "version": "1.18.0-alpha.0",
4
4
  "description": "Library for interacting with the WhatsApp Web API ",
5
5
  "main": "./index.js",
6
6
  "typings": "./index.d.ts",
@@ -51,5 +51,10 @@
51
51
  },
52
52
  "engines": {
53
53
  "node": ">=12.0.0"
54
+ },
55
+ "optionalDependencies": {
56
+ "archiver": "^5.3.1",
57
+ "fs-extra": "^10.1.0",
58
+ "unzipper": "^0.10.11"
54
59
  }
55
60
  }
package/src/Client.js CHANGED
@@ -10,7 +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 { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List } = require('./structures');
13
+ const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List, Reaction } = require('./structures');
14
14
  const LegacySessionAuth = require('./authStrategies/LegacySessionAuth');
15
15
  const NoAuth = require('./authStrategies/NoAuth');
16
16
 
@@ -92,7 +92,12 @@ class Client extends EventEmitter {
92
92
  browser = await puppeteer.connect(puppeteerOpts);
93
93
  page = await browser.newPage();
94
94
  } else {
95
- browser = await puppeteer.launch(puppeteerOpts);
95
+ const browserArgs = [...(puppeteerOpts.args || [])];
96
+ if(!browserArgs.find(arg => arg.includes('--user-agent'))) {
97
+ browserArgs.push(`--user-agent=${this.options.userAgent}`);
98
+ }
99
+
100
+ browser = await puppeteer.launch({...puppeteerOpts, args: browserArgs});
96
101
  page = (await browser.pages())[0];
97
102
  }
98
103
 
@@ -110,6 +115,52 @@ class Client extends EventEmitter {
110
115
  referer: 'https://whatsapp.com/'
111
116
  });
112
117
 
118
+ await page.evaluate(`function getElementByXpath(path) {
119
+ return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
120
+ }`);
121
+
122
+ let lastPercent = null,
123
+ lastPercentMessage = null;
124
+
125
+ await page.exposeFunction('loadingScreen', async (percent, message) => {
126
+ if (lastPercent !== percent || lastPercentMessage !== message) {
127
+ this.emit(Events.LOADING_SCREEN, percent, message);
128
+ lastPercent = percent;
129
+ lastPercentMessage = message;
130
+ }
131
+ });
132
+
133
+ await page.evaluate(
134
+ async function (selectors) {
135
+ var observer = new MutationObserver(function () {
136
+ let progressBar = window.getElementByXpath(
137
+ selectors.PROGRESS
138
+ );
139
+ let progressMessage = window.getElementByXpath(
140
+ selectors.PROGRESS_MESSAGE
141
+ );
142
+
143
+ if (progressBar) {
144
+ window.loadingScreen(
145
+ progressBar.value,
146
+ progressMessage.innerText
147
+ );
148
+ }
149
+ });
150
+
151
+ observer.observe(document, {
152
+ attributes: true,
153
+ childList: true,
154
+ characterData: true,
155
+ subtree: true,
156
+ });
157
+ },
158
+ {
159
+ PROGRESS: '//*[@id=\'app\']/div/div/div[2]/progress',
160
+ PROGRESS_MESSAGE: '//*[@id=\'app\']/div/div/div[3]',
161
+ }
162
+ );
163
+
113
164
  const INTRO_IMG_SELECTOR = '[data-testid="intro-md-beta-logo-dark"], [data-testid="intro-md-beta-logo-light"], [data-asset-intro-image-light="true"], [data-asset-intro-image-dark="true"]';
114
165
  const INTRO_QRCODE_SELECTOR = 'div[data-ref] canvas';
115
166
 
@@ -127,7 +178,7 @@ class Client extends EventEmitter {
127
178
  })
128
179
  ]);
129
180
 
130
- // Checks if an error ocurred on the first found selector. The second will be discarded and ignored by .race;
181
+ // Checks if an error occurred on the first found selector. The second will be discarded and ignored by .race;
131
182
  if (needAuthentication instanceof Error) throw needAuthentication;
132
183
 
133
184
  // Scan-qrcode selector was found. Needs authentication
@@ -368,7 +419,7 @@ class Client extends EventEmitter {
368
419
  this.emit(Events.MEDIA_UPLOADED, message);
369
420
  });
370
421
 
371
- await page.exposeFunction('onAppStateChangedEvent', (state) => {
422
+ await page.exposeFunction('onAppStateChangedEvent', async (state) => {
372
423
 
373
424
  /**
374
425
  * Emitted when the connection state changes
@@ -395,6 +446,7 @@ class Client extends EventEmitter {
395
446
  * @event Client#disconnected
396
447
  * @param {WAState|"NAVIGATION"} reason reason that caused the disconnect
397
448
  */
449
+ await this.authStrategy.disconnect();
398
450
  this.emit(Events.DISCONNECTED, state);
399
451
  this.destroy();
400
452
  }
@@ -434,6 +486,27 @@ class Client extends EventEmitter {
434
486
  this.emit(Events.INCOMING_CALL, cll);
435
487
  });
436
488
 
489
+ await page.exposeFunction('onReaction', (reactions) => {
490
+ for (const reaction of reactions) {
491
+ /**
492
+ * Emitted when a reaction is sent, received, updated or removed
493
+ * @event Client#message_reaction
494
+ * @param {object} reaction
495
+ * @param {object} reaction.id - Reaction id
496
+ * @param {number} reaction.orphan - Orphan
497
+ * @param {?string} reaction.orphanReason - Orphan reason
498
+ * @param {number} reaction.timestamp - Timestamp
499
+ * @param {string} reaction.reaction - Reaction
500
+ * @param {boolean} reaction.read - Read
501
+ * @param {object} reaction.msgId - Parent message id
502
+ * @param {string} reaction.senderId - Sender id
503
+ * @param {?number} reaction.ack - Ack
504
+ */
505
+
506
+ this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction));
507
+ }
508
+ });
509
+
437
510
  await page.evaluate(() => {
438
511
  window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(window.WWebJS.getMessageModel(msg)); });
439
512
  window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(window.WWebJS.getMessageModel(msg)); });
@@ -453,6 +526,22 @@ class Client extends EventEmitter {
453
526
  }
454
527
  }
455
528
  });
529
+
530
+ {
531
+ const module = window.Store.createOrUpdateReactionsModule;
532
+ const ogMethod = module.createOrUpdateReactions;
533
+ module.createOrUpdateReactions = ((...args) => {
534
+ window.onReaction(args[0].map(reaction => {
535
+ const msgKey = window.Store.MsgKey.fromString(reaction.msgKey);
536
+ const parentMsgKey = window.Store.MsgKey.fromString(reaction.parentMsgKey);
537
+ const timestamp = reaction.timestamp / 1000;
538
+
539
+ return {...reaction, msgKey, parentMsgKey, timestamp };
540
+ }));
541
+
542
+ return ogMethod(...args);
543
+ }).bind(module);
544
+ }
456
545
  });
457
546
 
458
547
  /**
@@ -460,11 +549,13 @@ class Client extends EventEmitter {
460
549
  * @event Client#ready
461
550
  */
462
551
  this.emit(Events.READY);
552
+ this.authStrategy.afterAuthReady();
463
553
 
464
554
  // Disconnect when navigating away when in PAIRING state (detect logout)
465
555
  this.pupPage.on('framenavigated', async () => {
466
556
  const appState = await this.getState();
467
557
  if(!appState || appState === WAState.PAIRING) {
558
+ await this.authStrategy.disconnect();
468
559
  this.emit(Events.DISCONNECTED, 'NAVIGATION');
469
560
  await this.destroy();
470
561
  }
@@ -476,6 +567,7 @@ class Client extends EventEmitter {
476
567
  */
477
568
  async destroy() {
478
569
  await this.pupBrowser.close();
570
+ await this.authStrategy.destroy();
479
571
  }
480
572
 
481
573
  /**
@@ -580,7 +672,7 @@ class Client extends EventEmitter {
580
672
  internalOptions.list = content;
581
673
  content = '';
582
674
  }
583
-
675
+
584
676
  if (internalOptions.sendMediaAsSticker && internalOptions.attachment) {
585
677
  internalOptions.attachment = await Util.formatToWebpSticker(
586
678
  internalOptions.attachment, {
@@ -744,7 +836,7 @@ class Client extends EventEmitter {
744
836
 
745
837
  return couldSet;
746
838
  }
747
-
839
+
748
840
  /**
749
841
  * Gets the current connection state for the client
750
842
  * @returns {WAState}
@@ -944,7 +1036,8 @@ class Client extends EventEmitter {
944
1036
  }
945
1037
 
946
1038
  return await this.pupPage.evaluate(async number => {
947
- const result = await window.Store.QueryExist(number);
1039
+ const wid = window.Store.WidFactory.createWid(number);
1040
+ const result = await window.Store.QueryExist(wid);
948
1041
  if (!result || result.wid === undefined) return null;
949
1042
  return result.wid;
950
1043
  }, number);
@@ -18,6 +18,9 @@ class BaseAuthStrategy {
18
18
  };
19
19
  }
20
20
  async getAuthEventPayload() {}
21
+ async afterAuthReady() {}
22
+ async disconnect() {}
23
+ async destroy() {}
21
24
  async logout() {}
22
25
  }
23
26
 
@@ -0,0 +1,204 @@
1
+ 'use strict';
2
+
3
+ /* Require Optional Dependencies */
4
+ try {
5
+ var fs = require('fs-extra');
6
+ var unzipper = require('unzipper');
7
+ var archiver = require('archiver');
8
+ } catch {
9
+ fs = undefined;
10
+ unzipper = undefined;
11
+ archiver = undefined;
12
+ }
13
+
14
+ const path = require('path');
15
+ const { Events } = require('./../util/Constants');
16
+ const BaseAuthStrategy = require('./BaseAuthStrategy');
17
+
18
+ /**
19
+ * Remote-based authentication
20
+ * @param {object} options - options
21
+ * @param {object} options.store - Remote database store instance
22
+ * @param {string} options.clientId - Client id to distinguish instances if you are using multiple, otherwise keep null if you are using only one instance
23
+ * @param {string} options.dataPath - Change the default path for saving session files, default is: "./.wwebjs_auth/"
24
+ * @param {number} options.backupSyncIntervalMs - Sets the time interval for periodic session backups. Accepts values starting from 60000ms {1 minute}
25
+ */
26
+ class RemoteAuth extends BaseAuthStrategy {
27
+ constructor({ clientId, dataPath, store, backupSyncIntervalMs } = {}) {
28
+ if (!fs && !unzipper && !archiver) throw new Error('Optional Dependencies [fs-extra, unzipper, archiver] are required to use RemoteAuth. Make sure to run npm install correctly and remove the --no-optional flag');
29
+ super();
30
+
31
+ const idRegex = /^[-_\w]+$/i;
32
+ if (clientId && !idRegex.test(clientId)) {
33
+ throw new Error('Invalid clientId. Only alphanumeric characters, underscores and hyphens are allowed.');
34
+ }
35
+ if (!backupSyncIntervalMs || backupSyncIntervalMs < 60000) {
36
+ throw new Error('Invalid backupSyncIntervalMs. Accepts values starting from 60000ms {1 minute}.');
37
+ }
38
+ if(!store) throw new Error('Remote database store is required.');
39
+
40
+ this.store = store;
41
+ this.clientId = clientId;
42
+ this.backupSyncIntervalMs = backupSyncIntervalMs;
43
+ this.dataPath = path.resolve(dataPath || './.wwebjs_auth/');
44
+ this.tempDir = `${this.dataPath}/wwebjs_temp_session`;
45
+ this.requiredDirs = ['Default', 'IndexedDB', 'Local Storage']; /* => Required Files & Dirs in WWebJS to restore session */
46
+ }
47
+
48
+ async beforeBrowserInitialized() {
49
+ const puppeteerOpts = this.client.options.puppeteer;
50
+ const sessionDirName = this.clientId ? `RemoteAuth-${this.clientId}` : 'RemoteAuth';
51
+ const dirPath = path.join(this.dataPath, sessionDirName);
52
+
53
+ if (puppeteerOpts.userDataDir && puppeteerOpts.userDataDir !== dirPath) {
54
+ throw new Error('RemoteAuth is not compatible with a user-supplied userDataDir.');
55
+ }
56
+
57
+ this.userDataDir = dirPath;
58
+ this.sessionName = sessionDirName;
59
+
60
+ await this.extractRemoteSession();
61
+
62
+ this.client.options.puppeteer = {
63
+ ...puppeteerOpts,
64
+ userDataDir: dirPath
65
+ };
66
+ }
67
+
68
+ async logout() {
69
+ await this.disconnect();
70
+ }
71
+
72
+ async destroy() {
73
+ clearInterval(this.backupSync);
74
+ }
75
+
76
+ async disconnect() {
77
+ await this.deleteRemoteSession();
78
+
79
+ let pathExists = await this.isValidPath(this.userDataDir);
80
+ if (pathExists) {
81
+ await fs.promises.rm(this.userDataDir, {
82
+ recursive: true,
83
+ force: true
84
+ }).catch(() => {});
85
+ }
86
+ clearInterval(this.backupSync);
87
+ }
88
+
89
+ async afterAuthReady() {
90
+ const sessionExists = await this.store.sessionExists({session: this.sessionName});
91
+ if(!sessionExists) {
92
+ await this.delay(60000); /* Initial delay sync required for session to be stable enough to recover */
93
+ await this.storeRemoteSession({emit: true});
94
+ }
95
+ var self = this;
96
+ this.backupSync = setInterval(async function () {
97
+ await self.storeRemoteSession();
98
+ }, this.backupSyncIntervalMs);
99
+ }
100
+
101
+ async storeRemoteSession(options) {
102
+ /* Compress & Store Session */
103
+ const pathExists = await this.isValidPath(this.userDataDir);
104
+ if (pathExists) {
105
+ await this.compressSession();
106
+ await this.store.save({session: this.sessionName});
107
+ await fs.promises.unlink(`${this.sessionName}.zip`);
108
+ await fs.promises.rm(`${this.tempDir}`, {
109
+ recursive: true,
110
+ force: true
111
+ }).catch(() => {});
112
+ if(options && options.emit) this.client.emit(Events.REMOTE_SESSION_SAVED);
113
+ }
114
+ }
115
+
116
+ async extractRemoteSession() {
117
+ const pathExists = await this.isValidPath(this.userDataDir);
118
+ const compressedSessionPath = `${this.sessionName}.zip`;
119
+ const sessionExists = await this.store.sessionExists({session: this.sessionName});
120
+ if (pathExists) {
121
+ await fs.promises.rm(this.userDataDir, {
122
+ recursive: true,
123
+ force: true
124
+ }).catch(() => {});
125
+ }
126
+ if (sessionExists) {
127
+ await this.store.extract({session: this.sessionName, path: compressedSessionPath});
128
+ await this.unCompressSession(compressedSessionPath);
129
+ } else {
130
+ fs.mkdirSync(this.userDataDir, { recursive: true });
131
+ }
132
+ }
133
+
134
+ async deleteRemoteSession() {
135
+ const sessionExists = await this.store.sessionExists({session: this.sessionName});
136
+ if (sessionExists) await this.store.delete({session: this.sessionName});
137
+ }
138
+
139
+ async compressSession() {
140
+ const archive = archiver('zip');
141
+ const stream = fs.createWriteStream(`${this.sessionName}.zip`);
142
+
143
+ await fs.copy(this.userDataDir, this.tempDir).catch(() => {});
144
+ await this.deleteMetadata();
145
+ return new Promise((resolve, reject) => {
146
+ archive
147
+ .directory(this.tempDir, false)
148
+ .on('error', err => reject(err))
149
+ .pipe(stream);
150
+
151
+ stream.on('close', () => resolve());
152
+ archive.finalize();
153
+ });
154
+ }
155
+
156
+ async unCompressSession(compressedSessionPath) {
157
+ var stream = fs.createReadStream(compressedSessionPath);
158
+ await new Promise((resolve, reject) => {
159
+ stream.pipe(unzipper.Extract({
160
+ path: this.userDataDir
161
+ }))
162
+ .on('error', err => reject(err))
163
+ .on('finish', () => resolve());
164
+ });
165
+ await fs.promises.unlink(compressedSessionPath);
166
+ }
167
+
168
+ async deleteMetadata() {
169
+ const sessionDirs = [this.tempDir, path.join(this.tempDir, 'Default')];
170
+ for (const dir of sessionDirs) {
171
+ const sessionFiles = await fs.promises.readdir(dir);
172
+ for (const element of sessionFiles) {
173
+ if (!this.requiredDirs.includes(element)) {
174
+ const dirElement = path.join(dir, element);
175
+ const stats = await fs.promises.lstat(dirElement);
176
+
177
+ if (stats.isDirectory()) {
178
+ await fs.promises.rm(dirElement, {
179
+ recursive: true,
180
+ force: true
181
+ });
182
+ } else {
183
+ await fs.promises.unlink(dirElement);
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ async isValidPath(path) {
191
+ try {
192
+ await fs.promises.access(path);
193
+ return true;
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+
199
+ async delay(ms) {
200
+ return new Promise(resolve => setTimeout(resolve, ms));
201
+ }
202
+ }
203
+
204
+ module.exports = RemoteAuth;
@@ -170,13 +170,22 @@ class Chat extends Base {
170
170
 
171
171
  /**
172
172
  * Loads chat messages, sorted from earliest to latest.
173
- * @param {Object} searchOptions Options for searching messages. Right now only limit is supported.
173
+ * @param {Object} searchOptions Options for searching messages. Right now only limit and fromMe is supported.
174
174
  * @param {Number} [searchOptions.limit] The amount of messages to return. If no limit is specified, the available messages will be returned. Note that the actual number of returned messages may be smaller if there aren't enough messages in the conversation. Set this to Infinity to load all messages.
175
+ * @param {Boolean} [searchOptions.fromMe] Return only messages from the bot number or vise versa. To get all messages, leave the option undefined.
175
176
  * @returns {Promise<Array<Message>>}
176
177
  */
177
178
  async fetchMessages(searchOptions) {
178
179
  let messages = await this.client.pupPage.evaluate(async (chatId, searchOptions) => {
179
- const msgFilter = m => !m.isNotification; // dont include notification messages
180
+ const msgFilter = (m) => {
181
+ if (m.isNotification) {
182
+ return false; // dont include notification messages
183
+ }
184
+ if (searchOptions && searchOptions.fromMe && m.id.fromMe !== searchOptions.fromMe) {
185
+ return false;
186
+ }
187
+ return true;
188
+ };
180
189
 
181
190
  const chat = window.Store.Chat.get(chatId);
182
191
  let msgs = chat.msgs.getModelsArray().filter(msgFilter);
@@ -184,7 +193,7 @@ class Chat extends Base {
184
193
  if (searchOptions && searchOptions.limit > 0) {
185
194
  while (msgs.length < searchOptions.limit) {
186
195
  const loadedMessages = await window.Store.ConversationMsgs.loadEarlierMsgs(chat);
187
- if (!loadedMessages) break;
196
+ if (!loadedMessages || !loadedMessages.length) break;
188
197
  msgs = [...loadedMessages.filter(msgFilter), ...msgs];
189
198
  }
190
199
 
@@ -147,7 +147,7 @@ class GroupChat extends Chat {
147
147
  this.groupMetadata.desc = description;
148
148
  return true;
149
149
  }
150
-
150
+
151
151
  /**
152
152
  * Updates the group settings to only allow admins to send messages.
153
153
  * @param {boolean} [adminsOnly=true] Enable or disable this option
@@ -335,6 +335,20 @@ class Message extends Base {
335
335
  return this.client.sendMessage(chatId, content, options);
336
336
  }
337
337
 
338
+ /**
339
+ * React to this message with an emoji
340
+ * @param {string} reaction - Emoji to react with. Send an empty string to remove the reaction.
341
+ * @return {Promise}
342
+ */
343
+ async react(reaction){
344
+ await this.client.pupPage.evaluate(async (messageId, reaction) => {
345
+ if (!messageId) { return undefined; }
346
+
347
+ const msg = await window.Store.Msg.get(messageId);
348
+ await window.Store.sendReactionToMsg(msg, reaction);
349
+ }, this.id._serialized, reaction);
350
+ }
351
+
338
352
  /**
339
353
  * Accept Group V4 Invite
340
354
  * @returns {Promise<Object>}
@@ -344,7 +358,7 @@ class Message extends Base {
344
358
  }
345
359
 
346
360
  /**
347
- * Forwards this message to another chat
361
+ * Forwards this message to another chat (that you chatted before, otherwise it will fail)
348
362
  *
349
363
  * @param {string|Chat} chat Chat model or chat ID to which the message will be forwarded
350
364
  * @returns {Promise}
@@ -396,12 +410,13 @@ class Message extends Base {
396
410
  signal: (new AbortController).signal
397
411
  });
398
412
 
399
- const data = window.WWebJS.arrayBufferToBase64(decryptedMedia);
413
+ const data = await window.WWebJS.arrayBufferToBase64Async(decryptedMedia);
400
414
 
401
415
  return {
402
416
  data,
403
417
  mimetype: msg.mimetype,
404
- filename: msg.filename
418
+ filename: msg.filename,
419
+ filesize: msg.size
405
420
  };
406
421
  } catch (e) {
407
422
  if(e.status && e.status === 404) return undefined;
@@ -410,7 +425,7 @@ class Message extends Base {
410
425
  }, this.id._serialized);
411
426
 
412
427
  if (!result) return undefined;
413
- return new MessageMedia(result.mimetype, result.data, result.filename);
428
+ return new MessageMedia(result.mimetype, result.data, result.filename, result.filesize);
414
429
  }
415
430
 
416
431
  /**
@@ -437,7 +452,7 @@ class Message extends Base {
437
452
  let msg = window.Store.Msg.get(msgId);
438
453
 
439
454
  if (msg.canStar()) {
440
- return msg.chat.sendStarMsgs([msg], true);
455
+ return window.Store.Cmd.sendStarMsgs(msg.chat, [msg], false);
441
456
  }
442
457
  }, this.id._serialized);
443
458
  }
@@ -450,7 +465,7 @@ class Message extends Base {
450
465
  let msg = window.Store.Msg.get(msgId);
451
466
 
452
467
  if (msg.canStar()) {
453
- return msg.chat.sendStarMsgs([msg], false);
468
+ return window.Store.Cmd.sendUnstarMsgs(msg.chat, [msg], false);
454
469
  }
455
470
  }, this.id._serialized);
456
471
  }
@@ -10,10 +10,11 @@ const { URL } = require('url');
10
10
  * Media attached to a message
11
11
  * @param {string} mimetype MIME type of the attachment
12
12
  * @param {string} data Base64-encoded data of the file
13
- * @param {?string} filename Document file name
13
+ * @param {?string} filename Document file name. Value can be null
14
+ * @param {?number} filesize Document file size in bytes. Value can be null
14
15
  */
15
16
  class MessageMedia {
16
- constructor(mimetype, data, filename) {
17
+ constructor(mimetype, data, filename, filesize) {
17
18
  /**
18
19
  * MIME type of the attachment
19
20
  * @type {string}
@@ -27,10 +28,16 @@ class MessageMedia {
27
28
  this.data = data;
28
29
 
29
30
  /**
30
- * Name of the file (for documents)
31
+ * Document file name. Value can be null
31
32
  * @type {?string}
32
33
  */
33
34
  this.filename = filename;
35
+
36
+ /**
37
+ * Document file size in bytes. Value can be null
38
+ * @type {?number}
39
+ */
40
+ this.filesize = filesize;
34
41
  }
35
42
 
36
43
  /**
@@ -68,6 +75,7 @@ class MessageMedia {
68
75
  const reqOptions = Object.assign({ headers: { accept: 'image/* video/* text/* audio/*' } }, options);
69
76
  const response = await fetch(url, reqOptions);
70
77
  const mime = response.headers.get('Content-Type');
78
+ const size = response.headers.get('Content-Length');
71
79
 
72
80
  const contentDisposition = response.headers.get('Content-Disposition');
73
81
  const name = contentDisposition ? contentDisposition.match(/((?<=filename=")(.*)(?="))/) : null;
@@ -83,7 +91,7 @@ class MessageMedia {
83
91
  data = btoa(data);
84
92
  }
85
93
 
86
- return { data, mime, name };
94
+ return { data, mime, name, size };
87
95
  }
88
96
 
89
97
  const res = options.client
@@ -96,7 +104,7 @@ class MessageMedia {
96
104
  if (!mimetype)
97
105
  mimetype = res.mime;
98
106
 
99
- return new MessageMedia(mimetype, res.data, filename);
107
+ return new MessageMedia(mimetype, res.data, filename, res.size || null);
100
108
  }
101
109
  }
102
110
 
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const Base = require('./Base');
4
+
5
+ /**
6
+ * Represents a Reaction on WhatsApp
7
+ * @extends {Base}
8
+ */
9
+ class Reaction extends Base {
10
+ constructor(client, data) {
11
+ super(client);
12
+
13
+ if (data) this._patch(data);
14
+ }
15
+
16
+ _patch(data) {
17
+ /**
18
+ * Reaction ID
19
+ * @type {object}
20
+ */
21
+ this.id = data.msgKey;
22
+ /**
23
+ * Orphan
24
+ * @type {number}
25
+ */
26
+ this.orphan = data.orphan;
27
+ /**
28
+ * Orphan reason
29
+ * @type {?string}
30
+ */
31
+ this.orphanReason = data.orphanReason;
32
+ /**
33
+ * Unix timestamp for when the reaction was created
34
+ * @type {number}
35
+ */
36
+ this.timestamp = data.timestamp;
37
+ /**
38
+ * Reaction
39
+ * @type {string}
40
+ */
41
+ this.reaction = data.reactionText;
42
+ /**
43
+ * Read
44
+ * @type {boolean}
45
+ */
46
+ this.read = data.read;
47
+ /**
48
+ * Message ID
49
+ * @type {object}
50
+ */
51
+ this.msgId = data.parentMsgKey;
52
+ /**
53
+ * Sender ID
54
+ * @type {string}
55
+ */
56
+ this.senderId = data.senderUserJid;
57
+ /**
58
+ * ACK
59
+ * @type {?number}
60
+ */
61
+ this.ack = data.ack;
62
+
63
+
64
+ return super._patch(data);
65
+ }
66
+
67
+ }
68
+
69
+ module.exports = Reaction;
@@ -17,5 +17,6 @@ module.exports = {
17
17
  Call: require('./Call'),
18
18
  Buttons: require('./Buttons'),
19
19
  List: require('./List'),
20
- Payment: require('./Payment')
20
+ Payment: require('./Payment'),
21
+ Reaction: require('./Reaction'),
21
22
  };
@@ -11,7 +11,7 @@ exports.DefaultOptions = {
11
11
  qrMaxRetries: 0,
12
12
  takeoverOnConflict: false,
13
13
  takeoverTimeoutMs: 0,
14
- userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36',
14
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36',
15
15
  ffmpegPath: 'ffmpeg',
16
16
  bypassCSP: false
17
17
  };
@@ -41,15 +41,18 @@ exports.Events = {
41
41
  MESSAGE_REVOKED_EVERYONE: 'message_revoke_everyone',
42
42
  MESSAGE_REVOKED_ME: 'message_revoke_me',
43
43
  MESSAGE_ACK: 'message_ack',
44
+ MESSAGE_REACTION: 'message_reaction',
44
45
  MEDIA_UPLOADED: 'media_uploaded',
45
46
  GROUP_JOIN: 'group_join',
46
47
  GROUP_LEAVE: 'group_leave',
47
48
  GROUP_UPDATE: 'group_update',
48
49
  QR_RECEIVED: 'qr',
50
+ LOADING_SCREEN: 'loading_screen',
49
51
  DISCONNECTED: 'disconnected',
50
52
  STATE_CHANGED: 'change_state',
51
53
  BATTERY_CHANGED: 'change_battery',
52
- INCOMING_CALL: 'incoming_call'
54
+ INCOMING_CALL: 'incoming_call',
55
+ REMOTE_SESSION_SAVED: 'remote_session_saved'
53
56
  };
54
57
 
55
58
  /**
@@ -49,6 +49,8 @@ exports.ExposeStore = (moduleRaidStr) => {
49
49
  window.Store.findCommonGroups = window.mR.findModule('findCommonGroups')[0].findCommonGroups;
50
50
  window.Store.StatusUtils = window.mR.findModule('setMyStatus')[0];
51
51
  window.Store.ConversationMsgs = window.mR.findModule('loadEarlierMsgs')[0];
52
+ window.Store.sendReactionToMsg = window.mR.findModule('sendReactionToMsg')[0].sendReactionToMsg;
53
+ window.Store.createOrUpdateReactionsModule = window.mR.findModule('createOrUpdateReactions')[0];
52
54
  window.Store.StickerTools = {
53
55
  ...window.mR.findModule('toWebpSticker')[0],
54
56
  ...window.mR.findModule('addWebpMetadata')[0]
@@ -99,7 +101,6 @@ exports.LoadUtils = () => {
99
101
  delete options.attachment;
100
102
  delete options.sendMediaAsSticker;
101
103
  }
102
-
103
104
  let quotedMsgOptions = {};
104
105
  if (options.quotedMessageId) {
105
106
  let quotedMessage = window.Store.Msg.get(options.quotedMessageId);
@@ -479,6 +480,20 @@ exports.LoadUtils = () => {
479
480
  return window.btoa(binary);
480
481
  };
481
482
 
483
+ window.WWebJS.arrayBufferToBase64Async = (arrayBuffer) =>
484
+ new Promise((resolve, reject) => {
485
+ const blob = new Blob([arrayBuffer], {
486
+ type: 'application/octet-stream',
487
+ });
488
+ const fileReader = new FileReader();
489
+ fileReader.onload = () => {
490
+ const [, data] = fileReader.result.split(',');
491
+ resolve(data);
492
+ };
493
+ fileReader.onerror = (e) => reject(e);
494
+ fileReader.readAsDataURL(blob);
495
+ });
496
+
482
497
  window.WWebJS.getFileHash = async (data) => {
483
498
  let buffer = await data.arrayBuffer();
484
499
  const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
package/src/util/Util.js CHANGED
@@ -6,7 +6,6 @@ const { tmpdir } = require('os');
6
6
  const ffmpeg = require('fluent-ffmpeg');
7
7
  const webp = require('node-webpmux');
8
8
  const fs = require('fs').promises;
9
-
10
9
  const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
11
10
 
12
11
  /**