whatsapp-web.js 1.17.0 → 1.18.0-alpha.1

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
@@ -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);
package/index.d.ts CHANGED
@@ -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 */
@@ -704,9 +747,9 @@ declare namespace WAWebJS {
704
747
  */
705
748
  reply: (content: MessageContent, chatId?: string, options?: MessageSendOptions) => Promise<Message>,
706
749
  /** React to this message with an emoji*/
707
- react: (reaction: string) => Promise,
750
+ react: (reaction: string) => Promise<void>,
708
751
  /**
709
- * Forwards this message to another chat
752
+ * Forwards this message to another chat (that you chatted before, otherwise it will fail)
710
753
  */
711
754
  forward: (chat: Chat | string) => Promise<void>,
712
755
  /** Star this message */
@@ -803,13 +846,16 @@ declare namespace WAWebJS {
803
846
  data: string
804
847
  /** Document file name. Value can be null */
805
848
  filename?: string | null
849
+ /** Document file size in bytes. Value can be null. */
850
+ filesize?: number | null
806
851
 
807
852
  /**
808
853
  * @param {string} mimetype MIME type of the attachment
809
854
  * @param {string} data Base64-encoded data of the file
810
855
  * @param {?string} filename Document file name. Value can be null
856
+ * @param {?number} filesize Document file size in bytes. Value can be null.
811
857
  */
812
- constructor(mimetype: string, data: string, filename?: string | null)
858
+ constructor(mimetype: string, data: string, filename?: string | null, filesize?: number | null)
813
859
 
814
860
  /** Creates a MessageMedia instance from a local file path */
815
861
  static fromFilePath: (filePath: string) => MessageMedia
@@ -1016,6 +1062,10 @@ declare namespace WAWebJS {
1016
1062
  * Set this to Infinity to load all messages.
1017
1063
  */
1018
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
1019
1069
  }
1020
1070
 
1021
1071
  /**
@@ -1298,6 +1348,19 @@ declare namespace WAWebJS {
1298
1348
 
1299
1349
  constructor(body: string, buttons: Array<{ id?: string; body: string }>, title?: string | null, footer?: string | null)
1300
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
+ }
1301
1364
  }
1302
1365
 
1303
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.17.0",
3
+ "version": "1.18.0-alpha.1",
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
 
@@ -115,6 +115,52 @@ class Client extends EventEmitter {
115
115
  referer: 'https://whatsapp.com/'
116
116
  });
117
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
+
118
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"]';
119
165
  const INTRO_QRCODE_SELECTOR = 'div[data-ref] canvas';
120
166
 
@@ -132,7 +178,7 @@ class Client extends EventEmitter {
132
178
  })
133
179
  ]);
134
180
 
135
- // 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;
136
182
  if (needAuthentication instanceof Error) throw needAuthentication;
137
183
 
138
184
  // Scan-qrcode selector was found. Needs authentication
@@ -373,7 +419,7 @@ class Client extends EventEmitter {
373
419
  this.emit(Events.MEDIA_UPLOADED, message);
374
420
  });
375
421
 
376
- await page.exposeFunction('onAppStateChangedEvent', (state) => {
422
+ await page.exposeFunction('onAppStateChangedEvent', async (state) => {
377
423
 
378
424
  /**
379
425
  * Emitted when the connection state changes
@@ -400,6 +446,7 @@ class Client extends EventEmitter {
400
446
  * @event Client#disconnected
401
447
  * @param {WAState|"NAVIGATION"} reason reason that caused the disconnect
402
448
  */
449
+ await this.authStrategy.disconnect();
403
450
  this.emit(Events.DISCONNECTED, state);
404
451
  this.destroy();
405
452
  }
@@ -439,6 +486,27 @@ class Client extends EventEmitter {
439
486
  this.emit(Events.INCOMING_CALL, cll);
440
487
  });
441
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
+
442
510
  await page.evaluate(() => {
443
511
  window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(window.WWebJS.getMessageModel(msg)); });
444
512
  window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(window.WWebJS.getMessageModel(msg)); });
@@ -458,6 +526,22 @@ class Client extends EventEmitter {
458
526
  }
459
527
  }
460
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
+ }
461
545
  });
462
546
 
463
547
  /**
@@ -465,11 +549,13 @@ class Client extends EventEmitter {
465
549
  * @event Client#ready
466
550
  */
467
551
  this.emit(Events.READY);
552
+ this.authStrategy.afterAuthReady();
468
553
 
469
554
  // Disconnect when navigating away when in PAIRING state (detect logout)
470
555
  this.pupPage.on('framenavigated', async () => {
471
556
  const appState = await this.getState();
472
557
  if(!appState || appState === WAState.PAIRING) {
558
+ await this.authStrategy.disconnect();
473
559
  this.emit(Events.DISCONNECTED, 'NAVIGATION');
474
560
  await this.destroy();
475
561
  }
@@ -481,6 +567,7 @@ class Client extends EventEmitter {
481
567
  */
482
568
  async destroy() {
483
569
  await this.pupBrowser.close();
570
+ await this.authStrategy.destroy();
484
571
  }
485
572
 
486
573
  /**
@@ -949,7 +1036,8 @@ class Client extends EventEmitter {
949
1036
  }
950
1037
 
951
1038
  return await this.pupPage.evaluate(async number => {
952
- const result = await window.Store.QueryExist(number);
1039
+ const wid = window.Store.WidFactory.createWid(number);
1040
+ const result = await window.Store.QueryExist(wid);
953
1041
  if (!result || result.wid === undefined) return null;
954
1042
  return result.wid;
955
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);
@@ -342,6 +342,8 @@ class Message extends Base {
342
342
  */
343
343
  async react(reaction){
344
344
  await this.client.pupPage.evaluate(async (messageId, reaction) => {
345
+ if (!messageId) { return undefined; }
346
+
345
347
  const msg = await window.Store.Msg.get(messageId);
346
348
  await window.Store.sendReactionToMsg(msg, reaction);
347
349
  }, this.id._serialized, reaction);
@@ -356,7 +358,7 @@ class Message extends Base {
356
358
  }
357
359
 
358
360
  /**
359
- * Forwards this message to another chat
361
+ * Forwards this message to another chat (that you chatted before, otherwise it will fail)
360
362
  *
361
363
  * @param {string|Chat} chat Chat model or chat ID to which the message will be forwarded
362
364
  * @returns {Promise}
@@ -408,12 +410,13 @@ class Message extends Base {
408
410
  signal: (new AbortController).signal
409
411
  });
410
412
 
411
- const data = window.WWebJS.arrayBufferToBase64(decryptedMedia);
413
+ const data = await window.WWebJS.arrayBufferToBase64Async(decryptedMedia);
412
414
 
413
415
  return {
414
416
  data,
415
417
  mimetype: msg.mimetype,
416
- filename: msg.filename
418
+ filename: msg.filename,
419
+ filesize: msg.size
417
420
  };
418
421
  } catch (e) {
419
422
  if(e.status && e.status === 404) return undefined;
@@ -422,7 +425,7 @@ class Message extends Base {
422
425
  }, this.id._serialized);
423
426
 
424
427
  if (!result) return undefined;
425
- return new MessageMedia(result.mimetype, result.data, result.filename);
428
+ return new MessageMedia(result.mimetype, result.data, result.filename, result.filesize);
426
429
  }
427
430
 
428
431
  /**
@@ -449,7 +452,7 @@ class Message extends Base {
449
452
  let msg = window.Store.Msg.get(msgId);
450
453
 
451
454
  if (msg.canStar()) {
452
- return msg.chat.sendStarMsgs([msg], true);
455
+ return window.Store.Cmd.sendStarMsgs(msg.chat, [msg], false);
453
456
  }
454
457
  }, this.id._serialized);
455
458
  }
@@ -462,7 +465,7 @@ class Message extends Base {
462
465
  let msg = window.Store.Msg.get(msgId);
463
466
 
464
467
  if (msg.canStar()) {
465
- return msg.chat.sendStarMsgs([msg], false);
468
+ return window.Store.Cmd.sendUnstarMsgs(msg.chat, [msg], false);
466
469
  }
467
470
  }, this.id._serialized);
468
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;
@@ -18,4 +18,5 @@ module.exports = {
18
18
  Buttons: require('./Buttons'),
19
19
  List: require('./List'),
20
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
  /**
@@ -50,6 +50,7 @@ exports.ExposeStore = (moduleRaidStr) => {
50
50
  window.Store.StatusUtils = window.mR.findModule('setMyStatus')[0];
51
51
  window.Store.ConversationMsgs = window.mR.findModule('loadEarlierMsgs')[0];
52
52
  window.Store.sendReactionToMsg = window.mR.findModule('sendReactionToMsg')[0].sendReactionToMsg;
53
+ window.Store.createOrUpdateReactionsModule = window.mR.findModule('createOrUpdateReactions')[0];
53
54
  window.Store.StickerTools = {
54
55
  ...window.mR.findModule('toWebpSticker')[0],
55
56
  ...window.mR.findModule('addWebpMetadata')[0]
@@ -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);