mirror of
https://github.com/cheveguerra/whatsapp-web.js.git
synced 2026-04-18 03:29:14 +00:00
Merge branch 'main' into patch-participants
This commit is contained in:
1
.github/workflows/lint.yml
vendored
1
.github/workflows/lint.yml
vendored
@@ -2,7 +2,6 @@ name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
eslint:
|
||||
|
||||
@@ -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
|
||||
|
||||
1
index.d.ts
vendored
1
index.d.ts
vendored
@@ -443,6 +443,7 @@ declare namespace WAWebJS {
|
||||
PAYMENT = 'payment',
|
||||
UNKNOWN = 'unknown',
|
||||
GROUP_INVITE = 'groups_v4_invite',
|
||||
BUTTONS_RESPONSE = 'buttons_response'
|
||||
}
|
||||
|
||||
/** Client status */
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"main": "./index.js",
|
||||
"typings": "./index.d.ts",
|
||||
"scripts": {
|
||||
"test": "mocha tests",
|
||||
"test": "mocha tests --recursive",
|
||||
"test-single": "mocha",
|
||||
"shell": "node --experimental-repl-await ./shell.js",
|
||||
"generate-docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose"
|
||||
},
|
||||
@@ -40,13 +41,13 @@
|
||||
"devDependencies": {
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"chai": "^4.3.4",
|
||||
"dotenv": "^10.0.0",
|
||||
"dotenv": "^16.0.0",
|
||||
"eslint": "^8.4.1",
|
||||
"eslint-plugin-mocha": "^9.0.0",
|
||||
"eslint-plugin-mocha": "^10.0.3",
|
||||
"jsdoc": "^3.6.4",
|
||||
"jsdoc-baseline": "^0.1.5",
|
||||
"mocha": "^9.0.2",
|
||||
"sinon": "^12.0.1"
|
||||
"sinon": "^13.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
|
||||
@@ -76,7 +76,7 @@ class Client extends EventEmitter {
|
||||
page = (await browser.pages())[0];
|
||||
}
|
||||
|
||||
page.setUserAgent(this.options.userAgent);
|
||||
await page.setUserAgent(this.options.userAgent);
|
||||
|
||||
this.pupBrowser = browser;
|
||||
this.pupPage = page;
|
||||
@@ -109,7 +109,7 @@ class Client extends EventEmitter {
|
||||
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"]';
|
||||
|
||||
if (this.options.session) {
|
||||
// Check if session restore was successfull
|
||||
// Check if session restore was successful
|
||||
try {
|
||||
await page.waitForSelector(KEEP_PHONE_CONNECTED_IMG_SELECTOR, { timeout: this.options.authTimeoutMs });
|
||||
} catch (err) {
|
||||
@@ -168,10 +168,22 @@ class Client extends EventEmitter {
|
||||
this._qrRefreshInterval = setInterval(getQrCode, this.options.qrRefreshIntervalMs);
|
||||
|
||||
// Wait for code scan
|
||||
await page.waitForSelector(KEEP_PHONE_CONNECTED_IMG_SELECTOR, { timeout: 0 });
|
||||
clearInterval(this._qrRefreshInterval);
|
||||
this._qrRefreshInterval = undefined;
|
||||
try {
|
||||
await page.waitForSelector(KEEP_PHONE_CONNECTED_IMG_SELECTOR, { timeout: 0 });
|
||||
clearInterval(this._qrRefreshInterval);
|
||||
this._qrRefreshInterval = undefined;
|
||||
} catch(error) {
|
||||
if (
|
||||
error.name === 'ProtocolError' &&
|
||||
error.message &&
|
||||
error.message.match(/Target closed/)
|
||||
) {
|
||||
// something has called .destroy() while waiting
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await page.evaluate(ExposeStore, moduleRaid.toString());
|
||||
@@ -430,11 +442,13 @@ class Client extends EventEmitter {
|
||||
*/
|
||||
this.emit(Events.READY);
|
||||
|
||||
// Disconnect when navigating away
|
||||
// Because WhatsApp Web now reloads when logging out from the device, this also covers that case
|
||||
// Disconnect when navigating away when in PAIRING state (detect logout)
|
||||
this.pupPage.on('framenavigated', async () => {
|
||||
this.emit(Events.DISCONNECTED, 'NAVIGATION');
|
||||
await this.destroy();
|
||||
const appState = await this.getState();
|
||||
if(appState === WAState.PAIRING) {
|
||||
this.emit(Events.DISCONNECTED, 'NAVIGATION');
|
||||
await this.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -727,7 +741,7 @@ class Client extends EventEmitter {
|
||||
return await this.pupPage.evaluate(async chatId => {
|
||||
let chat = await window.Store.Chat.get(chatId);
|
||||
await window.Store.Cmd.archiveChat(chat, true);
|
||||
return chat.archive;
|
||||
return true;
|
||||
}, chatId);
|
||||
}
|
||||
|
||||
@@ -739,7 +753,7 @@ class Client extends EventEmitter {
|
||||
return await this.pupPage.evaluate(async chatId => {
|
||||
let chat = await window.Store.Chat.get(chatId);
|
||||
await window.Store.Cmd.archiveChat(chat, false);
|
||||
return chat.archive;
|
||||
return false;
|
||||
}, chatId);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ class Chat extends Base {
|
||||
|
||||
/**
|
||||
* Indicates if the chat is muted or not
|
||||
* @type {number}
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.isMuted = data.isMuted;
|
||||
|
||||
|
||||
@@ -382,7 +382,7 @@ class Message extends Base {
|
||||
await this.client.pupPage.evaluate((msgId, everyone) => {
|
||||
let msg = window.Store.Msg.get(msgId);
|
||||
|
||||
if (everyone && msg.id.fromMe && msg.canRevoke()) {
|
||||
if (everyone && msg.id.fromMe && msg._canRevoke()) {
|
||||
return window.Store.Cmd.sendRevokeMsgs(msg.chat, [msg], true);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ exports.ExposeStore = (moduleRaidStr) => {
|
||||
window.Store = Object.assign({}, window.mR.findModule(m => m.default && m.default.Chat)[0].default);
|
||||
window.Store.AppState = window.mR.findModule('STREAM')[0].Socket;
|
||||
window.Store.Conn = window.mR.findModule('Conn')[0].Conn;
|
||||
window.Store.CryptoLib = window.mR.findModule('decryptE2EMedia')[0];
|
||||
window.Store.Wap = window.mR.findModule('queryLinkPreview')[0].default;
|
||||
window.Store.SendSeen = window.mR.findModule('sendSeen')[0];
|
||||
window.Store.SendClear = window.mR.findModule('sendClear')[0];
|
||||
@@ -31,7 +30,7 @@ exports.ExposeStore = (moduleRaidStr) => {
|
||||
window.Store.BlockContact = window.mR.findModule('blockContact')[0];
|
||||
window.Store.GroupMetadata = window.mR.findModule((module) => module.default && module.default.handlePendingInvite)[0].default;
|
||||
window.Store.UploadUtils = window.mR.findModule((module) => (module.default && module.default.encryptAndUpload) ? module.default : null)[0].default;
|
||||
window.Store.Label = window.mR.findModule('LabelCollection')[0].default;
|
||||
window.Store.Label = window.mR.findModule('LabelCollection')[0].LabelCollection;
|
||||
window.Store.Features = window.mR.findModule('FEATURE_CHANGE_EVENT')[0].GK;
|
||||
window.Store.QueryOrder = window.mR.findModule('queryOrder')[0];
|
||||
window.Store.QueryProduct = window.mR.findModule('queryProduct')[0];
|
||||
|
||||
162
tests/client.js
162
tests/client.js
@@ -7,7 +7,7 @@ const Contact = require('../src/structures/Contact');
|
||||
const Message = require('../src/structures/Message');
|
||||
const MessageMedia = require('../src/structures/MessageMedia');
|
||||
const Location = require('../src/structures/Location');
|
||||
const { MessageTypes } = require('../src/util/Constants');
|
||||
const { MessageTypes, WAState } = require('../src/util/Constants');
|
||||
|
||||
const remoteId = helper.remoteId;
|
||||
|
||||
@@ -145,6 +145,46 @@ describe('Client', function() {
|
||||
|
||||
await client.destroy();
|
||||
});
|
||||
|
||||
it('can take over if client was logged in somewhere else with takeoverOnConflict=true', async function() {
|
||||
this.timeout(40000);
|
||||
|
||||
const readyCallback1 = sinon.spy();
|
||||
const readyCallback2 = sinon.spy();
|
||||
const disconnectedCallback1 = sinon.spy();
|
||||
const disconnectedCallback2 = sinon.spy();
|
||||
|
||||
const client1 = helper.createClient({
|
||||
withSession: true,
|
||||
options: { takeoverOnConflict: true, takeoverTimeoutMs: 5000 }
|
||||
});
|
||||
const client2 = helper.createClient({withSession: true});
|
||||
|
||||
client1.on('ready', readyCallback1);
|
||||
client2.on('ready', readyCallback2);
|
||||
client1.on('disconnected', disconnectedCallback1);
|
||||
client2.on('disconnected', disconnectedCallback2);
|
||||
|
||||
await client1.initialize();
|
||||
expect(readyCallback1.called).to.equal(true);
|
||||
expect(readyCallback2.called).to.equal(false);
|
||||
expect(disconnectedCallback1.called).to.equal(false);
|
||||
expect(disconnectedCallback2.called).to.equal(false);
|
||||
|
||||
await client2.initialize();
|
||||
expect(readyCallback2.called).to.equal(true);
|
||||
expect(disconnectedCallback1.called).to.equal(false);
|
||||
expect(disconnectedCallback2.called).to.equal(false);
|
||||
|
||||
// wait for takeoverTimeoutMs to kick in
|
||||
await helper.sleep(5200);
|
||||
expect(disconnectedCallback1.called).to.equal(false);
|
||||
expect(disconnectedCallback2.called).to.equal(true);
|
||||
expect(disconnectedCallback2.calledWith(WAState.CONFLICT)).to.equal(true);
|
||||
|
||||
await client1.destroy();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('Authenticated', function() {
|
||||
@@ -160,6 +200,12 @@ describe('Client', function() {
|
||||
await client.destroy();
|
||||
});
|
||||
|
||||
it('can get current WhatsApp Web version', async function () {
|
||||
const version = await client.getWWebVersion();
|
||||
expect(typeof version).to.equal('string');
|
||||
console.log(`WA Version: ${version}`);
|
||||
});
|
||||
|
||||
describe('Expose Store', function() {
|
||||
it('exposes the store', async function() {
|
||||
const exposed = await client.pupPage.evaluate(() => {
|
||||
@@ -171,46 +217,46 @@ describe('Client', function() {
|
||||
|
||||
it('exposes all required WhatsApp Web internal models', async function() {
|
||||
const expectedModules = [
|
||||
'Chat',
|
||||
'Msg',
|
||||
'Contact',
|
||||
'Conn',
|
||||
'AppState',
|
||||
'CryptoLib',
|
||||
'Wap',
|
||||
'SendSeen',
|
||||
'SendClear',
|
||||
'SendDelete',
|
||||
'genId',
|
||||
'SendMessage',
|
||||
'MsgKey',
|
||||
'Invite',
|
||||
'OpaqueData',
|
||||
'MediaPrep',
|
||||
'MediaObject',
|
||||
'MediaUpload',
|
||||
'Cmd',
|
||||
'MediaTypes',
|
||||
'VCard',
|
||||
'UserConstructor',
|
||||
'Validators',
|
||||
'WidFactory',
|
||||
'BlockContact',
|
||||
'GroupMetadata',
|
||||
'Sticker',
|
||||
'UploadUtils',
|
||||
'Label',
|
||||
'Call',
|
||||
'Chat',
|
||||
'Cmd',
|
||||
'Conn',
|
||||
'Contact',
|
||||
'DownloadManager',
|
||||
'Features',
|
||||
'GroupMetadata',
|
||||
'Invite',
|
||||
'Label',
|
||||
'MediaObject',
|
||||
'MediaPrep',
|
||||
'MediaTypes',
|
||||
'MediaUpload',
|
||||
'Msg',
|
||||
'MsgKey',
|
||||
'OpaqueData',
|
||||
'QueryOrder',
|
||||
'QueryProduct',
|
||||
'DownloadManager'
|
||||
];
|
||||
'SendClear',
|
||||
'SendDelete',
|
||||
'SendMessage',
|
||||
'SendSeen',
|
||||
'Sticker',
|
||||
'UploadUtils',
|
||||
'UserConstructor',
|
||||
'VCard',
|
||||
'Validators',
|
||||
'Wap',
|
||||
'WidFactory',
|
||||
'genId'
|
||||
];
|
||||
|
||||
const loadedModules = await client.pupPage.evaluate(() => {
|
||||
return Object.keys(window.Store);
|
||||
});
|
||||
const loadedModules = await client.pupPage.evaluate((expectedModules) => {
|
||||
return expectedModules.filter(m => Boolean(window.Store[m]));
|
||||
}, expectedModules);
|
||||
|
||||
expect(loadedModules).to.include.members(expectedModules);
|
||||
expect(loadedModules).to.have.members(expectedModules);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -486,5 +532,53 @@ END:VCARD`;
|
||||
expect(formatted).to.eql('+1 (809) 220-1111');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search messages', function () {
|
||||
it('can search for messages', async function () {
|
||||
this.timeout(5000);
|
||||
|
||||
const m1 = await client.sendMessage(remoteId, 'I\'m searching for Super Mario Brothers');
|
||||
const m2 = await client.sendMessage(remoteId, 'This also contains Mario');
|
||||
const m3 = await client.sendMessage(remoteId, 'Nothing of interest here, just Luigi');
|
||||
|
||||
// wait for search index to catch up
|
||||
await helper.sleep(1000);
|
||||
|
||||
const msgs = await client.searchMessages('Mario', {chatId: remoteId});
|
||||
expect(msgs.length).to.be.greaterThanOrEqual(2);
|
||||
const msgIds = msgs.map(m => m.id._serialized);
|
||||
expect(msgIds).to.include.members([
|
||||
m1.id._serialized, m2.id._serialized
|
||||
]);
|
||||
expect(msgIds).to.not.include.members([m3.id._serialized]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status/About', function () {
|
||||
let me, previousStatus;
|
||||
|
||||
before(async function () {
|
||||
me = await client.getContactById(client.info.wid._serialized);
|
||||
previousStatus = await me.getAbout();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await client.setStatus(previousStatus);
|
||||
});
|
||||
|
||||
it('can set the status text', async function () {
|
||||
await client.setStatus('My shiny new status');
|
||||
|
||||
const status = await me.getAbout();
|
||||
expect(status).to.eql('My shiny new status');
|
||||
});
|
||||
|
||||
it('can set the status text to something else', async function () {
|
||||
await client.setStatus('Busy');
|
||||
|
||||
const status = await me.getAbout();
|
||||
expect(status).to.eql('Busy');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
188
tests/structures/chat.js
Normal file
188
tests/structures/chat.js
Normal file
@@ -0,0 +1,188 @@
|
||||
const { expect } = require('chai');
|
||||
|
||||
const helper = require('../helper');
|
||||
const Message = require('../../src/structures/Message');
|
||||
const { MessageTypes } = require('../../src/util/Constants');
|
||||
const { Contact } = require('../../src/structures');
|
||||
|
||||
const remoteId = helper.remoteId;
|
||||
|
||||
describe('Chat', function () {
|
||||
let client;
|
||||
let chat;
|
||||
|
||||
before(async function() {
|
||||
this.timeout(35000);
|
||||
client = helper.createClient({ withSession: true });
|
||||
await client.initialize();
|
||||
chat = await client.getChatById(remoteId);
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await client.destroy();
|
||||
});
|
||||
|
||||
it('can send a message to a chat', async function () {
|
||||
const msg = await chat.sendMessage('hello world');
|
||||
expect(msg).to.be.instanceOf(Message);
|
||||
expect(msg.type).to.equal(MessageTypes.TEXT);
|
||||
expect(msg.fromMe).to.equal(true);
|
||||
expect(msg.body).to.equal('hello world');
|
||||
expect(msg.to).to.equal(remoteId);
|
||||
});
|
||||
|
||||
it('can fetch messages sent in a chat', async function () {
|
||||
this.timeout(5000);
|
||||
await helper.sleep(1000);
|
||||
const msg = await chat.sendMessage('another message');
|
||||
|
||||
const messages = await chat.fetchMessages();
|
||||
expect(messages.length).to.be.greaterThanOrEqual(2);
|
||||
|
||||
const fetchedMsg = messages[messages.length-1];
|
||||
expect(fetchedMsg).to.be.instanceOf(Message);
|
||||
expect(fetchedMsg.type).to.equal(MessageTypes.TEXT);
|
||||
expect(fetchedMsg.id._serialized).to.equal(msg.id._serialized);
|
||||
expect(fetchedMsg.body).to.equal(msg.body);
|
||||
});
|
||||
|
||||
it('can use a limit when fetching messages sent in a chat', async function () {
|
||||
await helper.sleep(1000);
|
||||
const msg = await chat.sendMessage('yet another message');
|
||||
|
||||
const messages = await chat.fetchMessages({limit: 1});
|
||||
expect(messages).to.have.lengthOf(1);
|
||||
|
||||
const fetchedMsg = messages[0];
|
||||
expect(fetchedMsg).to.be.instanceOf(Message);
|
||||
expect(fetchedMsg.type).to.equal(MessageTypes.TEXT);
|
||||
expect(fetchedMsg.id._serialized).to.equal(msg.id._serialized);
|
||||
expect(fetchedMsg.body).to.equal(msg.body);
|
||||
});
|
||||
|
||||
it('can get the related contact', async function () {
|
||||
const contact = await chat.getContact();
|
||||
expect(contact).to.be.instanceOf(Contact);
|
||||
expect(contact.id._serialized).to.equal(chat.id._serialized);
|
||||
});
|
||||
|
||||
describe('Seen', function () {
|
||||
it('can mark a chat as unread', async function () {
|
||||
await chat.markUnread();
|
||||
await helper.sleep(500);
|
||||
|
||||
// refresh chat
|
||||
chat = await client.getChatById(remoteId);
|
||||
expect(chat.unreadCount).to.equal(-1);
|
||||
});
|
||||
|
||||
it('can mark a chat as seen', async function () {
|
||||
const res = await chat.sendSeen();
|
||||
expect(res).to.equal(true);
|
||||
|
||||
// refresh chat
|
||||
chat = await client.getChatById(remoteId);
|
||||
expect(chat.unreadCount).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Archiving', function (){
|
||||
it('can archive a chat', async function () {
|
||||
const res = await chat.archive();
|
||||
expect(res).to.equal(true);
|
||||
|
||||
await helper.sleep(1000);
|
||||
|
||||
// refresh chat
|
||||
chat = await client.getChatById(remoteId);
|
||||
expect(chat.archived).to.equal(true);
|
||||
});
|
||||
|
||||
it('can unarchive a chat', async function () {
|
||||
const res = await chat.unarchive();
|
||||
expect(res).to.equal(false);
|
||||
|
||||
await helper.sleep(1000);
|
||||
|
||||
// refresh chat
|
||||
chat = await client.getChatById(remoteId);
|
||||
expect(chat.archived).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pinning', function () {
|
||||
it('can pin a chat', async function () {
|
||||
const res = await chat.pin();
|
||||
expect(res).to.equal(true);
|
||||
|
||||
await helper.sleep(1000);
|
||||
|
||||
// refresh chat
|
||||
chat = await client.getChatById(remoteId);
|
||||
expect(chat.pinned).to.equal(true);
|
||||
});
|
||||
|
||||
it('can unpin a chat', async function () {
|
||||
const res = await chat.unpin();
|
||||
expect(res).to.equal(false);
|
||||
await helper.sleep(1000);
|
||||
|
||||
// refresh chat
|
||||
chat = await client.getChatById(remoteId);
|
||||
expect(chat.pinned).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Muting', function () {
|
||||
it('can mute a chat forever', async function() {
|
||||
await chat.mute();
|
||||
|
||||
// refresh chat
|
||||
chat = await client.getChatById(remoteId);
|
||||
expect(chat.isMuted).to.equal(true);
|
||||
expect(chat.muteExpiration).to.equal(-1);
|
||||
});
|
||||
|
||||
it('can mute a chat until a specific date', async function() {
|
||||
const unmuteDate = new Date(new Date().getTime() + (1000*60*60));
|
||||
await chat.mute(unmuteDate);
|
||||
|
||||
// refresh chat
|
||||
chat = await client.getChatById(remoteId);
|
||||
expect(chat.isMuted).to.equal(true);
|
||||
expect(chat.muteExpiration).to.equal(
|
||||
Math.round(unmuteDate.getTime() / 1000)
|
||||
);
|
||||
});
|
||||
|
||||
it('can unmute a chat', async function () {
|
||||
await chat.unmute();
|
||||
await helper.sleep(500);
|
||||
|
||||
// refresh chat
|
||||
chat = await client.getChatById(remoteId);
|
||||
expect(chat.isMuted).to.equal(false);
|
||||
expect(chat.muteExpiration).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line mocha/no-skipped-tests
|
||||
describe.skip('Destructive operations', function () {
|
||||
it('can clear all messages from chat', async function () {
|
||||
this.timeout(5000);
|
||||
|
||||
const res = await chat.clearMessages();
|
||||
expect(res).to.equal(true);
|
||||
|
||||
await helper.sleep(3000);
|
||||
|
||||
const msgs = await chat.fetchMessages();
|
||||
expect(msgs).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('can delete a chat', async function () {
|
||||
const res = await chat.delete();
|
||||
expect(res).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
2.2202.8
|
||||
2.2202.12
|
||||
Reference in New Issue
Block a user