diff --git a/.env.example b/.env.example
index 6cd9eb9..3add97c 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,3 @@
-WWEBJS_TEST_SESSION_PATH=test_session.json
-WWEBJS_TEST_REMOTE_ID=XXXXXXXXXX@c.us
\ No newline at end of file
+WWEBJS_TEST_REMOTE_ID=XXXXXXXXXX@c.us
+WWEBJS_TEST_CLIENT_ID=authenticated
+WWEBJS_TEST_MD=1
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 1e704a3..0000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-name: Bug report
-about: Is something not working as intended? Report it here.
-title: ''
-assignees: ''
-
----
-
-### Bug description
-A clear and concise description of what the bug is.
-
-### Reproduction steps
-Steps to reproduce the behavior:
-1.
-2.
-3.
-...
-
-### Expected behavior
-A clear and concise description of what you expected to happen.
-
-### Relevant code
-If applicable, add code snippets to help explain your problem.
-
-### Environment (please complete the following information):
-**WhatsApp**
- - Account type [Standard / Business]:
- - Device OS [iOS / Android]:
- - WhatsApp Web version [run `await client.getWWebVersion()`]:
-
-**Library**
- - Browser [Chrome / Chromium]:
- - NodeJS version (`node -v`):
- - npm or yarn version (`npm -v`):
- - whatsapp-web.js version:
-
-**Other**
- - Operating system (the one running node) [Linux / MacOS / Windows]:
- - Operating system version (ex. Windows 10):
-### Additional context
-Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..4fa8524
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,105 @@
+name: '🐛 Bug report'
+description: Create a report to help us improve
+labels: bug
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thank you for reporting an issue :pray:.
+
+ This issue tracker is for reporting bugs found in [`whatsapp-web.js`](https://github.com/pedroslopez/whatsapp-web.js).
+
+ If you have a question about how to achieve something and are struggling, please post a question in our [Discord server](https://discord.gg/wyKybbF) instead.
+
+ Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already:
+ - `whatsapp-web.js` [Issues tab](https://github.com/pedroslopez/whatsapp-web.js/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc)
+ - `whatsapp-web.js` [closed Issues tab](https://github.com/pedroslopez/whatsapp-web.js/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed)
+
+ The more information you fill in, the better the community can help you.
+ - type: checkboxes
+ attributes:
+ label: Is there an existing issue for this?
+ description: Please search to see if an issue already exists for the bug you encountered.
+ options:
+ - label: I have searched the existing issues
+ required: true
+ - type: textarea
+ id: description
+ attributes:
+ label: Describe the bug
+ description: Provide a clear and concise description of the challenge you are running into.
+ validations:
+ required: true
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected behavior
+ description: Provide a clear and concise description of what you expected to happen.
+ placeholder: |
+ As a user, I expected ___ behavior but I am seeing ___
+ validations:
+ required: true
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to Reproduce the Bug or Issue
+ description: Describe the steps we have to take to reproduce the behavior.
+ placeholder: |
+ 1. Do X
+ 2. Do Y
+ 3. Do Z
+ 4. See error
+ validations:
+ required: true
+ - type: textarea
+ id: relevant_code
+ attributes:
+ label: Relevant Code
+ description: If applicable, add code snippets to help explain your problem.
+ validations:
+ required: false
+ - type: dropdown
+ id: browser_type
+ attributes:
+ label: Browser Type
+ description: What web browser are you using?
+ options:
+ - Chromium
+ - Google Chrome
+ - Other (please write in Additional Context)
+ validations:
+ required: true
+ - type: dropdown
+ id: whatsapp_type
+ attributes:
+ label: WhatsApp Account Type
+ options:
+ - Standard
+ - WhatsApp Business
+ validations:
+ required: true
+ - type: dropdown
+ id: multidevice
+ attributes:
+ label: Does your WhatsApp account have multidevice enabled?
+ options:
+ - Yes, I am using Multi Device
+ - No, I am not using Multi Device
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Environment
+ description: |
+ - OS: [e.g. Mac, Windows, Linux, Docker + Ubuntu 18, etc]
+ - Phone OS: [e.g. Android, iOS]
+ - whatsapp-web.js version [e.g. 1.2.3]
+ - WhatsApp Web version [run `await client.getWWebVersion()`]:
+ - Node.js Version [e.g. 1.2.3]
+ validations:
+ required: true
+ - type: textarea
+ id: additional
+ attributes:
+ label: Additional context
+ description: Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 11fc491..0000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project
-title: ''
-labels: enhancement
-assignees: ''
-
----
-
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..9f4bc70
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,42 @@
+name: 🚀 Feature request
+description: Suggest an idea for this project
+labels: enhancement
+
+body:
+ - type: checkboxes
+ attributes:
+ label: Is there an existing issue for this?
+ description: Please search to see if an issue related to this feature request already exists.
+ options:
+ - label: I have searched the existing issues
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Is your feature request related to a problem? Please describe.
+ description: A concise description of the problem you are facing or the motivetion behind this feature request.
+ placeholder: I faced a problem due to ...
+ validations:
+ required: false
+
+ - type: textarea
+ attributes:
+ label: Describe the solution you'd like.
+ description: A concise description of the solution for the issue.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Describe an alternate solution.
+ description: Is there any other approach to solve the problem?
+ validations:
+ required: false
+
+ - type: textarea
+ attributes:
+ label: Additional context
+ description: |
+ Links? Screenshots? References? Anything that will give us more context about what you would like to see!
+ validations:
+ required: false
diff --git a/.gitignore b/.gitignore
index f0b9c4f..79f6467 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,8 +64,10 @@ typings/
# next.js build output
.next
-# macOS Thumbnails
+# macOS
._*
+.DS_Store
# Test sessions
-*session.json
\ No newline at end of file
+*session.json
+.wwebjs_auth/
\ No newline at end of file
diff --git a/.npmignore b/.npmignore
index c68d77a..a5131c9 100644
--- a/.npmignore
+++ b/.npmignore
@@ -12,6 +12,8 @@ yarn-debug.log*
yarn-error.log*
*session.json
+.wwebjs_auth/
+
.env
tools/
tests/
diff --git a/README.md b/README.md
index 7f76c99..03626ab 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[](https://www.npmjs.com/package/whatsapp-web.js) [](https://depfu.com/github/pedroslopez/whatsapp-web.js?project_id=9765)  [](https://discord.gg/H7DqQs4)
+[](https://www.npmjs.com/package/whatsapp-web.js) [](https://depfu.com/github/pedroslopez/whatsapp-web.js?project_id=9765)  [](https://discord.gg/H7DqQs4)
# whatsapp-web.js
A WhatsApp API client that connects through the WhatsApp Web browser app
@@ -47,43 +47,14 @@ client.initialize();
Take a look at [example.js](https://github.com/pedroslopez/whatsapp-web.js/blob/master/example.js) for another example with more use cases.
-## Remote Access
+For more information on saving and restoring sessions, check out the available [Authentication Strategies](https://wwebjs.dev/guide/authentication.html).
-You could also connect to any previously existing browser instance:
-
-```js
-const client = new Client({
- puppeteer: {
- browserWSEndpoint: `ws://localhost:3000`
- }
-});
-```
-
-### Docker
-
-1) Installing a browser using browserless:
-
-```
-docker run \
- --rm \
- -p 3000:3000 \
- -e "MAX_CONCURRENT_SESSIONS=1" \
- browserless/chrome:latest
-```
-
-Reference: https://docs.browserless.io/docs/docker-quickstart.html
-
-### Remote Debugging
-
-2) Running a browser with websocket remote debugging enabled:
-> chrome.exe --remote-debugging-port=9222
-
-After that check the following webpage and check http://127.0.0.1:9220/json and get the **webSocketDebuggerUrl**
## Supported features
| Feature | Status |
| ------------- | ------------- |
+| Multi Device | ✅ |
| Send messages | ✅ |
| Receive messages | ✅ |
| Send media (images/audio/documents) | ✅ |
@@ -116,11 +87,13 @@ Something missing? Make an issue and let us know!
Pull requests are welcome! If you see something you'd like to add, please do. For drastic changes, please open an issue first.
-## Donating
+## Supporting the project
-You can support the maintainer of this project through the link below
+You can support the maintainer of this project through the links below
-[](https://www.paypal.me/psla/)
+- [Support via GitHub Sponsors](https://github.com/sponsors/pedroslopez)
+- [Support via PayPal](https://www.paypal.me/psla/)
+- [Sign up for DigitalOcean](https://m.do.co/c/73f906a36ed4) and get $100 in credit when you sign up (Referral)
## Disclaimer
diff --git a/docs/Base.html b/docs/Base.html
index 431e533..f0946b9 100644
--- a/docs/Base.html
+++ b/docs/Base.html
@@ -4,7 +4,7 @@
Sets the current user's display name.
This is the name shown to WhatsApp users that have not added you as a contact beside your number in groups and in your profile.
@@ -1524,6 +1506,10 @@
+
Returns
+
+
Promise containing Boolean
+
async
setStatus(status)
@@ -1650,104 +1636,10 @@
authenticated
Emitted when authentication is successful
-
-
Parameters
-
-
-
-
Name
-
Type
-
Optional
-
Description
-
-
-
-
-
-
session
-
-
-
object
-
-
-
-
-
-
Object containing session information. Can be used to restore the session.
-
Values in session have the following properties:
-
-
-
-
Name
-
Type
-
Optional
-
Description
-
-
-
-
-
-
WABrowserId
-
-
-
string
-
-
-
-
-
-
-
-
-
-
WASecretBundle
-
-
-
string
-
-
-
-
-
-
-
-
-
-
WAToken1
-
-
-
string
-
-
-
-
-
-
-
-
-
-
WAToken2
-
-
-
string
-
-
-
-
-
-
-
-
-
-
-
-
-
-
change_battery
-
Emitted when the battery percentage for the attached device changes
+
Emitted when the battery percentage for the attached device changes. Will not be sent if using multi-device.
Parameters
@@ -1818,6 +1710,8 @@
+
Deprecated
+
change_state
Emitted when the connection state changes
@@ -2370,7 +2264,7 @@
qr
-
Emitted when the QR code is received
+
Emitted when a QR code is received
Parameter
@@ -2416,7 +2310,7 @@
diff --git a/docs/Client.js.html b/docs/Client.js.html
index 5616fe9..763cd64 100644
--- a/docs/Client.js.html
+++ b/docs/Client.js.html
@@ -4,7 +4,7 @@
- whatsapp-web.js 1.15.6 » Source: Client.js
+ whatsapp-web.js 1.17.0 » Source: Client.js
@@ -15,7 +15,7 @@
@@ -34,7 +34,6 @@
const EventEmitter = require('events');
const puppeteer = require('puppeteer');
const moduleRaid = require('@pedroslopez/moduleraid/moduleraid');
-const jsQR = require('jsqr');
const Util = require('./util/Util');
const InterfaceController = require('./util/InterfaceController');
@@ -42,22 +41,20 @@ const { WhatsWebURL, DefaultOptions, Events, WAState } = require('./ut
const { ExposeStore, LoadUtils } = require('./util/Injected');
const ChatFactory = require('./factories/ChatFactory');
const ContactFactory = require('./factories/ContactFactory');
-const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification , Label, Call, Buttons, List} = require('./structures');
+const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List} = require('./structures');
+const LegacySessionAuth = require('./authStrategies/LegacySessionAuth');
+const NoAuth = require('./authStrategies/NoAuth');
+
/**
* Starting point for interacting with the WhatsApp Web API
* @extends {EventEmitter}
* @param {object} options - Client options
+ * @param {AuthStrategy} options.authStrategy - Determines how to save and restore sessions. Will use LegacySessionAuth if options.session is set. Otherwise, NoAuth will be used.
* @param {number} options.authTimeoutMs - Timeout for authentication selector in puppeteer
* @param {object} options.puppeteer - Puppeteer launch options. View docs here: https://github.com/puppeteer/puppeteer/
- * @param {number} options.qrRefreshIntervalMs - Refresh interval for qr code (how much time to wait before checking if the qr code has changed)
- * @param {number} options.qrTimeoutMs - Timeout for qr code selector in puppeteer
* @param {number} options.qrMaxRetries - How many times should the qrcode be refreshed before giving up
- * @param {string} options.restartOnAuthFail - Restart client with a new session (i.e. use null 'session' var) if authentication fails
- * @param {object} options.session - Whatsapp session to restore. If not set, will start a new session
- * @param {string} options.session.WABrowserId
- * @param {string} options.session.WASecretBundle
- * @param {string} options.session.WAToken1
- * @param {string} options.session.WAToken2
+ * @param {string} options.restartOnAuthFail - @deprecated This option should be set directly on the LegacySessionAuth.
+ * @param {object} options.session - @deprecated Only here for backwards-compatibility. You should move to using LocalAuth, or set the authStrategy to LegacySessionAuth explicitly.
* @param {number} options.takeoverOnConflict - If another whatsapp web session is detected (another browser), take over the session in the current browser
* @param {number} options.takeoverTimeoutMs - How much time to wait before taking over the session
* @param {string} options.userAgent - User agent to use in puppeteer
@@ -79,13 +76,33 @@ const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification
* @fires Client#group_update
* @fires Client#disconnected
* @fires Client#change_state
- * @fires Client#change_battery
*/
class Client extends EventEmitter {
constructor(options = {}) {
super();
this.options = Util.mergeDefault(DefaultOptions, options);
+
+ if(!this.options.authStrategy) {
+ if(Object.prototype.hasOwnProperty.call(this.options, 'session')) {
+ process.emitWarning(
+ 'options.session is deprecated and will be removed in a future release due to incompatibility with multi-device. ' +
+ 'Use the LocalAuth authStrategy, don\'t pass in a session as an option, or suppress this warning by using the LegacySessionAuth strategy explicitly (see https://wwebjs.dev/guide/authentication.html#legacysessionauth-strategy).',
+ 'DeprecationWarning'
+ );
+
+ this.authStrategy = new LegacySessionAuth({
+ session: this.options.session,
+ restartOnAuthFail: this.options.restartOnAuthFail
+ });
+ } else {
+ this.authStrategy = new NoAuth();
+ }
+ } else {
+ this.authStrategy = this.options.authStrategy;
+ }
+
+ this.authStrategy.setup(this);
this.pupBrowser = null;
this.pupPage = null;
@@ -98,41 +115,30 @@ class Client extends EventEmitter {
*/
async initialize() {
let [browser, page] = [null, null];
-
- if(this.options.puppeteer && this.options.puppeteer.browserWSEndpoint) {
- browser = await puppeteer.connect(this.options.puppeteer);
+
+ await this.authStrategy.beforeBrowserInitialized();
+
+ const puppeteerOpts = this.options.puppeteer;
+ if (puppeteerOpts && puppeteerOpts.browserWSEndpoint) {
+ browser = await puppeteer.connect(puppeteerOpts);
page = await browser.newPage();
} else {
- browser = await puppeteer.launch(this.options.puppeteer);
+ const browserArgs = [...(puppeteerOpts.args || [])];
+ if(!browserArgs.find(arg => arg.includes('--user-agent'))) {
+ browserArgs.push(`--user-agent=${this.options.userAgent}`);
+ }
+
+ browser = await puppeteer.launch({...puppeteerOpts, args: browserArgs});
page = (await browser.pages())[0];
}
-
+
await page.setUserAgent(this.options.userAgent);
+ if (this.options.bypassCSP) await page.setBypassCSP(true);
this.pupBrowser = browser;
this.pupPage = page;
- // remember me
- await page.evaluateOnNewDocument(() => {
- localStorage.setItem('remember-me', 'true');
- });
-
- if (this.options.session) {
- await page.evaluateOnNewDocument(
- session => {
- if(document.referrer === 'https://whatsapp.com/') {
- localStorage.clear();
- localStorage.setItem('WABrowserId', session.WABrowserId);
- localStorage.setItem('WASecretBundle', session.WASecretBundle);
- localStorage.setItem('WAToken1', session.WAToken1);
- localStorage.setItem('WAToken2', session.WAToken2);
- }
- }, this.options.session);
- }
-
- if(this.options.bypassCSP) {
- await page.setBypassCSP(true);
- }
+ await this.authStrategy.afterBrowserInitialized();
await page.goto(WhatsWebURL, {
waitUntil: 'load',
@@ -140,56 +146,54 @@ class Client extends EventEmitter {
referer: 'https://whatsapp.com/'
});
- const KEEP_PHONE_CONNECTED_IMG_SELECTOR = '[data-icon="intro-md-beta-logo-dark"], [data-icon="intro-md-beta-logo-light"], [data-asset-intro-image-light="true"], [data-asset-intro-image-dark="true"]';
+ 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"]';
+ const INTRO_QRCODE_SELECTOR = 'div[data-ref] canvas';
- if (this.options.session) {
- // Check if session restore was successful
- try {
- await page.waitForSelector(KEEP_PHONE_CONNECTED_IMG_SELECTOR, { timeout: this.options.authTimeoutMs });
- } catch (err) {
- if (err.name === 'TimeoutError') {
- /**
- * Emitted when there has been an error while trying to restore an existing session
- * @event Client#auth_failure
- * @param {string} message
- */
- this.emit(Events.AUTHENTICATION_FAILURE, 'Unable to log in. Are the session details valid?');
- browser.close();
- if (this.options.restartOnAuthFail) {
- // session restore failed so try again but without session to force new authentication
- this.options.session = null;
- this.initialize();
- }
- return;
+ // Checks which selector appears first
+ const needAuthentication = await Promise.race([
+ new Promise(resolve => {
+ page.waitForSelector(INTRO_IMG_SELECTOR, { timeout: this.options.authTimeoutMs })
+ .then(() => resolve(false))
+ .catch((err) => resolve(err));
+ }),
+ new Promise(resolve => {
+ page.waitForSelector(INTRO_QRCODE_SELECTOR, { timeout: this.options.authTimeoutMs })
+ .then(() => resolve(true))
+ .catch((err) => resolve(err));
+ })
+ ]);
+
+ // Checks if an error ocurred on the first found selector. The second will be discarded and ignored by .race;
+ if (needAuthentication instanceof Error) throw needAuthentication;
+
+ // Scan-qrcode selector was found. Needs authentication
+ if (needAuthentication) {
+ const { failed, failureEventPayload, restart } = await this.authStrategy.onAuthenticationNeeded();
+ if(failed) {
+ /**
+ * Emitted when there has been an error while trying to restore an existing session
+ * @event Client#auth_failure
+ * @param {string} message
+ */
+ this.emit(Events.AUTHENTICATION_FAILURE, failureEventPayload);
+ await this.destroy();
+ if (restart) {
+ // session restore failed so try again but without session to force new authentication
+ return this.initialize();
}
-
- throw err;
+ return;
}
- } else {
+ const QR_CONTAINER = 'div[data-ref]';
+ const QR_RETRY_BUTTON = 'div[data-ref] > span > button';
let qrRetries = 0;
-
- const getQrCode = async () => {
- // Check if retry button is present
- var QR_RETRY_SELECTOR = 'div[data-ref] > span > button';
- var qrRetry = await page.$(QR_RETRY_SELECTOR);
- if (qrRetry) {
- await qrRetry.click();
- }
-
- // Wait for QR Code
- const QR_CANVAS_SELECTOR = 'canvas';
- await page.waitForSelector(QR_CANVAS_SELECTOR, { timeout: this.options.qrTimeoutMs });
- const qrImgData = await page.$eval(QR_CANVAS_SELECTOR, canvas => [].slice.call(canvas.getContext('2d').getImageData(0, 0, 264, 264).data));
- const qr = jsQR(qrImgData, 264, 264).data;
-
+ await page.exposeFunction('qrChanged', async (qr) => {
/**
- * Emitted when the QR code is received
+ * Emitted when a QR code is received
* @event Client#qr
* @param {string} qr QR Code
*/
this.emit(Events.QR_RECEIVED, qr);
-
if (this.options.qrMaxRetries > 0) {
qrRetries++;
if (qrRetries > this.options.qrMaxRetries) {
@@ -197,15 +201,39 @@ class Client extends EventEmitter {
await this.destroy();
}
}
- };
- getQrCode();
- this._qrRefreshInterval = setInterval(getQrCode, this.options.qrRefreshIntervalMs);
+ });
+
+ await page.evaluate(function (selectors) {
+ const qr_container = document.querySelector(selectors.QR_CONTAINER);
+ window.qrChanged(qr_container.dataset.ref);
+
+ const obs = new MutationObserver((muts) => {
+ muts.forEach(mut => {
+ // Listens to qr token change
+ if (mut.type === 'attributes' && mut.attributeName === 'data-ref') {
+ window.qrChanged(mut.target.dataset.ref);
+ } else
+ // Listens to retry button, when found, click it
+ if (mut.type === 'childList') {
+ const retry_button = document.querySelector(selectors.QR_RETRY_BUTTON);
+ if (retry_button) retry_button.click();
+ }
+ });
+ });
+ obs.observe(qr_container.parentElement, {
+ subtree: true,
+ childList: true,
+ attributes: true,
+ attributeFilter: ['data-ref'],
+ });
+ }, {
+ QR_CONTAINER,
+ QR_RETRY_BUTTON
+ });
// Wait for code scan
try {
- await page.waitForSelector(KEEP_PHONE_CONNECTED_IMG_SELECTOR, { timeout: 0 });
- clearInterval(this._qrRefreshInterval);
- this._qrRefreshInterval = undefined;
+ await page.waitForSelector(INTRO_IMG_SELECTOR, { timeout: 0 });
} catch(error) {
if (
error.name === 'ProtocolError' &&
@@ -218,44 +246,29 @@ class Client extends EventEmitter {
throw error;
}
+
}
await page.evaluate(ExposeStore, moduleRaid.toString());
-
- // Get session tokens
- const localStorage = JSON.parse(await page.evaluate(() => {
- return JSON.stringify(window.localStorage);
- }));
-
- const session = {
- WABrowserId: localStorage.WABrowserId,
- WASecretBundle: localStorage.WASecretBundle,
- WAToken1: localStorage.WAToken1,
- WAToken2: localStorage.WAToken2
- };
+ const authEventPayload = await this.authStrategy.getAuthEventPayload();
/**
* Emitted when authentication is successful
* @event Client#authenticated
- * @param {object} session Object containing session information. Can be used to restore the session.
- * @param {string} session.WABrowserId
- * @param {string} session.WASecretBundle
- * @param {string} session.WAToken1
- * @param {string} session.WAToken2
*/
- this.emit(Events.AUTHENTICATED, session);
+ this.emit(Events.AUTHENTICATED, authEventPayload);
// Check window.Store Injection
await page.waitForFunction('window.Store != undefined');
- const isMD = await page.evaluate(() => {
- return window.Store.Features.features.MD_BACKEND;
+ await page.evaluate(async () => {
+ // safely unregister service workers
+ const registrations = await navigator.serviceWorker.getRegistrations();
+ for (let registration of registrations) {
+ registration.unregister();
+ }
});
- if(isMD) {
- throw new Error('Multi-device is not yet supported by whatsapp-web.js. Please check out https://github.com/pedroslopez/whatsapp-web.js/pull/889 to follow the progress.');
- }
-
//Load util functions (serializers, helper functions)
await page.evaluate(LoadUtils);
@@ -265,7 +278,7 @@ class Client extends EventEmitter {
* @type {ClientInfo}
*/
this.info = new ClientInfo(this, await page.evaluate(() => {
- return window.Store.Conn.serialize();
+ return { ...window.Store.Conn.serialize(), wid: window.Store.User.getMeUser() };
}));
// Add InterfaceController
@@ -429,11 +442,12 @@ class Client extends EventEmitter {
if (battery === undefined) return;
/**
- * Emitted when the battery percentage for the attached device changes
+ * Emitted when the battery percentage for the attached device changes. Will not be sent if using multi-device.
* @event Client#change_battery
* @param {object} batteryInfo
* @param {number} batteryInfo.battery - The current battery percentage
* @param {boolean} batteryInfo.plugged - Indicates if the phone is plugged in (true) or not (false)
+ * @deprecated
*/
this.emit(Events.BATTERY_CHANGED, { battery, plugged });
});
@@ -452,14 +466,14 @@ class Client extends EventEmitter {
* @param {boolean} call.webClientShouldHandle - If Waweb should handle
* @param {object} call.participants - Participants
*/
- const cll = new Call(this,call);
+ const cll = new Call(this, call);
this.emit(Events.INCOMING_CALL, cll);
});
-
+
await page.evaluate(() => {
window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(window.WWebJS.getMessageModel(msg)); });
window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(window.WWebJS.getMessageModel(msg)); });
- window.Store.Msg.on('change:ack', (msg,ack) => { window.onMessageAckEvent(window.WWebJS.getMessageModel(msg), ack); });
+ window.Store.Msg.on('change:ack', (msg, ack) => { window.onMessageAckEvent(window.WWebJS.getMessageModel(msg), ack); });
window.Store.Msg.on('change:isUnsentMedia', (msg, unsent) => { if (msg.id.fromMe && !unsent) window.onMessageMediaUploadedEvent(window.WWebJS.getMessageModel(msg)); });
window.Store.Msg.on('remove', (msg) => { if (msg.isNewMsg) window.onRemoveMessageEvent(window.WWebJS.getMessageModel(msg)); });
window.Store.AppState.on('change:state', (_AppState, state) => { window.onAppStateChangedEvent(state); });
@@ -497,9 +511,6 @@ class Client extends EventEmitter {
* Closes the client
*/
async destroy() {
- if (this._qrRefreshInterval) {
- clearInterval(this._qrRefreshInterval);
- }
await this.pupBrowser.close();
}
@@ -507,9 +518,11 @@ class Client extends EventEmitter {
* Logs out the client, closing the current session
*/
async logout() {
- return await this.pupPage.evaluate(() => {
+ await this.pupPage.evaluate(() => {
return window.Store.AppState.logout();
});
+
+ await this.authStrategy.logout();
}
/**
@@ -539,7 +552,7 @@ class Client extends EventEmitter {
/**
* Message options.
* @typedef {Object} MessageSendOptions
- * @property {boolean} [linkPreview=true] - Show links preview
+ * @property {boolean} [linkPreview=true] - Show links preview. Has no effect on multi-device accounts.
* @property {boolean} [sendAudioAsVoice=false] - Send audio as voice message
* @property {boolean} [sendVideoAsGif=false] - Send video as gif
* @property {boolean} [sendMediaAsSticker=false] - Send media as a sticker
@@ -589,34 +602,36 @@ class Client extends EventEmitter {
} else if (content instanceof Location) {
internalOptions.location = content;
content = '';
- } else if(content instanceof Contact) {
+ } else if (content instanceof Contact) {
internalOptions.contactCard = content.id._serialized;
content = '';
- } else if(Array.isArray(content) && content.length > 0 && content[0] instanceof Contact) {
+ } else if (Array.isArray(content) && content.length > 0 && content[0] instanceof Contact) {
internalOptions.contactCardList = content.map(contact => contact.id._serialized);
content = '';
- } else if(content instanceof Buttons){
- if(content.type !== 'chat'){internalOptions.attachment = content.body;}
+ } else if (content instanceof Buttons) {
+ if (content.type !== 'chat') { internalOptions.attachment = content.body; }
internalOptions.buttons = content;
content = '';
- } else if(content instanceof List){
+ } else if (content instanceof List) {
internalOptions.list = content;
content = '';
}
-
+
if (internalOptions.sendMediaAsSticker && internalOptions.attachment) {
- internalOptions.attachment =
- await Util.formatToWebpSticker(internalOptions.attachment, {
+ internalOptions.attachment = await Util.formatToWebpSticker(
+ internalOptions.attachment, {
name: options.stickerName,
author: options.stickerAuthor,
categories: options.stickerCategories
- });
+ }, this.pupPage
+ );
}
const newMessage = await this.pupPage.evaluate(async (chatId, message, options, sendSeen) => {
const chatWid = window.Store.WidFactory.createWid(chatId);
const chat = await window.Store.Chat.find(chatWid);
+
if (sendSeen) {
window.WWebJS.sendSeen(chatId);
}
@@ -703,7 +718,7 @@ class Client extends EventEmitter {
*/
async getInviteInfo(inviteCode) {
return await this.pupPage.evaluate(inviteCode => {
- return window.Store.Wap.groupInviteInfo(inviteCode);
+ return window.Store.InviteInfo.sendQueryGroupInvite(inviteCode);
}, inviteCode);
}
@@ -722,25 +737,25 @@ class Client extends EventEmitter {
/**
* Accepts a private invitation to join a group
- * @param {object} inviteV4 Invite V4 Info
+ * @param {object} inviteInfo Invite V4 Info
* @returns {Promise<Object>}
*/
async acceptGroupV4Invite(inviteInfo) {
- if(!inviteInfo.inviteCode) throw 'Invalid invite code, try passing the message.inviteV4 object';
+ if (!inviteInfo.inviteCode) throw 'Invalid invite code, try passing the message.inviteV4 object';
if (inviteInfo.inviteCodeExp == 0) throw 'Expired invite code';
- return await this.pupPage.evaluate(async inviteInfo => {
- let { groupId, fromId, inviteCode, inviteCodeExp, toId } = inviteInfo;
- return await window.Store.Wap.acceptGroupV4Invite(groupId, fromId, inviteCode, String(inviteCodeExp), toId);
+ return this.pupPage.evaluate(async inviteInfo => {
+ let { groupId, fromId, inviteCode, inviteCodeExp } = inviteInfo;
+ return await window.Store.JoinInviteV4.sendJoinGroupViaInviteV4(inviteCode, String(inviteCodeExp), groupId, fromId);
}, inviteInfo);
}
-
+
/**
* Sets the current user's status message
* @param {string} status New status message
*/
async setStatus(status) {
await this.pupPage.evaluate(async status => {
- return await window.Store.Wap.sendSetStatus(status);
+ return await window.Store.StatusUtils.setMyStatus(status);
}, status);
}
@@ -748,13 +763,24 @@ class Client extends EventEmitter {
* Sets the current user's display name.
* This is the name shown to WhatsApp users that have not added you as a contact beside your number in groups and in your profile.
* @param {string} displayName New display name
+ * @returns {Promise<Boolean>}
*/
async setDisplayName(displayName) {
- await this.pupPage.evaluate(async displayName => {
- return await window.Store.Wap.setPushname(displayName);
- }, displayName);
- }
+ const couldSet = await this.pupPage.evaluate(async displayName => {
+ if(!window.Store.Conn.canSetMyPushname()) return false;
+ if(window.Store.MDBackend) {
+ // TODO
+ return false;
+ } else {
+ const res = await window.Store.Wap.setPushname(displayName);
+ return !res.status || res.status === 200;
+ }
+ }, displayName);
+
+ return couldSet;
+ }
+
/**
* Gets the current connection state for the client
* @returns {WAState}
@@ -771,7 +797,16 @@ class Client extends EventEmitter {
*/
async sendPresenceAvailable() {
return await this.pupPage.evaluate(() => {
- return window.Store.Wap.sendPresenceAvailable();
+ return window.Store.PresenceUtils.sendPresenceAvailable();
+ });
+ }
+
+ /**
+ * Marks the client as unavailable
+ */
+ async sendPresenceUnavailable() {
+ return await this.pupPage.evaluate(() => {
+ return window.Store.PresenceUtils.sendPresenceUnavailable();
});
}
@@ -810,8 +845,9 @@ class Client extends EventEmitter {
return true;
}
const MAX_PIN_COUNT = 3;
- if (window.Store.Chat.models.length > MAX_PIN_COUNT) {
- let maxPinned = window.Store.Chat.models[MAX_PIN_COUNT - 1].pin;
+ const chatModels = window.Store.Chat.getModelsArray();
+ if (chatModels.length > MAX_PIN_COUNT) {
+ let maxPinned = chatModels[MAX_PIN_COUNT - 1].pin;
if (maxPinned) {
return false;
}
@@ -877,13 +913,43 @@ class Client extends EventEmitter {
* @returns {Promise<string>}
*/
async getProfilePicUrl(contactId) {
- const profilePic = await this.pupPage.evaluate((contactId) => {
- return window.Store.Wap.profilePicFind(contactId);
+ const profilePic = await this.pupPage.evaluate(async contactId => {
+ try {
+ const chatWid = window.Store.WidFactory.createWid(contactId);
+ return await window.Store.ProfilePic.profilePicFind(chatWid);
+ } catch (err) {
+ if(err.name === 'ServerStatusCodeError') return undefined;
+ throw err;
+ }
}, contactId);
-
+
return profilePic ? profilePic.eurl : undefined;
}
+ /**
+ * Gets the Contact's common groups with you. Returns empty array if you don't have any common group.
+ * @param {string} contactId the whatsapp user's ID (_serialized format)
+ * @returns {Promise<WAWebJS.ChatId[]>}
+ */
+ async getCommonGroups(contactId) {
+ const commonGroups = await this.pupPage.evaluate(async (contactId) => {
+ const contact = window.Store.Contact.get(contactId);
+ if (contact.commonGroups) {
+ return contact.commonGroups.serialize();
+ }
+ const status = await window.Store.findCommonGroups(contact);
+ if (status) {
+ return contact.commonGroups.serialize();
+ }
+ return [];
+ }, contactId);
+ const chats = [];
+ for (const group of commonGroups) {
+ chats.push(group.id);
+ }
+ return chats;
+ }
+
/**
* Force reset of connection state for the client
*/
@@ -899,10 +965,7 @@ class Client extends EventEmitter {
* @returns {Promise<Boolean>}
*/
async isRegisteredUser(id) {
- return await this.pupPage.evaluate(async (id) => {
- let result = await window.Store.Wap.queryExist(id);
- return result.jid !== undefined;
- }, id);
+ return Boolean(await this.getNumberId(id));
}
/**
@@ -912,14 +975,15 @@ class Client extends EventEmitter {
* @returns {Promise<Object|null>}
*/
async getNumberId(number) {
- if (!number.endsWith('@c.us')) number += '@c.us';
- try {
- return await this.pupPage.evaluate(async numberId => {
- return window.WWebJS.getNumberId(numberId);
- }, number);
- } catch(_) {
- return null;
+ if (!number.endsWith('@c.us')) {
+ number += '@c.us';
}
+
+ return await this.pupPage.evaluate(async number => {
+ const result = await window.Store.QueryExist(number);
+ if (!result || result.wid === undefined) return null;
+ return result.wid;
+ }, number);
}
/**
@@ -928,14 +992,14 @@ class Client extends EventEmitter {
* @returns {Promise<string>}
*/
async getFormattedNumber(number) {
- if(!number.endsWith('@s.whatsapp.net')) number = number.replace('c.us', 's.whatsapp.net');
- if(!number.includes('@s.whatsapp.net')) number = `${number}@s.whatsapp.net`;
-
+ if (!number.endsWith('@s.whatsapp.net')) number = number.replace('c.us', 's.whatsapp.net');
+ if (!number.includes('@s.whatsapp.net')) number = `${number}@s.whatsapp.net`;
+
return await this.pupPage.evaluate(async numberId => {
return window.Store.NumberInfo.formattedPhoneNumber(numberId);
}, number);
}
-
+
/**
* Get the country code of a WhatsApp ID.
* @param {string} number Number or ID
@@ -948,7 +1012,7 @@ class Client extends EventEmitter {
return window.Store.NumberInfo.findCC(numberId);
}, number);
}
-
+
/**
* Create a new group
* @param {string} name group title
@@ -967,12 +1031,9 @@ class Client extends EventEmitter {
}
const createRes = await this.pupPage.evaluate(async (name, participantIds) => {
- const res = await window.Store.Wap.createGroup(name, participantIds);
- console.log(res);
- if (!res.status === 200) {
- throw 'An error occurred while creating the group!';
- }
-
+ const participantWIDs = participantIds.map(p => window.Store.WidFactory.createWid(p));
+ const id = window.Store.MsgKey.newId();
+ const res = await window.Store.GroupUtils.sendCreateGroup(name, participantWIDs, undefined, id);
return res;
}, name, participants);
@@ -993,9 +1054,9 @@ class Client extends EventEmitter {
async getLabels() {
const labels = await this.pupPage.evaluate(async () => {
return window.WWebJS.getLabels();
- });
+ });
- return labels.map(data => new Label(this , data));
+ return labels.map(data => new Label(this, data));
}
/**
@@ -1006,7 +1067,7 @@ class Client extends EventEmitter {
async getLabelById(labelId) {
const label = await this.pupPage.evaluate(async (labelId) => {
return window.WWebJS.getLabel(labelId);
- }, labelId);
+ }, labelId);
return new Label(this, label);
}
@@ -1016,12 +1077,12 @@ class Client extends EventEmitter {
* @param {string} chatId
* @returns {Promise<Array<Label>>}
*/
- async getChatLabels(chatId){
+ async getChatLabels(chatId) {
const labels = await this.pupPage.evaluate(async (chatId) => {
return window.WWebJS.getChatLabels(chatId);
}, chatId);
- return labels.map(data => new Label(this, data));
+ return labels.map(data => new Label(this, data));
}
/**
@@ -1029,16 +1090,16 @@ class Client extends EventEmitter {
* @param {string} labelId
* @returns {Promise<Array<Chat>>}
*/
- async getChatsByLabelId(labelId){
+ async getChatsByLabelId(labelId) {
const chatIds = await this.pupPage.evaluate(async (labelId) => {
const label = window.Store.Label.get(labelId);
- const labelItems = label.labelItemCollection.models;
+ const labelItems = label.labelItemCollection.getModelsArray();
return labelItems.reduce((result, item) => {
- if(item.parentType === 'Chat'){
+ if (item.parentType === 'Chat') {
result.push(item.parentId);
}
return result;
- },[]);
+ }, []);
}, labelId);
return Promise.all(chatIds.map(id => this.getChatById(id)));
@@ -1050,8 +1111,8 @@ class Client extends EventEmitter {
*/
async getBlockedContacts() {
const blockedContacts = await this.pupPage.evaluate(() => {
- let chatIds = window.Store.Blocklist.models.map(a => a.id._serialized);
- return Promise.all(chatIds.map(id => window.WWebJS.getContact(id)));
+ let chatIds = window.Store.Blocklist.getModelsArray().map(a => a.id._serialized);
+ return Promise.all(chatIds.map(id => window.WWebJS.getContact(id)));
});
return blockedContacts.map(contact => ContactFactory.create(this.client, contact));
@@ -1069,7 +1130,7 @@ module.exports = Client;
diff --git a/docs/ClientInfo.html b/docs/ClientInfo.html
index 1ea8a5e..dc67b2d 100644
--- a/docs/ClientInfo.html
+++ b/docs/ClientInfo.html
@@ -4,7 +4,7 @@
- whatsapp-web.js 1.15.6 » Class: ClientInfo
+ whatsapp-web.js 1.17.0 » Class: ClientInfo
@@ -15,7 +15,7 @@
@@ -101,7 +101,7 @@
phone
object
-
Information about the phone this client is connected to
+
Information about the phone this client is connected to. Not available in multi-device.
Properties
@@ -188,10 +188,12 @@
+
Deprecated
+
platform
string
-
Platform the phone is running on
+
Platform WhatsApp is running on
pushname
@@ -211,6 +213,8 @@
getBatteryStatus() → (object, number, or boolean)
Get current battery percentage and charging status for the attached device
Reloads this Message object's data in-place with the latest values from WhatsApp Web.
+ Note that the Message must still be in the web app cache for this to work, otherwise will return null.
Sends a message as a reply to this message. If chatId is specified, it will be sent
through the specified Chat. If not, it will send the message
@@ -580,7 +650,7 @@
diff --git a/docs/MessageMedia.html b/docs/MessageMedia.html
index 0442fd7..0ba5c86 100644
--- a/docs/MessageMedia.html
+++ b/docs/MessageMedia.html
@@ -4,7 +4,7 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/authStrategies_LocalAuth.js.html b/docs/authStrategies_LocalAuth.js.html
new file mode 100644
index 0000000..c2a9a7e
--- /dev/null
+++ b/docs/authStrategies_LocalAuth.js.html
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+ whatsapp-web.js 1.17.0 » Source: authStrategies/LocalAuth.js
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Source: authStrategies/LocalAuth.js
+
+
+
'use strict';
+
+const path = require('path');
+const fs = require('fs');
+const BaseAuthStrategy = require('./BaseAuthStrategy');
+
+/**
+ * Local directory-based authentication
+ * @param {object} options - options
+ * @param {string} options.clientId - Client id to distinguish instances if you are using multiple, otherwise keep null if you are using only one instance
+ * @param {string} options.dataPath - Change the default path for saving session files, default is: "./.wwebjs_auth/"
+*/
+class LocalAuth extends BaseAuthStrategy {
+ constructor({ clientId, dataPath }={}) {
+ super();
+
+ const idRegex = /^[-_\w]+$/i;
+ if(clientId && !idRegex.test(clientId)) {
+ throw new Error('Invalid clientId. Only alphanumeric characters, underscores and hyphens are allowed.');
+ }
+
+ this.dataPath = path.resolve(dataPath || './.wwebjs_auth/');
+ this.clientId = clientId;
+ }
+
+ async beforeBrowserInitialized() {
+ const puppeteerOpts = this.client.options.puppeteer;
+ const sessionDirName = this.clientId ? `session-${this.clientId}` : 'session';
+ const dirPath = path.join(this.dataPath, sessionDirName);
+
+ if(puppeteerOpts.userDataDir && puppeteerOpts.userDataDir !== dirPath) {
+ throw new Error('LocalAuth is not compatible with a user-supplied userDataDir.');
+ }
+
+ fs.mkdirSync(dirPath, { recursive: true });
+
+ this.client.options.puppeteer = {
+ ...puppeteerOpts,
+ userDataDir: dirPath
+ };
+
+ this.userDataDir = dirPath;
+ }
+
+ async logout() {
+ if (this.userDataDir) {
+ return (fs.rmSync ? fs.rmSync : fs.rmdirSync).call(this, this.userDataDir, { recursive: true });
+ }
+ }
+
+}
+
+module.exports = LocalAuth;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/authStrategies_NoAuth.js.html b/docs/authStrategies_NoAuth.js.html
new file mode 100644
index 0000000..68a6acb
--- /dev/null
+++ b/docs/authStrategies_NoAuth.js.html
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+ whatsapp-web.js 1.17.0 » Source: authStrategies/NoAuth.js
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Source: authStrategies/NoAuth.js
+
+
+
'use strict';
+
+const BaseAuthStrategy = require('./BaseAuthStrategy');
+
+/**
+ * No session restoring functionality
+ * Will need to authenticate via QR code every time
+*/
+class NoAuth extends BaseAuthStrategy { }
+
+
+module.exports = NoAuth;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/global.html b/docs/global.html
index b373bf7..353f699 100644
--- a/docs/global.html
+++ b/docs/global.html
@@ -4,7 +4,7 @@
- whatsapp-web.js 1.15.6 » Globals
+ whatsapp-web.js 1.17.0 » Globals
@@ -15,7 +15,7 @@
@@ -1714,7 +1714,7 @@
Yes
-
Show links preview
+
Show links preview. Has no effect on multi-device accounts.
This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with WhatsApp or any of its subsidiaries or its affiliates. The official WhatsApp website can be found at https://whatsapp.com. "WhatsApp" as well as related names, marks, emblems and images are registered trademarks of their respective owners.