feat: Multi-device support (#889)

* 🚑 Added ready selector for multi-device

* SendMessage fix

* File management system and some fixes

* cleanup

* cleanup again

* eslint

* critical fix for reloading the same session

* Checking for valid folder name (regex)

* ESLint hotfix (regex escapes)

* Typings cleanup

* cleanup listener

* Multi-device Branch merge (#888)

* Duplicate

* qr fix and allow non-beta users to connect

* urgent: selector fix

* urgent: qr timeout fix

* fix

* Updated type so no TS error when sending list/buttons

* Update index.d.ts

* fix QueryExist for Multidevice (#928)

* creates isRegisteredUserBeta

* fix QueryExist

* fix Error: GROUP_JID: invalid jid type: Not an instance of WID issue (#926)

* fix Error: GROUP_JID: invalid jid type: Not an instance of WID issue

* clean code

* Cleanup

* Fix for update chrome error

* ESLint fix

* :red_light: fix for RMDIR

* Update README.md

* Update README.md

* fix: getProfilePicUrl fix by victormga (#941)

* fix: MD presence available/unavailable (#942)

* delete session when appropriate & fix for SW

* ignore QR timeout errors

* Presence and ChatState updates working for MD+Non-MD

* shell uses new session storage

* lint fix

* support session.json-based auth for non-md

* md fix

* md fix

* fix shell clientId

* remove exclusive mocha test

* make linkPreview default to false

* remove ignored errors on getQuotedMessage

* fix: dont modify existing this.options.puppeteer object

* tests work with new dir auth

* remove exclusive test

* fixes and tests for group creation and participant functions

* remove unused function

* wip fix group settings functions

* isRegisteredUser && getNumberId hotFix (#955)

* isRegisteredUser && getNumberId hotFix

A fix for client.isRegisteredUser and client.getNumberId. Use for reference or if you are stuck with MD and NEEDS this function. Problably Whatsapp will break this in a couple weeks

* fix for non-md

Co-authored-by: Rajeh Taher <rajeh@reforward.dev>

* Fix WA 2.2146.9 MD +  victormga branch (#991)

* qrcode now uses observers instead of timeout

* automatic auth/qrcode detection

* Fix WA 2.2146.9 MD

Got from github:victormga/whatsapp-web.js#multidevice maybe it's behind pedro branch

Co-authored-by: victormga <victor_mga@hotmail.com>

* fix

* fix*

* getnumberid to multidevice (#1027)

* getNumberId to main

 isRegisteredUser && getNumberId hotFix #955 To main

* Update Client.js

Co-authored-by: tuyuribr <45042245+tuyuribr@users.noreply.github.com>

* Update Client.js

* Message.raw() (#1005)

* Message.raw()

* i just noticed

* Update index.d.ts

* Update index.d.ts

* Update Message.js

* Get rid of sharp now!!!!!!!! (#1045)

* commit 1

* finally, gotten rid of sharp

* pckg.json

* service worker fix & disableMessage option

* typings

* Update example.js

* clear session system

* Update Client.js

* Update Client.js

* Fix accepting group private invite (#1094)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* [MD] Add getCommonGroups with specific user. (#1097)

* Add getCommonGroups with specific user.

* Fix

* Fix

* Fix

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Fix getCommonGroups. (#1122)

* Fix of Unexpected identifier async destroy() (#1123)

* Fix of Unexpected identifier async destroy()

* Fix made in #1107

* Temporary fix for "Sticker" module

* some really quick changes

* Update Injected.js

* Update Injected.js

* Update index.d.ts

* fix: getNumberId Solved (#1142)

* getNumberId Solved

* isRegisteredUser Solved

* formmated

* Apply suggestions from code review

* Update src/util/Injected.js

Co-authored-by: Rajeh Taher <rajeh@reforward.dev>

* Fix: "Chrome user data dir was not found ..."

fixes the error caused by puppeteer.

* Update Client.js (#1154)

* fix: getNumberId and isRegisteredUser (#1159)

* fix: getNumberId and isRegisteredUser

* Apply suggestions from code review

Co-authored-by: Rajeh Taher <rajeh@reforward.dev>

* Update client.js

* Update Injected.js

* Update Client.js

* Update index.d.ts

* Update Client.js

* Update Client.js

* fix lint indentation

* fix auth_failure event for non-md, tests

* fix setting group subject

* fix finding Label module

* set remember-me after clearing localStorage

* fix: send messages to groups correctly on MD, use new ID format

* fix setting / getting contact status

* fix msg.getInfo, add message tests

* fix group settings functions

* fix set group description, handle errors in setSubject

* fix group invite functions

* fix leaving group

* bring back phone info for non-md users

* remove unused option, update typings

* add back jsdoc for qr event

* fix setting sticker metadata, clean up sticker functions

* rawData is a get only property

* fix and simplify getNumberId/isRegisteredUser

* fix getInviteInfo

* setDisplayName returns bool, not yet implemented for md

* fix: stream module (#1241)

* linkPreview has no effect on MD, return default to true

* fix: del linkPreview option on md

* cleanup, types and docs updates

* update readmes / test notes

* remove DS_Store

* DS_Store in gitignore

* test stability (timeouts/sleeps)

Co-authored-by: Rajeh Taher <rajeh@reforward.tk>
Co-authored-by: Gustavo B <52040719+Gugabit@users.noreply.github.com>
Co-authored-by: Maikel Ortega Hernández <maikeloh@gmail.com>
Co-authored-by: victormga <victor_mga@hotmail.com>
Co-authored-by: Pedro Lopez <pedroslopez@me.com>
Co-authored-by: tuyuribr <45042245+tuyuribr@users.noreply.github.com>
Co-authored-by: gon <68490103+nekiak@users.noreply.github.com>
Co-authored-by: Alon Schwartzblat <63599777+Schwartzblat@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Šebestíček <44745014+SebestikCZ@users.noreply.github.com>
Co-authored-by: Emmanuel Anaya Luna <38712443+KeruMx@users.noreply.github.com>
Co-authored-by: L337C0D3R <51872799+L337C0D3R@users.noreply.github.com>
Co-authored-by: Reni Delonzek <renidelonzek@gmail.com>
This commit is contained in:
Rajeh Taher
2022-02-27 14:51:08 -08:00
committed by GitHub
parent 8ef37b68ae
commit 0d55d40885
20 changed files with 1132 additions and 553 deletions

View File

@@ -1,2 +1,3 @@
WWEBJS_TEST_SESSION_PATH=test_session.json
WWEBJS_TEST_REMOTE_ID=XXXXXXXXXX@c.us
WWEBJS_TEST_REMOTE_ID=XXXXXXXXXX@c.us
WWEBJS_TEST_CLIENT_ID=authenticated
WWEBJS_TEST_MD=1

9
.gitignore vendored
View File

@@ -64,8 +64,13 @@ typings/
# next.js build output
.next
# macOS Thumbnails
# macOS
._*
.DS_Store
# Test sessions
*session.json
*session.json
# user data
WWebJS/
userDataDir/

View File

@@ -1,15 +1,9 @@
const fs = require('fs');
const { Client, Location, List, Buttons } = require('./index');
const SESSION_FILE_PATH = './session.json';
let sessionCfg;
if (fs.existsSync(SESSION_FILE_PATH)) {
sessionCfg = require(SESSION_FILE_PATH);
}
const client = new Client({ puppeteer: { headless: false }, session: sessionCfg });
// You can use an existing session and avoid scanning a QR code by adding a "session" object to the client options.
// This object must include WABrowserId, WASecretBundle, WAToken1 and WAToken2.
const client = new Client({
clientId: 'example',
puppeteer: { headless: false }
});
// You also could connect to an existing instance of a browser
// {
@@ -25,18 +19,12 @@ client.on('qr', (qr) => {
console.log('QR RECEIVED', qr);
});
client.on('authenticated', (session) => {
console.log('AUTHENTICATED', session);
sessionCfg=session;
fs.writeFile(SESSION_FILE_PATH, JSON.stringify(session), function (err) {
if (err) {
console.error(err);
}
});
client.on('authenticated', () => {
console.log('AUTHENTICATED');
});
client.on('auth_failure', msg => {
// Fired if session restore was unsuccessfull
// Fired if session restore was unsuccessful
console.error('AUTHENTICATION FAILURE', msg);
});
@@ -124,9 +112,8 @@ client.on('message', async msg => {
client.sendMessage(msg.from, `
*Connection info*
User name: ${info.pushname}
My number: ${info.me.user}
My number: ${info.wid.user}
Platform: ${info.platform}
WhatsApp version: ${info.phone.wa_version}
`);
} else if (msg.body === '!mediainfo' && msg.hasMedia) {
const attachmentData = await msg.downloadMedia();
@@ -267,12 +254,6 @@ client.on('group_update', (notification) => {
console.log('update', notification);
});
client.on('change_battery', (batteryInfo) => {
// Battery percentage for attached device has changed
const { battery, plugged } = batteryInfo;
console.log(`Battery: ${battery}% - Charging? ${plugged}`);
});
client.on('change_state', state => {
console.log('CHANGE STATE', state );
});

77
index.d.ts vendored
View File

@@ -84,6 +84,9 @@ declare namespace WAWebJS {
/** Returns the contact ID's profile picture URL, if privacy settings allow it */
getProfilePicUrl(contactId: string): Promise<string>
/** Gets the Contact's common groups with you. Returns empty array if you don't have any common group. */
getCommonGroups(contactId: string): Promise<ChatId[]>
/** Gets the current connection state for the client */
getState(): Promise<WAState>
@@ -118,6 +121,9 @@ declare namespace WAWebJS {
/** Marks the client as online */
sendPresenceAvailable(): Promise<void>
/** Marks the client as offline */
sendPresenceUnavailable(): Promise<void>
/** Mark as seen for the Chat */
sendSeen(chatId: string): Promise<boolean>
@@ -134,7 +140,7 @@ declare namespace WAWebJS {
* Sets the current user's display name
* @param displayName New display name
*/
setDisplayName(displayName: string): Promise<void>
setDisplayName(displayName: string): Promise<boolean>
/** Changes and returns the archive state of the Chat */
unarchiveChat(chatId: string): Promise<boolean>
@@ -150,11 +156,17 @@ declare namespace WAWebJS {
/** Emitted when authentication is successful */
on(event: 'authenticated', listener: (
/** Object containing session information. Can be used to restore the session */
session: ClientSession
/**
* Object containing session information. Can be used to restore the session
* @deprecated
*/
session?: ClientSession
) => void): this
/** Emitted when the battery percentage for the attached device changes */
/**
* Emitted when the battery percentage for the attached device changes
* @deprecated
*/
on(event: 'change_battery', listener: (batteryInfo: BatteryInfo) => void): this
/** Emitted when the connection state changes */
@@ -249,14 +261,12 @@ declare namespace WAWebJS {
/** Current connection information */
export interface ClientInfo {
/**
* Current user ID
* @deprecated Use .wid instead
*/
me: ContactId
/** Current user ID */
wid: ContactId
/** Information about the phone this client is connected to */
/**
* Information about the phone this client is connected to. Not available in multi-device.
* @deprecated
*/
phone: ClientInfoPhone
/** Platform the phone is running on */
platform: string
@@ -267,7 +277,10 @@ declare namespace WAWebJS {
getBatteryStatus: () => Promise<BatteryInfo>
}
/** Information about the phone this client is connected to */
/**
* Information about the phone this client is connected to
* @deprecated
*/
export interface ClientInfoPhone {
/** WhatsApp Version running on the phone */
wa_version: string
@@ -300,8 +313,19 @@ declare namespace WAWebJS {
/** Restart client with a new session (i.e. use null 'session' var) if authentication fails
* @default false */
restartOnAuthFail?: boolean
/** Whatsapp session to restore. If not set, will start a new session */
/**
* Enable authentication via a `session` option.
* @deprecated Will be removed in a future release
*/
useDeprecatedSessionAuth?: boolean
/**
* WhatsApp session to restore. If not set, will start a new session
* @deprecated Set `useDeprecatedSessionAuth: true` to enable. This auth method is not supported by MultiDevice and will be removed in a future release.
*/
session?: ClientSession
/** Client id to distinguish instances if you are using multiple, otherwise keep empty if you are using only one instance
* @default '' */
clientId: string
/** If another whatsapp web session is detected (another browser), take over the session in the current browser
* @default false */
takeoverOnConflict?: boolean,
@@ -314,9 +338,15 @@ declare namespace WAWebJS {
/** Ffmpeg path to use when formating videos to webp while sending stickers
* @default 'ffmpeg' */
ffmpegPath?: string
/** Path to place session objects in
@default './WWebJS' */
dataPath?: string
}
/** Represents a Whatsapp client session */
/**
* Represents a WhatsApp client session
* @deprecated
*/
export interface ClientSession {
WABrowserId: string,
WASecretBundle: string,
@@ -324,6 +354,9 @@ declare namespace WAWebJS {
WAToken2: string,
}
/**
* @deprecated
*/
export interface BatteryInfo {
/** The current battery percentage */
battery: number,
@@ -608,6 +641,13 @@ declare namespace WAWebJS {
selectedButtonId?: string,
/** Selected list row ID */
selectedRowId?: string,
/** Returns message in a raw format */
rawData: object,
/*
* 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.
*/
reload: () => Promise<Message>,
/** Accept the Group V4 Invite in message */
acceptGroupV4Invite: () => Promise<{status: number}>,
/** Deletes the message from the chat */
@@ -679,7 +719,7 @@ declare namespace WAWebJS {
/** Options for sending a message */
export interface MessageSendOptions {
/** Show links preview */
/** Show links preview. Has no effect on multi-device accounts. */
linkPreview?: boolean
/** Send audio as voice message */
sendAudioAsVoice?: boolean
@@ -741,7 +781,7 @@ declare namespace WAWebJS {
static fromUrl: (url: string, options?: MediaFromURLOptions) => Promise<MessageMedia>
}
export type MessageContent = string | MessageMedia | Location | Contact | Contact[] | List | Buttons
export type MessageContent = string | MessageMedia | Location | Contact | Contact[] | List | Buttons
/**
* Represents a Contact on WhatsApp
@@ -834,6 +874,9 @@ declare namespace WAWebJS {
/** Gets the Contact's current "about" info. Returns null if you don't have permission to read their status. */
getAbout: () => Promise<string | null>,
/** Gets the Contact's common groups with you. Returns empty array if you don't have any common group. */
getCommonGroups: () => Promise<ChatId[]>
}
@@ -1011,9 +1054,9 @@ declare namespace WAWebJS {
/** Demotes participants by IDs to regular users */
demoteParticipants: ChangeParticipantsPermisions;
/** Updates the group subject */
setSubject: (subject: string) => Promise<void>;
setSubject: (subject: string) => Promise<boolean>;
/** Updates the group description */
setDescription: (description: string) => Promise<void>;
setDescription: (description: string) => Promise<boolean>;
/** Updates the group settings to only allow admins to send messages
* @param {boolean} [adminsOnly=true] Enable or disable this option
* @returns {Promise<boolean>} Returns true if the setting was properly updated. This can return false if the user does not have the necessary permissions.

View File

@@ -5,7 +5,7 @@
"main": "./index.js",
"typings": "./index.d.ts",
"scripts": {
"test": "mocha tests --recursive",
"test": "mocha tests --recursive --timeout 5000",
"test-single": "mocha",
"shell": "node --experimental-repl-await ./shell.js",
"generate-docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose"
@@ -35,12 +35,12 @@
"mime": "^3.0.0",
"node-fetch": "^2.6.5",
"node-webpmux": "^3.1.0",
"puppeteer": "^13.0.0",
"sharp": "^0.28.3"
"puppeteer": "^13.0.0"
},
"devDependencies": {
"@types/node-fetch": "^2.5.12",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"dotenv": "^16.0.0",
"eslint": "^8.4.1",
"eslint-plugin-mocha": "^10.0.3",

View File

@@ -7,19 +7,12 @@
*/
const repl = require('repl');
const fs = require('fs');
const { Client } = require('./index');
const SESSION_FILE_PATH = './session.json';
let sessionCfg;
if (fs.existsSync(SESSION_FILE_PATH)) {
sessionCfg = require(SESSION_FILE_PATH);
}
const client = new Client({
puppeteer: { headless: false },
session: sessionCfg
clientId: 'shell'
});
console.log('Initializing...');
@@ -30,6 +23,10 @@ client.on('qr', () => {
console.log('Please scan the QR code on the browser.');
});
client.on('authenticated', (session) => {
console.log(JSON.stringify(session));
});
client.on('ready', () => {
const shell = repl.start('wwebjs> ');
shell.context.client = client;

View File

@@ -1,9 +1,10 @@
'use strict';
const path = require('path');
const fs = require('fs');
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');
@@ -11,27 +12,24 @@ const { WhatsWebURL, DefaultOptions, Events, WAState } = require('./util/Constan
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');
/**
* Starting point for interacting with the WhatsApp Web API
* @extends {EventEmitter}
* @param {object} options - Client options
* @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 {boolean} options.useDeprecatedSessionAuth - Enable JSON-based authentication. This is deprecated due to not being supported by MultiDevice, and will be removed in a future version.
* @param {object} options.session - This is deprecated due to not being supported by MultiDevice, and will be removed in a future version.
* @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.dataPath - Change the default path for saving session files, default is: "./WWebJS/"
* @param {string} options.userAgent - User agent to use in puppeteer
* @param {string} options.ffmpegPath - Ffmpeg path to use when formating videos to webp while sending stickers
* @param {boolean} options.bypassCSP - Sets bypassing of page's Content-Security-Policy.
* @param {string} options.clientId - Client id to distinguish instances if you are using multiple, otherwise keep null if you are using only one instance
*
* @fires Client#qr
* @fires Client#authenticated
@@ -48,7 +46,6 @@ 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 = {}) {
@@ -56,6 +53,19 @@ class Client extends EventEmitter {
this.options = Util.mergeDefault(DefaultOptions, options);
this.id = this.options.clientId;
// eslint-disable-next-line no-useless-escape
const foldernameRegex = /^(?!.{256,})(?!(aux|clock\$|con|nul|prn|com[1-9]|lpt[1-9])(?:$|\.))[^ ][ \.\w-$()+=[\];#@~,&amp;']+[^\. ]$/i;
if (this.id && !foldernameRegex.test(this.id)) throw Error('Invalid client ID. Make sure you abide by the folder naming rules of your operating system.');
if (!this.options.useDeprecatedSessionAuth) {
this.dataDir = this.options.puppeteer.userDataDir;
const dirPath = path.join(process.cwd(), this.options.dataPath, this.id ? 'session-' + this.id : 'session');
if (!this.dataDir) this.dataDir = dirPath;
fs.mkdirSync(this.dataDir, { recursive: true });
}
this.pupBrowser = null;
this.pupPage = null;
@@ -67,39 +77,39 @@ 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);
const puppeteerOpts = {
...this.options.puppeteer,
userDataDir: this.options.useDeprecatedSessionAuth ? undefined : this.dataDir
};
if (puppeteerOpts && puppeteerOpts.browserWSEndpoint) {
browser = await puppeteer.connect(puppeteerOpts);
page = await browser.newPage();
} else {
browser = await puppeteer.launch(this.options.puppeteer);
browser = await puppeteer.launch(puppeteerOpts);
page = (await browser.pages())[0];
}
await page.setUserAgent(this.options.userAgent);
this.pupBrowser = browser;
this.pupPage = page;
// remember me
await page.evaluateOnNewDocument(() => {
localStorage.setItem('remember-me', 'true');
});
if (this.options.useDeprecatedSessionAuth && 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);
}
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);
localStorage.setItem('remember-me', 'true');
}, this.options.session);
}
if(this.options.bypassCSP) {
if (this.options.bypassCSP) {
await page.setBypassCSP(true);
}
@@ -109,56 +119,55 @@ 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) {
if(this.options.session) {
/**
* Emitted when there has been an error while trying to restore an existing session
* @event Client#auth_failure
* @param {string} message
* @deprecated
*/
this.emit(Events.AUTHENTICATION_FAILURE, 'Unable to log in. Are the session details valid?');
await this.destroy();
if (this.options.restartOnAuthFail) {
// session restore failed so try again but without session to force new authentication
this.options.session = null;
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) {
@@ -166,15 +175,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' &&
@@ -187,32 +220,30 @@ 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);
}));
let authEventPayload = undefined;
if (this.options.useDeprecatedSessionAuth) {
// 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
};
authEventPayload = {
WABrowserId: localStorage.WABrowserId,
WASecretBundle: localStorage.WASecretBundle,
WAToken1: localStorage.WAToken1,
WAToken2: localStorage.WAToken2
};
}
/**
* 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');
@@ -221,8 +252,17 @@ class Client extends EventEmitter {
return window.Store.Features.features.MD_BACKEND;
});
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.');
await page.evaluate(async () => {
// safely unregister service workers
const registrations = await navigator.serviceWorker.getRegistrations();
for (let registration of registrations) {
registration.unregister();
}
});
if (this.options.useDeprecatedSessionAuth && isMD) {
throw new Error('Authenticating via JSON session is not supported for MultiDevice-enabled WhatsApp accounts.');
}
//Load util functions (serializers, helper functions)
@@ -234,7 +274,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
@@ -398,11 +438,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 });
});
@@ -421,14 +462,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); });
@@ -466,9 +507,6 @@ class Client extends EventEmitter {
* Closes the client
*/
async destroy() {
if (this._qrRefreshInterval) {
clearInterval(this._qrRefreshInterval);
}
await this.pupBrowser.close();
}
@@ -476,9 +514,13 @@ 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();
});
if (this.dataDir) {
return (fs.rmSync ? fs.rmSync : fs.rmdirSync).call(this.dataDir, { recursive: true });
}
}
/**
@@ -508,7 +550,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
@@ -558,34 +600,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);
}
@@ -672,7 +716,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);
}
@@ -691,25 +735,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);
}
@@ -717,11 +761,22 @@ 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);
const couldSet = await this.pupPage.evaluate(async displayName => {
if(!window.Store.Conn.canSetMyPushname()) return false;
if(window.Store.Features.features.MD_BACKEND) {
// TODO
return false;
} else {
const res = await window.Store.Wap.setPushname(displayName);
return !res.status || res.status === 200;
}
}, displayName);
return couldSet;
}
/**
@@ -740,7 +795,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();
});
}
@@ -847,12 +911,37 @@ class Client extends EventEmitter {
*/
async getProfilePicUrl(contactId) {
const profilePic = await this.pupPage.evaluate((contactId) => {
return window.Store.Wap.profilePicFind(contactId);
const chatWid = window.Store.WidFactory.createWid(contactId);
return window.Store.getProfilePicFull(chatWid);
}, 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
*/
@@ -868,10 +957,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));
}
/**
@@ -881,14 +967,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);
}
/**
@@ -897,14 +984,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
@@ -917,7 +1004,7 @@ class Client extends EventEmitter {
return window.Store.NumberInfo.findCC(numberId);
}, number);
}
/**
* Create a new group
* @param {string} name group title
@@ -936,12 +1023,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.genId();
const res = await window.Store.GroupUtils.sendCreateGroup(name, participantWIDs, undefined, id);
return res;
}, name, participants);
@@ -962,9 +1046,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));
}
/**
@@ -975,7 +1059,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);
}
@@ -985,12 +1069,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));
}
/**
@@ -998,16 +1082,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;
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)));
@@ -1020,7 +1104,7 @@ 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)));
return Promise.all(chatIds.map(id => window.WWebJS.getContact(id)));
});
return blockedContacts.map(contact => ContactFactory.create(this.client, contact));

View File

@@ -20,12 +20,6 @@ class ClientInfo extends Base {
*/
this.pushname = data.pushname;
/**
* @type {object}
* @deprecated Use .wid instead
*/
this.me = data.wid;
/**
* Current user ID
* @type {object}
@@ -33,18 +27,19 @@ class ClientInfo extends Base {
this.wid = data.wid;
/**
* Information about the phone this client is connected to
* Information about the phone this client is connected to. Not available in multi-device.
* @type {object}
* @property {string} wa_version WhatsApp Version running on the phone
* @property {string} os_version OS Version running on the phone (iOS or Android version)
* @property {string} device_manufacturer Device manufacturer
* @property {string} device_model Device model
* @property {string} os_build_number OS build number
* @deprecated
*/
this.phone = data.phone;
/**
* Platform the phone is running on
* Platform WhatsApp is running on
* @type {string}
*/
this.platform = data.platform;
@@ -57,6 +52,7 @@ class ClientInfo extends Base {
* @returns {object} batteryStatus
* @returns {number} batteryStatus.battery - The current battery percentage
* @returns {boolean} batteryStatus.plugged - Indicates if the phone is plugged in (true) or not (false)
* @deprecated
*/
async getBatteryStatus() {
return await this.client.pupPage.evaluate(() => {
@@ -64,7 +60,6 @@ class ClientInfo extends Base {
return { battery, plugged };
});
}
}
module.exports = ClientInfo;

View File

@@ -183,7 +183,8 @@ class Contact extends Base {
*/
async getAbout() {
const about = await this.client.pupPage.evaluate(async (contactId) => {
return window.Store.Wap.statusFind(contactId);
const wid = window.Store.WidFactory.createWid(contactId);
return window.Store.StatusUtils.getStatus(wid);
}, this.id._serialized);
if (typeof about.status !== 'string')
@@ -191,6 +192,14 @@ class Contact extends Base {
return about.status;
}
/**
* Gets the Contact's common groups with you. Returns empty array if you don't have any common group.
* @returns {Promise<WAWebJS.ChatId[]>}
*/
async getCommonGroups() {
return await this.client.getCommonGroups(this.id._serialized);
}
}

View File

@@ -60,7 +60,9 @@ class GroupChat extends Chat {
*/
async addParticipants(participantIds) {
return await this.client.pupPage.evaluate((chatId, participantIds) => {
return window.Store.Wap.addParticipants(chatId, participantIds);
const chatWid = window.Store.WidFactory.createWid(chatId);
const participantWids = participantIds.map(p => window.Store.WidFactory.createWid(p));
return window.Store.GroupParticipants.sendAddParticipants(chatWid, participantWids);
}, this.id._serialized, participantIds);
}
@@ -71,7 +73,9 @@ class GroupChat extends Chat {
*/
async removeParticipants(participantIds) {
return await this.client.pupPage.evaluate((chatId, participantIds) => {
return window.Store.Wap.removeParticipants(chatId, participantIds);
const chatWid = window.Store.WidFactory.createWid(chatId);
const participantWids = participantIds.map(p => window.Store.WidFactory.createWid(p));
return window.Store.GroupParticipants.sendRemoveParticipants(chatWid, participantWids);
}, this.id._serialized, participantIds);
}
@@ -82,7 +86,9 @@ class GroupChat extends Chat {
*/
async promoteParticipants(participantIds) {
return await this.client.pupPage.evaluate((chatId, participantIds) => {
return window.Store.Wap.promoteParticipants(chatId, participantIds);
const chatWid = window.Store.WidFactory.createWid(chatId);
const participantWids = participantIds.map(p => window.Store.WidFactory.createWid(p));
return window.Store.GroupParticipants.sendPromoteParticipants(chatWid, participantWids);
}, this.id._serialized, participantIds);
}
@@ -93,39 +99,53 @@ class GroupChat extends Chat {
*/
async demoteParticipants(participantIds) {
return await this.client.pupPage.evaluate((chatId, participantIds) => {
return window.Store.Wap.demoteParticipants(chatId, participantIds);
const chatWid = window.Store.WidFactory.createWid(chatId);
const participantWids = participantIds.map(p => window.Store.WidFactory.createWid(p));
return window.Store.GroupParticipants.sendDemoteParticipants(chatWid, participantWids);
}, this.id._serialized, participantIds);
}
/**
* Updates the group subject
* @param {string} subject
* @returns {Promise}
* @returns {Promise<boolean>} Returns true if the subject was properly updated. This can return false if the user does not have the necessary permissions.
*/
async setSubject(subject) {
let res = await this.client.pupPage.evaluate((chatId, subject) => {
return window.Store.Wap.changeSubject(chatId, subject);
const success = await this.client.pupPage.evaluate(async (chatId, subject) => {
const chatWid = window.Store.WidFactory.createWid(chatId);
try {
return await window.Store.GroupUtils.sendSetGroupSubject(chatWid, subject);
} catch (err) {
if(err.name === 'ServerStatusCodeError') return false;
throw err;
}
}, this.id._serialized, subject);
if(res.status == 200) {
this.name = subject;
}
if(!success) return false;
this.name = subject;
return true;
}
/**
* Updates the group description
* @param {string} description
* @returns {Promise}
* @returns {Promise<boolean>} Returns true if the description was properly updated. This can return false if the user does not have the necessary permissions.
*/
async setDescription(description) {
let res = await this.client.pupPage.evaluate((chatId, description) => {
let descId = window.Store.GroupMetadata.get(chatId).descId;
return window.Store.Wap.setGroupDescription(chatId, description, window.Store.genId(), descId);
const success = await this.client.pupPage.evaluate(async (chatId, description) => {
const chatWid = window.Store.WidFactory.createWid(chatId);
let descId = window.Store.GroupMetadata.get(chatWid).descId;
try {
return await window.Store.GroupUtils.sendSetGroupDescription(chatWid, description, window.Store.genId(), descId);
} catch (err) {
if(err.name === 'ServerStatusCodeError') return false;
throw err;
}
}, this.id._serialized, description);
if (res.status == 200) {
this.groupMetadata.desc = description;
}
if(!success) return false;
this.groupMetadata.desc = description;
return true;
}
/**
@@ -134,12 +154,18 @@ class GroupChat extends Chat {
* @returns {Promise<boolean>} Returns true if the setting was properly updated. This can return false if the user does not have the necessary permissions.
*/
async setMessagesAdminsOnly(adminsOnly=true) {
let res = await this.client.pupPage.evaluate((chatId, value) => {
return window.Store.Wap.setGroupProperty(chatId, 'announcement', value);
const success = await this.client.pupPage.evaluate(async (chatId, adminsOnly) => {
const chatWid = window.Store.WidFactory.createWid(chatId);
try {
return await window.Store.GroupUtils.sendSetGroupProperty(chatWid, 'announcement', adminsOnly ? 1 : 0);
} catch (err) {
if(err.name === 'ServerStatusCodeError') return false;
throw err;
}
}, this.id._serialized, adminsOnly);
if (res.status !== 200) return false;
if(!success) return false;
this.groupMetadata.announce = adminsOnly;
return true;
}
@@ -150,11 +176,17 @@ class GroupChat extends Chat {
* @returns {Promise<boolean>} Returns true if the setting was properly updated. This can return false if the user does not have the necessary permissions.
*/
async setInfoAdminsOnly(adminsOnly=true) {
let res = await this.client.pupPage.evaluate((chatId, value) => {
return window.Store.Wap.setGroupProperty(chatId, 'restrict', value);
const success = await this.client.pupPage.evaluate(async (chatId, adminsOnly) => {
const chatWid = window.Store.WidFactory.createWid(chatId);
try {
return await window.Store.GroupUtils.sendSetGroupProperty(chatWid, 'restrict', adminsOnly ? 1 : 0);
} catch (err) {
if(err.name === 'ServerStatusCodeError') return false;
throw err;
}
}, this.id._serialized, adminsOnly);
if (res.status !== 200) return false;
if(!success) return false;
this.groupMetadata.restrict = adminsOnly;
return true;
@@ -165,25 +197,25 @@ class GroupChat extends Chat {
* @returns {Promise<string>} Group's invite code
*/
async getInviteCode() {
let res = await this.client.pupPage.evaluate(chatId => {
return window.Store.Wap.groupInviteCode(chatId);
const code = await this.client.pupPage.evaluate(async chatId => {
const chatWid = window.Store.WidFactory.createWid(chatId);
return window.Store.Invite.sendQueryGroupInviteCode(chatWid);
}, this.id._serialized);
if (res.status == 200) {
return res.code;
}
throw new Error('Not authorized');
return code;
}
/**
* Invalidates the current group invite code and generates a new one
* @returns {Promise}
* @returns {Promise<string>} New invite code
*/
async revokeInvite() {
return await this.client.pupPage.evaluate(chatId => {
return window.Store.Wap.revokeGroupInvite(chatId);
const code = await this.client.pupPage.evaluate(chatId => {
const chatWid = window.Store.WidFactory.createWid(chatId);
return window.Store.Invite.sendRevokeGroupInviteCode(chatWid);
}, this.id._serialized);
return code;
}
/**
@@ -191,8 +223,9 @@ class GroupChat extends Chat {
* @returns {Promise}
*/
async leave() {
return await this.client.pupPage.evaluate(chatId => {
return window.Store.Wap.leaveGroup(chatId);
await this.client.pupPage.evaluate(chatId => {
const chatWid = window.Store.WidFactory.createWid(chatId);
return window.Store.GroupUtils.sendExitGroup(chatWid);
}, this.id._serialized);
}

View File

@@ -19,13 +19,14 @@ class Message extends Base {
}
_patch(data) {
this._data = data;
/**
* MediaKey that represents the sticker 'ID'
* @type {string}
*/
this.mediaKey = data.mediaKey;
/**
* ID that represents the message
* @type {object}
@@ -50,7 +51,7 @@ class Message extends Base {
*/
this.body = this.hasMedia ? data.caption || '' : data.body || '';
/**
/**
* Message type
* @type {MessageTypes}
*/
@@ -70,9 +71,9 @@ class Message extends Base {
/**
* ID for who this message is for.
*
*
* If the message is sent by the current user, it will be the Chat to which the message is being sent.
* If the message is sent by another user, it will be the ID for the current user.
* If the message is sent by another user, it will be the ID for the current user.
* @type {string}
*/
this.to = (typeof (data.to) === 'object' && data.to !== null) ? data.to._serialized : data.to;
@@ -87,8 +88,8 @@ class Message extends Base {
* String that represents from which device type the message was sent
* @type {string}
*/
this.deviceType = data.id.id.length > 21 ? 'android' : data.id.id.substring(0,2) =='3A' ? 'ios' : 'web';
this.deviceType = data.id.id.length > 21 ? 'android' : data.id.id.substring(0, 2) == '3A' ? 'ios' : 'web';
/**
* Indicates if the message was forwarded
* @type {boolean}
@@ -114,14 +115,14 @@ class Message extends Base {
* @type {boolean}
*/
this.isStarred = data.star;
/**
* Indicates if the message was a broadcast
* @type {boolean}
*/
this.broadcast = data.broadcast;
/**
/**
* Indicates if the message was sent by the current user
* @type {boolean}
*/
@@ -157,7 +158,7 @@ class Message extends Base {
fromId: data.from._serialized,
toId: data.to._serialized
} : undefined;
/**
* Indicates the mentions in the message body.
* @type {Array<string>}
@@ -214,7 +215,7 @@ class Message extends Base {
/**
* Links included in the message.
* @type {Array<{link: string, isSuspicious: boolean}>}
*
*
*/
this.links = data.links;
@@ -222,7 +223,7 @@ class Message extends Base {
if (data.dynamicReplyButtons) {
this.dynamicReplyButtons = data.dynamicReplyButtons;
}
/** Selected Button Id **/
if (data.selectedButtonId) {
this.selectedButtonId = data.selectedButtonId;
@@ -232,7 +233,7 @@ class Message extends Base {
if (data.listResponse && data.listResponse.singleSelectReply.selectedRowId) {
this.selectedRowId = data.listResponse.singleSelectReply.selectedRowId;
}
return super._patch(data);
}
@@ -240,6 +241,32 @@ class Message extends Base {
return this.fromMe ? this.to : this.from;
}
/**
* 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.
* @returns {Promise<Message>}
*/
async reload() {
const newData = await this.client.pupPage.evaluate((msgId) => {
const msg = window.Store.Msg.get(msgId);
if(!msg) return null;
return window.WWebJS.getMessageModel(msg);
}, this.id._serialized);
if(!newData) return null;
this._patch(newData);
return this;
}
/**
* Returns message in a raw format
* @type {Object}
*/
get rawData() {
return this._data;
}
/**
* Returns the Chat this message was sent in
* @returns {Promise<Chat>}
@@ -280,12 +307,12 @@ class Message extends Base {
}
/**
* 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
* 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
* in the same Chat as the original message was sent.
*
* @param {string|MessageMedia|Location} content
* @param {string} [chatId]
*
* @param {string|MessageMedia|Location} content
* @param {string} [chatId]
* @param {MessageSendOptions} [options]
* @returns {Promise<Message>}
*/
@@ -309,10 +336,10 @@ class Message extends Base {
async acceptGroupV4Invite() {
return await this.client.acceptGroupV4Invite(this.inviteV4);
}
/**
* Forwards this message to another chat
*
*
* @param {string|Chat} chat Chat model or chat ID to which the message will be forwarded
* @returns {Promise}
*/
@@ -342,7 +369,7 @@ class Message extends Base {
if (msg.mediaData.mediaStage != 'RESOLVED') {
// try to resolve media
await msg.downloadMedia({
downloadEvenIfExpensive: true,
downloadEvenIfExpensive: true,
rmrReason: 1
});
}
@@ -362,9 +389,9 @@ class Message extends Base {
type: msg.type,
signal: (new AbortController).signal
});
const data = window.WWebJS.arrayBufferToBase64(decryptedMedia);
return {
data,
mimetype: msg.mimetype,
@@ -440,14 +467,10 @@ class Message extends Base {
async getInfo() {
const info = await this.client.pupPage.evaluate(async (msgId) => {
const msg = window.Store.Msg.get(msgId);
if(!msg) return null;
return await window.Store.Wap.queryMsgInfo(msg.id);
}, this.id._serialized);
if (!msg) return null;
if(info.status) {
return null;
}
return await window.Store.MessageInfo.sendQueryMsgInfo(msg.id);
}, this.id._serialized);
return info;
}

View File

@@ -7,10 +7,9 @@ exports.DefaultOptions = {
headless: true,
defaultViewport: null
},
session: false,
qrTimeoutMs: 45000,
qrRefreshIntervalMs: 20000,
authTimeoutMs: 45000,
dataPath: './WWebJS/',
useDeprecatedSessionAuth: false,
authTimeoutMs: 0,
qrMaxRetries: 0,
takeoverOnConflict: false,
takeoverTimeoutMs: 0,

View File

@@ -8,36 +8,59 @@ exports.ExposeStore = (moduleRaidStr) => {
window.Store = Object.assign({}, window.mR.findModule(m => m.default && m.default.Chat)[0].default);
window.Store.AppState = window.mR.findModule('Socket')[0].Socket;
window.Store.Conn = window.mR.findModule('Conn')[0].Conn;
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];
window.Store.SendDelete = window.mR.findModule('sendDelete')[0];
window.Store.genId = window.mR.findModule('randomId')[0].randomId;
window.Store.SendMessage = window.mR.findModule('addAndSendMsgToChat')[0];
window.Store.MsgKey = window.mR.findModule((module) => module.default && module.default.fromString)[0].default;
window.Store.BlockContact = window.mR.findModule('blockContact')[0];
window.Store.Call = window.mR.findModule('CallCollection')[0].CallCollection;
window.Store.Cmd = window.mR.findModule('Cmd')[0].Cmd;
window.Store.CryptoLib = window.mR.findModule('decryptE2EMedia')[0];
window.Store.DownloadManager = window.mR.findModule('downloadManager')[0].downloadManager;
window.Store.Features = window.mR.findModule('FEATURE_CHANGE_EVENT')[0].GK;
window.Store.genId = window.mR.findModule('newTag')[0].newTag;
window.Store.GroupMetadata = window.mR.findModule((module) => module.default && module.default.handlePendingInvite)[0].default;
window.Store.Invite = window.mR.findModule('sendJoinGroupViaInvite')[0];
window.Store.OpaqueData = window.mR.findModule(module => module.default && module.default.createFromData)[0].default;
window.Store.InviteInfo = window.mR.findModule('sendQueryGroupInvite')[0];
window.Store.Label = window.mR.findModule('LabelCollection')[0].LabelCollection;
window.Store.MediaPrep = window.mR.findModule('MediaPrep')[0];
window.Store.MediaObject = window.mR.findModule('getOrCreateMediaObject')[0];
window.Store.MediaUpload = window.mR.findModule('uploadMedia')[0];
window.Store.NumberInfo = window.mR.findModule('formattedPhoneNumber')[0];
window.Store.Cmd = window.mR.findModule('Cmd')[0].Cmd;
window.Store.MediaTypes = window.mR.findModule('msgToMediaType')[0];
window.Store.VCard = window.mR.findModule('vcardFromContactModel')[0];
window.Store.MediaUpload = window.mR.findModule('uploadMedia')[0];
window.Store.MsgKey = window.mR.findModule((module) => module.default && module.default.fromString)[0].default;
window.Store.MessageInfo = window.mR.findModule('sendQueryMsgInfo')[0];
window.Store.OpaqueData = window.mR.findModule(module => module.default && module.default.createFromData)[0].default;
window.Store.QueryExist = window.mR.findModule(module => typeof module.default === 'function' && module.default.toString().includes('Should not reach queryExists MD'))[0].default;
window.Store.QueryProduct = window.mR.findModule('queryProduct')[0];
window.Store.QueryOrder = window.mR.findModule('queryOrder')[0];
window.Store.SendClear = window.mR.findModule('sendClear')[0];
window.Store.SendDelete = window.mR.findModule('sendDelete')[0];
window.Store.SendMessage = window.mR.findModule('addAndSendMsgToChat')[0];
window.Store.SendSeen = window.mR.findModule('sendSeen')[0];
window.Store.Sticker = window.mR.findModule('Sticker')[0].Sticker;
window.Store.User = window.mR.findModule('getMaybeMeUser')[0];
window.Store.UploadUtils = window.mR.findModule((module) => (module.default && module.default.encryptAndUpload) ? module.default : null)[0].default;
window.Store.UserConstructor = window.mR.findModule((module) => (module.default && module.default.prototype && module.default.prototype.isServer && module.default.prototype.isUser) ? module.default : null)[0].default;
window.Store.Validators = window.mR.findModule('findLinks')[0];
window.Store.VCard = window.mR.findModule('vcardFromContactModel')[0];
window.Store.Wap = window.mR.findModule('queryLinkPreview')[0].default;
window.Store.WidFactory = window.mR.findModule('createWid')[0];
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].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];
window.Store.DownloadManager = window.mR.findModule('downloadManager')[0].downloadManager;
window.Store.Call = window.mR.findModule('CallCollection')[0].CallCollection;
window.Store.getProfilePicFull = window.mR.findModule('getProfilePicFull')[0].getProfilePicFull;
window.Store.PresenceUtils = window.mR.findModule('sendPresenceAvailable')[0];
window.Store.ChatState = window.mR.findModule('sendChatStateComposing')[0];
window.Store.GroupParticipants = window.mR.findModule('sendPromoteParticipants')[0];
window.Store.JoinInviteV4 = window.mR.findModule('sendJoinGroupViaInviteV4')[0];
window.Store.findCommonGroups = window.mR.findModule('findCommonGroups')[0].findCommonGroups;
window.Store.StatusUtils = window.mR.findModule('setMyStatus')[0];
window.Store.StickerTools = {
...window.mR.findModule('toWebpSticker')[0],
...window.mR.findModule('addWebpMetadata')[0]
};
window.Store.GroupUtils = {
...window.mR.findModule('sendCreateGroup')[0],
...window.mR.findModule('sendSetGroupSubject')[0],
...window.mR.findModule('markExited')[0]
};
if(!window.Store.Chat._find) {
if (!window.Store.Chat._find) {
window.Store.Chat._find = e => {
const target = window.Store.Chat.get(e);
return target ? Promise.resolve(target) : Promise.resolve({
@@ -50,14 +73,6 @@ exports.ExposeStore = (moduleRaidStr) => {
exports.LoadUtils = () => {
window.WWebJS = {};
window.WWebJS.getNumberId = async (id) => {
let result = await window.Store.Wap.queryExist(id);
if (result.jid === undefined)
throw 'The number provided is not a registered whatsapp user';
return result.jid;
};
window.WWebJS.sendSeen = async (chatId) => {
let chat = window.Store.Chat.get(chatId);
if (chat !== undefined) {
@@ -67,14 +82,14 @@ exports.LoadUtils = () => {
return false;
};
window.WWebJS.sendMessage = async (chat, content, options = {}) => {
let attOptions = {};
if (options.attachment) {
attOptions = options.sendMediaAsSticker
attOptions = options.sendMediaAsSticker
? await window.WWebJS.processStickerData(options.attachment)
: await window.WWebJS.processMediaData(options.attachment, {
forceVoice: options.sendAudioAsVoice,
forceVoice: options.sendAudioAsVoice,
forceDocument: options.sendMediaAsDocument,
forceGif: options.sendVideoAsGif
});
@@ -144,22 +159,26 @@ exports.LoadUtils = () => {
if (options.linkPreview) {
delete options.linkPreview;
const link = window.Store.Validators.findLink(content);
if (link) {
const preview = await window.Store.Wap.queryLinkPreview(link.url);
preview.preview = true;
preview.subtype = 'url';
options = { ...options, ...preview };
// Not supported yet by WhatsApp Web on MD
if(!window.Store.Features.features.MD_BACKEND) {
const link = window.Store.Validators.findLink(content);
if (link) {
const preview = await window.Store.Wap.queryLinkPreview(link.url);
preview.preview = true;
preview.subtype = 'url';
options = { ...options, ...preview };
}
}
}
let buttonOptions = {};
if(options.buttons){
let caption;
if(options.buttons.type === 'chat') {
if (options.buttons.type === 'chat') {
content = options.buttons.body;
caption = content;
}else{
} else {
caption = options.caption ? options.caption : ' '; //Caption can't be empty
}
buttonOptions = {
@@ -192,11 +211,16 @@ exports.LoadUtils = () => {
delete options.list;
delete listOptions.list.footer;
}
const meUser = window.Store.User.getMaybeMeUser();
const isMD = window.Store.Features.features.MD_BACKEND;
const newMsgId = new window.Store.MsgKey({
fromMe: true,
remote: chat.id,
from: meUser,
to: chat.id,
id: window.Store.genId(),
participant: isMD && chat.id.isGroup() ? meUser : undefined,
selfDir: 'out',
});
const extraOptions = options.extraOptions || {};
@@ -213,7 +237,7 @@ exports.LoadUtils = () => {
id: newMsgId,
ack: 0,
body: content,
from: window.Store.Conn.wid,
from: meUser,
to: chat.id,
local: true,
self: 'out',
@@ -234,11 +258,25 @@ exports.LoadUtils = () => {
return window.Store.Msg.get(newMsgId._serialized);
};
window.WWebJS.toStickerData = async (mediaInfo) => {
if (mediaInfo.mimetype == 'image/webp') return mediaInfo;
const file = window.WWebJS.mediaInfoToFile(mediaInfo);
const webpSticker = await window.Store.StickerTools.toWebpSticker(file);
const webpBuffer = await webpSticker.arrayBuffer();
const data = window.WWebJS.arrayBufferToBase64(webpBuffer);
return {
mimetype: 'image/webp',
data
};
};
window.WWebJS.processStickerData = async (mediaInfo) => {
if (mediaInfo.mimetype !== 'image/webp') throw new Error('Invalid media type');
const file = window.WWebJS.mediaInfoToFile(mediaInfo);
let filehash = await window.WWebJS.getFileHash(file);
let filehash = await window.WWebJS.getFileHash(file);
let mediaKey = await window.WWebJS.generateHash(32);
const controller = new AbortController();
@@ -324,11 +362,11 @@ exports.LoadUtils = () => {
window.WWebJS.getMessageModel = message => {
const msg = message.serialize();
msg.isEphemeral = message.isEphemeral;
msg.isStatusV3 = message.isStatusV3;
msg.links = (message.getLinks()).map(link => ({
link: link.href,
msg.links = (message.getLinks()).map(link => ({
link: link.href,
isSuspicious: Boolean(link.suspiciousCharacters && link.suspiciousCharacters.size)
}));
@@ -338,28 +376,30 @@ exports.LoadUtils = () => {
if (msg.dynamicReplyButtons) {
msg.dynamicReplyButtons = JSON.parse(JSON.stringify(msg.dynamicReplyButtons));
}
if(msg.replyButtons) {
if (msg.replyButtons) {
msg.replyButtons = JSON.parse(JSON.stringify(msg.replyButtons));
}
if(typeof msg.id.remote === 'object') {
msg.id = Object.assign({}, msg.id, {remote: msg.id.remote._serialized});
if (typeof msg.id.remote === 'object') {
msg.id = Object.assign({}, msg.id, { remote: msg.id.remote._serialized });
}
delete msg.pendingAckUpdate;
return msg;
};
window.WWebJS.getChatModel = async chat => {
let res = chat.serialize();
res.isGroup = chat.isGroup;
res.formattedTitle = chat.formattedTitle;
res.isMuted = chat.mute && chat.mute.isMuted;
if (chat.groupMetadata) {
await window.Store.GroupMetadata.update(chat.id._serialized);
const chatWid = window.Store.WidFactory.createWid((chat.id._serialized));
await window.Store.GroupMetadata.update(chatWid);
res.groupMetadata = chat.groupMetadata.serialize();
}
@@ -414,7 +454,7 @@ exports.LoadUtils = () => {
};
window.WWebJS.mediaInfoToFile = ({ data, mimetype, filename }) => {
const binaryData = atob(data);
const binaryData = window.atob(data);
const buffer = new ArrayBuffer(binaryData.length);
const view = new Uint8Array(buffer);
@@ -431,15 +471,15 @@ exports.LoadUtils = () => {
window.WWebJS.arrayBufferToBase64 = (arrayBuffer) => {
let binary = '';
const bytes = new Uint8Array( arrayBuffer );
const bytes = new Uint8Array(arrayBuffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
binary += String.fromCharCode(bytes[i]);
}
return window.btoa( binary );
return window.btoa(binary);
};
window.WWebJS.getFileHash = async (data) => {
window.WWebJS.getFileHash = async (data) => {
let buffer = await data.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
return btoa(String.fromCharCode(...new Uint8Array(hashBuffer)));
@@ -449,7 +489,7 @@ exports.LoadUtils = () => {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
@@ -474,15 +514,18 @@ exports.LoadUtils = () => {
};
window.WWebJS.sendChatstate = async (state, chatId) => {
if (window.Store.Features.features.MD_BACKEND) {
chatId = window.Store.WidFactory.createWid(chatId);
}
switch (state) {
case 'typing':
await window.Store.Wap.sendChatstateComposing(chatId);
await window.Store.ChatState.sendChatStateComposing(chatId);
break;
case 'recording':
await window.Store.Wap.sendChatstateRecording(chatId);
await window.Store.ChatState.sendChatStateRecording(chatId);
break;
case 'stop':
await window.Store.Wap.sendChatstatePaused(chatId);
await window.Store.ChatState.sendChatStatePaused(chatId);
break;
default:
throw 'Invalid chatstate';
@@ -494,7 +537,7 @@ exports.LoadUtils = () => {
window.WWebJS.getLabelModel = label => {
let res = label.serialize();
res.hexColor = label.hexColor;
return res;
};
@@ -527,20 +570,3 @@ exports.LoadUtils = () => {
return undefined;
};
};
exports.MarkAllRead = () => {
let Chats = window.Store.Chat.models;
for (let chatIndex in Chats) {
if (isNaN(chatIndex)) {
continue;
}
let chat = Chats[chatIndex];
if (chat.unreadCount > 0) {
chat.markSeen();
window.Store.Wap.sendConversationSeen(chat.id, chat.getLastMsgKeyForAction(), chat.unreadCount - chat.pendingSeenCount);
}
}
};

View File

@@ -1,6 +1,5 @@
'use strict';
const sharp = require('sharp');
const path = require('path');
const Crypto = require('crypto');
const { tmpdir } = require('os');
@@ -14,7 +13,6 @@ const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
* Utility methods
*/
class Util {
constructor() {
throw new Error(`The ${this.constructor.name} class may not be instantiated.`);
}
@@ -23,7 +21,7 @@ class Util {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
@@ -55,33 +53,19 @@ class Util {
*
* @returns {Promise<MessageMedia>} media in webp format
*/
static async formatImageToWebpSticker(media) {
static async formatImageToWebpSticker(media, pupPage) {
if (!media.mimetype.includes('image'))
throw new Error('media is not a image');
if (media.mimetype.includes('webp')) {
return media;
}
const buff = Buffer.from(media.data, 'base64');
let sharpImg = sharp(buff);
sharpImg = sharpImg.webp();
sharpImg = sharpImg.resize(512, 512, {
fit: 'contain',
background: { r: 0, g: 0, b: 0, alpha: 0 },
});
let webpBase64 = (await sharpImg.toBuffer()).toString('base64');
return {
mimetype: 'image/webp',
data: webpBase64,
filename: media.filename,
};
return pupPage.evaluate((media) => {
return window.WWebJS.toStickerData(media);
}, media);
}
/**
* Formats a video to webp
* @param {MessageMedia} media
@@ -91,14 +75,14 @@ class Util {
static async formatVideoToWebpSticker(media) {
if (!media.mimetype.includes('video'))
throw new Error('media is not a video');
const videoType = media.mimetype.split('/')[1];
const tempFile = path.join(
tmpdir(),
`${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.webp`
);
const stream = new (require('stream').Readable)();
const buffer = Buffer.from(
media.data.replace(`data:${media.mimetype};base64,`, ''),
@@ -135,17 +119,17 @@ class Util {
.toFormat('webp')
.save(tempFile);
});
const data = await fs.readFile(tempFile, 'base64');
await fs.unlink(tempFile);
return {
return {
mimetype: 'image/webp',
data: data,
filename: media.filename,
};
}
/**
* Sticker metadata.
* @typedef {Object} StickerMetadata
@@ -161,14 +145,14 @@ class Util {
*
* @returns {Promise<MessageMedia>} media in webp format
*/
static async formatToWebpSticker(media, metadata) {
static async formatToWebpSticker(media, metadata, pupPage) {
let webpMedia;
if (media.mimetype.includes('image'))
webpMedia = await this.formatImageToWebpSticker(media);
else if (media.mimetype.includes('video'))
if (media.mimetype.includes('image'))
webpMedia = await this.formatImageToWebpSticker(media, pupPage);
else if (media.mimetype.includes('video'))
webpMedia = await this.formatVideoToWebpSticker(media);
else
else
throw new Error('Invalid media format');
if (metadata.name || metadata.author) {

View File

@@ -3,8 +3,12 @@
These tests require an authenticated WhatsApp Web session, as well as an additional phone that you can send messages to.
This can be configured using the following environment variables:
- `WWEBJS_TEST_SESSION`: A JSON-formatted string with the session details. Must include `WABrowserId`, `WASecretBundle`, `WAToken1` and `WAToken2`.
- `WWEBJS_TEST_SESSION_PATH`: Path to a JSON file that contains the session details. Must include `WABrowserId`, `WASecretBundle`, `WAToken1` and `WAToken2`.
- `WWEBJS_TEST_SESSION`: A JSON-formatted string with legacy auth session details. Must include `WABrowserId`, `WASecretBundle`, `WAToken1` and `WAToken2`.
- `WWEBJS_TEST_SESSION_PATH`: Path to a JSON file that contains the legacy auth session details. Must include `WABrowserId`, `WASecretBundle`, `WAToken1` and `WAToken2`.
- `WWEBJS_TEST_CLIENT_ID`: `clientId` to use for local file based authentication.
- `WWEBJS_TEST_REMOTE_ID`: A valid WhatsApp ID that you can send messages to, e.g. `123456789@c.us`. It should be different from the ID used by the provided session.
You *must* set `WWEBJS_TEST_REMOTE_ID` **and** either `WWEBJS_TEST_SESSION` or `WWEBJS_TEST_SESSION_PATH` for the tests to run properly.
You *must* set `WWEBJS_TEST_REMOTE_ID` **and** either `WWEBJS_TEST_SESSION`, `WWEBJS_TEST_SESSION_PATH` or `WWEBJS_TEST_CLIENT_ID` for the tests to run properly.
### Multidevice
Some of the tested functionality depends on whether the account has multidevice enabled or not. If you are using multidevice, you should set `WWEBJS_TEST_MD=1`.

View File

@@ -1,4 +1,5 @@
const {expect} = require('chai');
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const sinon = require('sinon');
const helper = require('./helper');
@@ -9,7 +10,11 @@ const MessageMedia = require('../src/structures/MessageMedia');
const Location = require('../src/structures/Location');
const { MessageTypes, WAState } = require('../src/util/Constants');
const expect = chai.expect;
chai.use(chaiAsPromised);
const remoteId = helper.remoteId;
const isMD = helper.isMD();
describe('Client', function() {
describe('Authentication', function() {
@@ -47,76 +52,6 @@ describe('Client', function() {
expect(disconnectedCallback.calledOnceWith('Max qrcode retries reached')).to.eql(true);
});
it('should fail auth if session is invalid', async function() {
this.timeout(40000);
const authFailCallback = sinon.spy();
const qrCallback = sinon.spy();
const readyCallback = sinon.spy();
const client = helper.createClient({
options: {
session: {
WABrowserId: 'invalid',
WASecretBundle: 'invalid',
WAToken1: 'invalid',
WAToken2: 'invalid'
},
authTimeoutMs: 10000,
restartOnAuthFail: false
}
});
client.on('qr', qrCallback);
client.on('auth_failure', authFailCallback);
client.on('ready', readyCallback);
client.initialize();
await helper.sleep(25000);
expect(authFailCallback.called).to.equal(true);
expect(authFailCallback.args[0][0]).to.equal('Unable to log in. Are the session details valid?');
expect(readyCallback.called).to.equal(false);
expect(qrCallback.called).to.equal(false);
await client.destroy();
});
it('can restart without a session if session was invalid and restartOnAuthFail=true', async function() {
this.timeout(40000);
const authFailCallback = sinon.spy();
const qrCallback = sinon.spy();
const client = helper.createClient({
options:{
session: {
WABrowserId: 'invalid',
WASecretBundle: 'invalid',
WAToken1: 'invalid',
WAToken2: 'invalid'
},
authTimeoutMs: 10000,
restartOnAuthFail: true
}
});
client.on('auth_failure', authFailCallback);
client.on('qr', qrCallback);
client.initialize();
await helper.sleep(35000);
expect(authFailCallback.called).to.equal(true);
expect(qrCallback.called).to.equal(true);
expect(qrCallback.args[0][0]).to.have.lengthOf(152);
await client.destroy();
});
it('should authenticate with existing session', async function() {
this.timeout(40000);
@@ -124,7 +59,10 @@ describe('Client', function() {
const qrCallback = sinon.spy();
const readyCallback = sinon.spy();
const client = helper.createClient({withSession: true});
const client = helper.createClient({
authenticated: true,
});
client.on('qr', qrCallback);
client.on('authenticated', authenticatedCallback);
client.on('ready', readyCallback);
@@ -132,59 +70,137 @@ describe('Client', function() {
await client.initialize();
expect(authenticatedCallback.called).to.equal(true);
const newSession = authenticatedCallback.args[0][0];
expect(newSession).to.have.key([
'WABrowserId',
'WASecretBundle',
'WAToken1',
'WAToken2'
]);
expect(authenticatedCallback.called).to.equal(true);
if(helper.isUsingDeprecatedSession()) {
const newSession = authenticatedCallback.args[0][0];
expect(newSession).to.have.key([
'WABrowserId',
'WASecretBundle',
'WAToken1',
'WAToken2'
]);
}
expect(readyCallback.called).to.equal(true);
expect(qrCallback.called).to.equal(false);
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('Non-MD only', function () {
if(!isMD) {
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({
authenticated: true,
options: { takeoverOnConflict: true, takeoverTimeoutMs: 5000 }
});
const client2 = helper.createClient({authenticated: 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();
});
it('should fail auth if session is invalid', async function() {
this.timeout(40000);
const authFailCallback = sinon.spy();
const qrCallback = sinon.spy();
const readyCallback = sinon.spy();
const client = helper.createClient({
options: {
session: {
WABrowserId: 'invalid',
WASecretBundle: 'invalid',
WAToken1: 'invalid',
WAToken2: 'invalid'
},
authTimeoutMs: 10000,
restartOnAuthFail: false,
useDeprecatedSessionAuth: true
}
});
client.on('qr', qrCallback);
client.on('auth_failure', authFailCallback);
client.on('ready', readyCallback);
client.initialize();
await helper.sleep(25000);
expect(authFailCallback.called).to.equal(true);
expect(authFailCallback.args[0][0]).to.equal('Unable to log in. Are the session details valid?');
expect(readyCallback.called).to.equal(false);
expect(qrCallback.called).to.equal(false);
await client.destroy();
});
it('can restart without a session if session was invalid and restartOnAuthFail=true', async function() {
this.timeout(40000);
const authFailCallback = sinon.spy();
const qrCallback = sinon.spy();
const client = helper.createClient({
options:{
session: {
WABrowserId: 'invalid',
WASecretBundle: 'invalid',
WAToken1: 'invalid',
WAToken2: 'invalid'
},
authTimeoutMs: 10000,
restartOnAuthFail: true,
useDeprecatedSessionAuth: true
}
});
client.on('auth_failure', authFailCallback);
client.on('qr', qrCallback);
client.initialize();
await helper.sleep(35000);
expect(authFailCallback.called).to.equal(true);
expect(qrCallback.called).to.equal(true);
expect(qrCallback.args[0][0]).to.have.lengthOf(152);
await client.destroy();
});
}
});
});
describe('Authenticated', function() {
@@ -192,7 +208,7 @@ describe('Client', function() {
before(async function() {
this.timeout(35000);
client = helper.createClient({withSession: true});
client = helper.createClient({authenticated: true});
await client.initialize();
});
@@ -221,27 +237,38 @@ describe('Client', function() {
'BlockContact',
'Call',
'Chat',
'ChatState',
'Cmd',
'Conn',
'Contact',
'DownloadManager',
'Features',
'GroupMetadata',
'GroupParticipants',
'GroupUtils',
'Invite',
'InviteInfo',
'JoinInviteV4',
'Label',
'MediaObject',
'MediaPrep',
'MediaTypes',
'MediaUpload',
'MessageInfo',
'Msg',
'MsgKey',
'OpaqueData',
'QueryOrder',
'QueryProduct',
'PresenceUtils',
'QueryExist',
'QueryProduct',
'QueryOrder',
'SendClear',
'SendDelete',
'SendMessage',
'SendSeen',
'StatusUtils',
'Sticker',
'UploadUtils',
'UserConstructor',
@@ -249,7 +276,9 @@ describe('Client', function() {
'Validators',
'Wap',
'WidFactory',
'genId'
'findCommonGroups',
'genId',
'getProfilePicFull',
];
const loadedModules = await client.pupPage.evaluate((expectedModules) => {
@@ -535,8 +564,6 @@ END:VCARD`;
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');
@@ -581,4 +608,4 @@ END:VCARD`;
});
});
});
});
});

View File

@@ -1,13 +1,25 @@
const path = require('path');
const crypto = require('crypto');
const Client = require('../src/Client');
const Util = require('../src/util/Util');
require('dotenv').config();
const remoteId = process.env.WWEBJS_TEST_REMOTE_ID;
if(!remoteId) throw new Error('The WWEBJS_TEST_REMOTE_ID environment variable has not been set.');
function isUsingDeprecatedSession() {
return Boolean(process.env.WWEBJS_TEST_SESSION || process.env.WWEBJS_TEST_SESSION_PATH);
}
function isMD() {
return Boolean(process.env.WWEBJS_TEST_MD);
}
if(isUsingDeprecatedSession() && isMD()) throw 'Cannot use deprecated sessions with WWEBJS_TEST_MD=true';
function getSessionFromEnv() {
if (!isUsingDeprecatedSession()) return null;
const envSession = process.env.WWEBJS_TEST_SESSION;
if(envSession) return JSON.parse(envSession);
@@ -16,17 +28,27 @@ function getSessionFromEnv() {
const absPath = path.resolve(process.cwd(), envSessionPath);
return require(absPath);
}
throw new Error('No session found in environment.');
}
function createClient({withSession, options: additionalOpts}={}) {
function createClient({authenticated, options: additionalOpts}={}) {
const options = {};
if(withSession) {
options.session = getSessionFromEnv();
if(authenticated) {
const deprecatedSession = getSessionFromEnv();
if(deprecatedSession) {
options.session = deprecatedSession;
options.useDeprecatedSessionAuth = true;
} else {
const clientId = process.env.WWEBJS_TEST_CLIENT_ID;
if(!clientId) throw new Error('No session found in environment.');
options.clientId = clientId;
}
} else {
options.clientId = crypto.randomBytes(5).toString('hex');
}
return new Client(Util.mergeDefault(options, additionalOpts || {}));
const allOpts = {...options, ...(additionalOpts || {})};
return new Client(allOpts);
}
function sleep(ms) {
@@ -36,5 +58,7 @@ function sleep(ms) {
module.exports = {
sleep,
createClient,
remoteId
isUsingDeprecatedSession,
isMD,
remoteId,
};

View File

@@ -13,7 +13,7 @@ describe('Chat', function () {
before(async function() {
this.timeout(35000);
client = helper.createClient({ withSession: true });
client = helper.createClient({ authenticated: true });
await client.initialize();
chat = await client.getChatById(remoteId);
});
@@ -32,9 +32,9 @@ describe('Chat', function () {
});
it('can fetch messages sent in a chat', async function () {
this.timeout(5000);
await helper.sleep(1000);
const msg = await chat.sendMessage('another message');
await helper.sleep(500);
const messages = await chat.fetchMessages();
expect(messages.length).to.be.greaterThanOrEqual(2);
@@ -49,6 +49,7 @@ describe('Chat', function () {
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');
await helper.sleep(500);
const messages = await chat.fetchMessages({limit: 1});
expect(messages).to.have.lengthOf(1);
@@ -80,6 +81,8 @@ describe('Chat', function () {
const res = await chat.sendSeen();
expect(res).to.equal(true);
await helper.sleep(1000);
// refresh chat
chat = await client.getChatById(remoteId);
expect(chat.unreadCount).to.equal(0);
@@ -137,6 +140,8 @@ describe('Chat', function () {
it('can mute a chat forever', async function() {
await chat.mute();
await helper.sleep(1000);
// refresh chat
chat = await client.getChatById(remoteId);
expect(chat.isMuted).to.equal(true);
@@ -147,6 +152,8 @@ describe('Chat', function () {
const unmuteDate = new Date(new Date().getTime() + (1000*60*60));
await chat.mute(unmuteDate);
await helper.sleep(1000);
// refresh chat
chat = await client.getChatById(remoteId);
expect(chat.isMuted).to.equal(true);
@@ -168,9 +175,7 @@ describe('Chat', function () {
// 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);
it('can clear all messages from chat', async function () {
const res = await chat.clearMessages();
expect(res).to.equal(true);

227
tests/structures/group.js Normal file
View File

@@ -0,0 +1,227 @@
const { expect } = require('chai');
const helper = require('../helper');
const remoteId = helper.remoteId;
describe('Group', function() {
let client;
let group;
before(async function() {
this.timeout(35000);
client = helper.createClient({
authenticated: true,
});
await client.initialize();
const createRes = await client.createGroup('My Awesome Group', [remoteId]);
expect(createRes.gid).to.exist;
await helper.sleep(500);
group = await client.getChatById(createRes.gid._serialized);
expect(group).to.exist;
});
beforeEach(async function () {
await helper.sleep(500);
});
describe('Settings', function () {
it('can change the group subject', async function () {
expect(group.name).to.equal('My Awesome Group');
const res = await group.setSubject('My Amazing Group');
expect(res).to.equal(true);
await helper.sleep(1000);
// reload
group = await client.getChatById(group.id._serialized);
expect(group.name).to.equal('My Amazing Group');
});
it('can change the group description', async function () {
expect(group.description).to.equal(undefined);
const res = await group.setDescription('some description');
expect(res).to.equal(true);
expect(group.description).to.equal('some description');
await helper.sleep(1000);
// reload
group = await client.getChatById(group.id._serialized);
expect(group.description).to.equal('some description');
});
it('can set only admins able to send messages', async function () {
expect(group.groupMetadata.announce).to.equal(false);
const res = await group.setMessagesAdminsOnly();
expect(res).to.equal(true);
expect(group.groupMetadata.announce).to.equal(true);
await helper.sleep(1000);
// reload
group = await client.getChatById(group.id._serialized);
expect(group.groupMetadata.announce).to.equal(true);
});
it('can set all participants able to send messages', async function () {
expect(group.groupMetadata.announce).to.equal(true);
const res = await group.setMessagesAdminsOnly(false);
expect(res).to.equal(true);
expect(group.groupMetadata.announce).to.equal(false);
await helper.sleep(1000);
// reload
group = await client.getChatById(group.id._serialized);
expect(group.groupMetadata.announce).to.equal(false);
});
it('can set only admins able to set group info', async function () {
expect(group.groupMetadata.restrict).to.equal(false);
const res = await group.setInfoAdminsOnly();
expect(res).to.equal(true);
expect(group.groupMetadata.restrict).to.equal(true);
await helper.sleep(1000);
// reload
group = await client.getChatById(group.id._serialized);
expect(group.groupMetadata.restrict).to.equal(true);
});
it('can set all participants able to set group info', async function () {
expect(group.groupMetadata.restrict).to.equal(true);
const res = await group.setInfoAdminsOnly(false);
expect(res).to.equal(true);
expect(group.groupMetadata.restrict).to.equal(false);
await helper.sleep(1000);
// reload
group = await client.getChatById(group.id._serialized);
expect(group.groupMetadata.restrict).to.equal(false);
});
});
describe('Invites', function () {
it('can get the invite code', async function () {
const code = await group.getInviteCode();
expect(typeof code).to.equal('string');
});
it('can get invite info', async function () {
const code = await group.getInviteCode();
const info = await client.getInviteInfo(code);
expect(info.id._serialized).to.equal(group.id._serialized);
expect(info.participants.length).to.equal(2);
});
it('can revoke the invite code', async function () {
const code = await group.getInviteCode();
const newCode = await group.revokeInvite();
expect(typeof newCode).to.equal('string');
expect(newCode).to.not.equal(code);
});
});
describe('Participants', function () {
it('can promote a user to admin', async function () {
let participant = group.participants.find(p => p.id._serialized === remoteId);
expect(participant).to.exist;
expect(participant.isAdmin).to.equal(false);
const res = await group.promoteParticipants([remoteId]);
expect(res.status).to.be.greaterThanOrEqual(200);
await helper.sleep(1000);
// reload and check
group = await client.getChatById(group.id._serialized);
participant = group.participants.find(p => p.id._serialized=== remoteId);
expect(participant).to.exist;
expect(participant.isAdmin).to.equal(true);
});
it('can demote a user', async function () {
let participant = group.participants.find(p => p.id._serialized=== remoteId);
expect(participant).to.exist;
expect(participant.isAdmin).to.equal(true);
const res = await group.demoteParticipants([remoteId]);
expect(res.status).to.be.greaterThanOrEqual(200);
await helper.sleep(1000);
// reload and check
group = await client.getChatById(group.id._serialized);
participant = group.participants.find(p => p.id._serialized=== remoteId);
expect(participant).to.exist;
expect(participant.isAdmin).to.equal(false);
});
it('can remove a user from the group', async function () {
let participant = group.participants.find(p => p.id._serialized=== remoteId);
expect(participant).to.exist;
const res = await group.removeParticipants([remoteId]);
expect(res.status).to.be.greaterThanOrEqual(200);
await helper.sleep(1000);
// reload and check
group = await client.getChatById(group.id._serialized);
participant = group.participants.find(p => p.id._serialized=== remoteId);
expect(participant).to.not.exist;
});
it('can add back a user to the group', async function () {
let participant = group.participants.find(p => p.id._serialized=== remoteId);
expect(participant).to.not.exist;
const res = await group.addParticipants([remoteId]);
expect(res.status).to.be.greaterThanOrEqual(200);
await helper.sleep(1000);
// reload and check
group = await client.getChatById(group.id._serialized);
participant = group.participants.find(p => p.id._serialized=== remoteId);
expect(participant).to.exist;
});
});
describe('Leave / re-join', function () {
let code;
before(async function () {
code = await group.getInviteCode();
});
it('can leave the group', async function () {
expect(group.isReadOnly).to.equal(false);
await group.leave();
await helper.sleep(1000);
// reload and check
group = await client.getChatById(group.id._serialized);
expect(group.isReadOnly).to.equal(true);
});
it('can join a group via invite code', async function () {
const chatId = await client.acceptInvite(code);
expect(chatId).to.equal(group.id._serialized);
await helper.sleep(1000);
// reload and check
group = await client.getChatById(group.id._serialized);
expect(group.isReadOnly).to.equal(false);
});
});
after(async function () {
await client.destroy();
});
});

112
tests/structures/message.js Normal file
View File

@@ -0,0 +1,112 @@
const { expect } = require('chai');
const sinon = require('sinon');
const helper = require('../helper');
const { Contact, Chat } = require('../../src/structures');
const remoteId = helper.remoteId;
describe('Message', function () {
let client;
let chat;
let message;
before(async function() {
this.timeout(35000);
client = helper.createClient({ authenticated: true });
await client.initialize();
chat = await client.getChatById(remoteId);
message = await chat.sendMessage('this is only a test');
// wait for message to be sent
await helper.sleep(1000);
});
after(async function () {
await client.destroy();
});
it('can get the related chat', async function () {
const chat = await message.getChat();
expect(chat).to.be.instanceOf(Chat);
expect(chat.id._serialized).to.equal(remoteId);
});
it('can get the related contact', async function () {
const contact = await message.getContact();
expect(contact).to.be.instanceOf(Contact);
expect(contact.id._serialized).to.equal(client.info.wid._serialized);
});
it('can get message info', async function () {
const info = await message.getInfo();
expect(typeof info).to.equal('object');
expect(Array.isArray(info.played)).to.equal(true);
expect(Array.isArray(info.read)).to.equal(true);
expect(Array.isArray(info.delivery)).to.equal(true);
});
describe('Replies', function () {
let replyMsg;
it('can reply to a message', async function () {
replyMsg = await message.reply('this is my reply');
expect(replyMsg.hasQuotedMsg).to.equal(true);
});
it('can get the quoted message', async function () {
const quotedMsg = await replyMsg.getQuotedMessage();
expect(quotedMsg.id._serialized).to.equal(message.id._serialized);
});
});
describe('Star', function () {
it('can star a message', async function () {
expect(message.isStarred).to.equal(false);
await message.star();
// reload and check
await message.reload();
expect(message.isStarred).to.equal(true);
});
it('can un-star a message', async function () {
expect(message.isStarred).to.equal(true);
await message.unstar();
// reload and check
await message.reload();
expect(message.isStarred).to.equal(false);
});
});
describe('Delete', function () {
it('can delete a message for me', async function () {
await message.delete();
await helper.sleep(1000);
expect(await message.reload()).to.equal(null);
});
it('can delete a message for everyone', async function () {
message = await chat.sendMessage('sneaky message');
await helper.sleep(1000);
const callback = sinon.spy();
client.once('message_revoke_everyone', callback);
await message.delete(true);
await helper.sleep(1000);
expect(await message.reload()).to.equal(null);
expect(callback.called).to.equal(true);
const [ revokeMsg, originalMsg ] = callback.args[0];
expect(revokeMsg.id._serialized).to.equal(originalMsg.id._serialized);
expect(originalMsg.body).to.equal('sneaky message');
expect(originalMsg.type).to.equal('chat');
expect(revokeMsg.body).to.equal('');
expect(revokeMsg.type).to.equal('revoked');
});
});
});