mirror of
https://github.com/cheveguerra/whatsapp-web.js.git
synced 2026-04-20 20:49:14 +00:00
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:
@@ -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
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -64,8 +64,13 @@ typings/
|
|||||||
# next.js build output
|
# next.js build output
|
||||||
.next
|
.next
|
||||||
|
|
||||||
# macOS Thumbnails
|
# macOS
|
||||||
._*
|
._*
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# Test sessions
|
# Test sessions
|
||||||
*session.json
|
*session.json
|
||||||
|
|
||||||
|
# user data
|
||||||
|
WWebJS/
|
||||||
|
userDataDir/
|
||||||
35
example.js
35
example.js
@@ -1,15 +1,9 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const { Client, Location, List, Buttons } = require('./index');
|
const { Client, Location, List, Buttons } = require('./index');
|
||||||
|
|
||||||
const SESSION_FILE_PATH = './session.json';
|
const client = new Client({
|
||||||
let sessionCfg;
|
clientId: 'example',
|
||||||
if (fs.existsSync(SESSION_FILE_PATH)) {
|
puppeteer: { headless: false }
|
||||||
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.
|
|
||||||
|
|
||||||
// You also could connect to an existing instance of a browser
|
// You also could connect to an existing instance of a browser
|
||||||
// {
|
// {
|
||||||
@@ -25,18 +19,12 @@ client.on('qr', (qr) => {
|
|||||||
console.log('QR RECEIVED', qr);
|
console.log('QR RECEIVED', qr);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('authenticated', (session) => {
|
client.on('authenticated', () => {
|
||||||
console.log('AUTHENTICATED', session);
|
console.log('AUTHENTICATED');
|
||||||
sessionCfg=session;
|
|
||||||
fs.writeFile(SESSION_FILE_PATH, JSON.stringify(session), function (err) {
|
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('auth_failure', msg => {
|
client.on('auth_failure', msg => {
|
||||||
// Fired if session restore was unsuccessfull
|
// Fired if session restore was unsuccessful
|
||||||
console.error('AUTHENTICATION FAILURE', msg);
|
console.error('AUTHENTICATION FAILURE', msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,9 +112,8 @@ client.on('message', async msg => {
|
|||||||
client.sendMessage(msg.from, `
|
client.sendMessage(msg.from, `
|
||||||
*Connection info*
|
*Connection info*
|
||||||
User name: ${info.pushname}
|
User name: ${info.pushname}
|
||||||
My number: ${info.me.user}
|
My number: ${info.wid.user}
|
||||||
Platform: ${info.platform}
|
Platform: ${info.platform}
|
||||||
WhatsApp version: ${info.phone.wa_version}
|
|
||||||
`);
|
`);
|
||||||
} else if (msg.body === '!mediainfo' && msg.hasMedia) {
|
} else if (msg.body === '!mediainfo' && msg.hasMedia) {
|
||||||
const attachmentData = await msg.downloadMedia();
|
const attachmentData = await msg.downloadMedia();
|
||||||
@@ -267,12 +254,6 @@ client.on('group_update', (notification) => {
|
|||||||
console.log('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 => {
|
client.on('change_state', state => {
|
||||||
console.log('CHANGE STATE', state );
|
console.log('CHANGE STATE', state );
|
||||||
});
|
});
|
||||||
|
|||||||
75
index.d.ts
vendored
75
index.d.ts
vendored
@@ -84,6 +84,9 @@ declare namespace WAWebJS {
|
|||||||
/** Returns the contact ID's profile picture URL, if privacy settings allow it */
|
/** Returns the contact ID's profile picture URL, if privacy settings allow it */
|
||||||
getProfilePicUrl(contactId: string): Promise<string>
|
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 */
|
/** Gets the current connection state for the client */
|
||||||
getState(): Promise<WAState>
|
getState(): Promise<WAState>
|
||||||
|
|
||||||
@@ -118,6 +121,9 @@ declare namespace WAWebJS {
|
|||||||
/** Marks the client as online */
|
/** Marks the client as online */
|
||||||
sendPresenceAvailable(): Promise<void>
|
sendPresenceAvailable(): Promise<void>
|
||||||
|
|
||||||
|
/** Marks the client as offline */
|
||||||
|
sendPresenceUnavailable(): Promise<void>
|
||||||
|
|
||||||
/** Mark as seen for the Chat */
|
/** Mark as seen for the Chat */
|
||||||
sendSeen(chatId: string): Promise<boolean>
|
sendSeen(chatId: string): Promise<boolean>
|
||||||
|
|
||||||
@@ -134,7 +140,7 @@ declare namespace WAWebJS {
|
|||||||
* Sets the current user's display name
|
* Sets the current user's display name
|
||||||
* @param displayName New 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 */
|
/** Changes and returns the archive state of the Chat */
|
||||||
unarchiveChat(chatId: string): Promise<boolean>
|
unarchiveChat(chatId: string): Promise<boolean>
|
||||||
@@ -150,11 +156,17 @@ declare namespace WAWebJS {
|
|||||||
|
|
||||||
/** Emitted when authentication is successful */
|
/** Emitted when authentication is successful */
|
||||||
on(event: 'authenticated', listener: (
|
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
|
) => 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
|
on(event: 'change_battery', listener: (batteryInfo: BatteryInfo) => void): this
|
||||||
|
|
||||||
/** Emitted when the connection state changes */
|
/** Emitted when the connection state changes */
|
||||||
@@ -249,14 +261,12 @@ declare namespace WAWebJS {
|
|||||||
|
|
||||||
/** Current connection information */
|
/** Current connection information */
|
||||||
export interface ClientInfo {
|
export interface ClientInfo {
|
||||||
/**
|
|
||||||
* Current user ID
|
|
||||||
* @deprecated Use .wid instead
|
|
||||||
*/
|
|
||||||
me: ContactId
|
|
||||||
/** Current user ID */
|
/** Current user ID */
|
||||||
wid: ContactId
|
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
|
phone: ClientInfoPhone
|
||||||
/** Platform the phone is running on */
|
/** Platform the phone is running on */
|
||||||
platform: string
|
platform: string
|
||||||
@@ -267,7 +277,10 @@ declare namespace WAWebJS {
|
|||||||
getBatteryStatus: () => Promise<BatteryInfo>
|
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 {
|
export interface ClientInfoPhone {
|
||||||
/** WhatsApp Version running on the phone */
|
/** WhatsApp Version running on the phone */
|
||||||
wa_version: string
|
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
|
/** Restart client with a new session (i.e. use null 'session' var) if authentication fails
|
||||||
* @default false */
|
* @default false */
|
||||||
restartOnAuthFail?: boolean
|
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
|
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
|
/** If another whatsapp web session is detected (another browser), take over the session in the current browser
|
||||||
* @default false */
|
* @default false */
|
||||||
takeoverOnConflict?: boolean,
|
takeoverOnConflict?: boolean,
|
||||||
@@ -314,9 +338,15 @@ declare namespace WAWebJS {
|
|||||||
/** Ffmpeg path to use when formating videos to webp while sending stickers
|
/** Ffmpeg path to use when formating videos to webp while sending stickers
|
||||||
* @default 'ffmpeg' */
|
* @default 'ffmpeg' */
|
||||||
ffmpegPath?: string
|
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 {
|
export interface ClientSession {
|
||||||
WABrowserId: string,
|
WABrowserId: string,
|
||||||
WASecretBundle: string,
|
WASecretBundle: string,
|
||||||
@@ -324,6 +354,9 @@ declare namespace WAWebJS {
|
|||||||
WAToken2: string,
|
WAToken2: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
export interface BatteryInfo {
|
export interface BatteryInfo {
|
||||||
/** The current battery percentage */
|
/** The current battery percentage */
|
||||||
battery: number,
|
battery: number,
|
||||||
@@ -608,6 +641,13 @@ declare namespace WAWebJS {
|
|||||||
selectedButtonId?: string,
|
selectedButtonId?: string,
|
||||||
/** Selected list row ID */
|
/** Selected list row ID */
|
||||||
selectedRowId?: string,
|
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 */
|
/** Accept the Group V4 Invite in message */
|
||||||
acceptGroupV4Invite: () => Promise<{status: number}>,
|
acceptGroupV4Invite: () => Promise<{status: number}>,
|
||||||
/** Deletes the message from the chat */
|
/** Deletes the message from the chat */
|
||||||
@@ -679,7 +719,7 @@ declare namespace WAWebJS {
|
|||||||
|
|
||||||
/** Options for sending a message */
|
/** Options for sending a message */
|
||||||
export interface MessageSendOptions {
|
export interface MessageSendOptions {
|
||||||
/** Show links preview */
|
/** Show links preview. Has no effect on multi-device accounts. */
|
||||||
linkPreview?: boolean
|
linkPreview?: boolean
|
||||||
/** Send audio as voice message */
|
/** Send audio as voice message */
|
||||||
sendAudioAsVoice?: boolean
|
sendAudioAsVoice?: boolean
|
||||||
@@ -835,6 +875,9 @@ declare namespace WAWebJS {
|
|||||||
/** Gets the Contact's current "about" info. Returns null if you don't have permission to read their status. */
|
/** Gets the Contact's current "about" info. Returns null if you don't have permission to read their status. */
|
||||||
getAbout: () => Promise<string | null>,
|
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[]>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContactId {
|
export interface ContactId {
|
||||||
@@ -1011,9 +1054,9 @@ declare namespace WAWebJS {
|
|||||||
/** Demotes participants by IDs to regular users */
|
/** Demotes participants by IDs to regular users */
|
||||||
demoteParticipants: ChangeParticipantsPermisions;
|
demoteParticipants: ChangeParticipantsPermisions;
|
||||||
/** Updates the group subject */
|
/** Updates the group subject */
|
||||||
setSubject: (subject: string) => Promise<void>;
|
setSubject: (subject: string) => Promise<boolean>;
|
||||||
/** Updates the group description */
|
/** 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
|
/** Updates the group settings to only allow admins to send messages
|
||||||
* @param {boolean} [adminsOnly=true] Enable or disable this option
|
* @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.
|
* @returns {Promise<boolean>} Returns true if the setting was properly updated. This can return false if the user does not have the necessary permissions.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"main": "./index.js",
|
"main": "./index.js",
|
||||||
"typings": "./index.d.ts",
|
"typings": "./index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha tests --recursive",
|
"test": "mocha tests --recursive --timeout 5000",
|
||||||
"test-single": "mocha",
|
"test-single": "mocha",
|
||||||
"shell": "node --experimental-repl-await ./shell.js",
|
"shell": "node --experimental-repl-await ./shell.js",
|
||||||
"generate-docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose"
|
"generate-docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose"
|
||||||
@@ -35,12 +35,12 @@
|
|||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
"node-fetch": "^2.6.5",
|
"node-fetch": "^2.6.5",
|
||||||
"node-webpmux": "^3.1.0",
|
"node-webpmux": "^3.1.0",
|
||||||
"puppeteer": "^13.0.0",
|
"puppeteer": "^13.0.0"
|
||||||
"sharp": "^0.28.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node-fetch": "^2.5.12",
|
"@types/node-fetch": "^2.5.12",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
|
"chai-as-promised": "^7.1.1",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"eslint": "^8.4.1",
|
"eslint": "^8.4.1",
|
||||||
"eslint-plugin-mocha": "^10.0.3",
|
"eslint-plugin-mocha": "^10.0.3",
|
||||||
|
|||||||
13
shell.js
13
shell.js
@@ -7,19 +7,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const repl = require('repl');
|
const repl = require('repl');
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const { Client } = require('./index');
|
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({
|
const client = new Client({
|
||||||
puppeteer: { headless: false },
|
puppeteer: { headless: false },
|
||||||
session: sessionCfg
|
clientId: 'shell'
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Initializing...');
|
console.log('Initializing...');
|
||||||
@@ -30,6 +23,10 @@ client.on('qr', () => {
|
|||||||
console.log('Please scan the QR code on the browser.');
|
console.log('Please scan the QR code on the browser.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
client.on('authenticated', (session) => {
|
||||||
|
console.log(JSON.stringify(session));
|
||||||
|
});
|
||||||
|
|
||||||
client.on('ready', () => {
|
client.on('ready', () => {
|
||||||
const shell = repl.start('wwebjs> ');
|
const shell = repl.start('wwebjs> ');
|
||||||
shell.context.client = client;
|
shell.context.client = client;
|
||||||
|
|||||||
288
src/Client.js
288
src/Client.js
@@ -1,9 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const puppeteer = require('puppeteer');
|
const puppeteer = require('puppeteer');
|
||||||
const moduleRaid = require('@pedroslopez/moduleraid/moduleraid');
|
const moduleRaid = require('@pedroslopez/moduleraid/moduleraid');
|
||||||
const jsQR = require('jsqr');
|
|
||||||
|
|
||||||
const Util = require('./util/Util');
|
const Util = require('./util/Util');
|
||||||
const InterfaceController = require('./util/InterfaceController');
|
const InterfaceController = require('./util/InterfaceController');
|
||||||
@@ -18,20 +19,17 @@ const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification
|
|||||||
* @param {object} options - Client options
|
* @param {object} options - Client options
|
||||||
* @param {number} options.authTimeoutMs - Timeout for authentication selector in puppeteer
|
* @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 {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 {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 {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 {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 {string} options.session.WABrowserId
|
* @param {object} options.session - This is deprecated due to not being supported by MultiDevice, and will be removed in a future version.
|
||||||
* @param {string} options.session.WASecretBundle
|
|
||||||
* @param {string} options.session.WAToken1
|
|
||||||
* @param {string} options.session.WAToken2
|
|
||||||
* @param {number} options.takeoverOnConflict - If another whatsapp web session is detected (another browser), take over the session in the current browser
|
* @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 {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.userAgent - User agent to use in puppeteer
|
||||||
* @param {string} options.ffmpegPath - Ffmpeg path to use when formating videos to webp while sending stickers
|
* @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 {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#qr
|
||||||
* @fires Client#authenticated
|
* @fires Client#authenticated
|
||||||
@@ -48,7 +46,6 @@ const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification
|
|||||||
* @fires Client#group_update
|
* @fires Client#group_update
|
||||||
* @fires Client#disconnected
|
* @fires Client#disconnected
|
||||||
* @fires Client#change_state
|
* @fires Client#change_state
|
||||||
* @fires Client#change_battery
|
|
||||||
*/
|
*/
|
||||||
class Client extends EventEmitter {
|
class Client extends EventEmitter {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
@@ -56,6 +53,19 @@ class Client extends EventEmitter {
|
|||||||
|
|
||||||
this.options = Util.mergeDefault(DefaultOptions, options);
|
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-$()+=[\];#@~,&']+[^\. ]$/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.pupBrowser = null;
|
||||||
this.pupPage = null;
|
this.pupPage = null;
|
||||||
|
|
||||||
@@ -68,11 +78,15 @@ class Client extends EventEmitter {
|
|||||||
async initialize() {
|
async initialize() {
|
||||||
let [browser, page] = [null, null];
|
let [browser, page] = [null, null];
|
||||||
|
|
||||||
if(this.options.puppeteer && this.options.puppeteer.browserWSEndpoint) {
|
const puppeteerOpts = {
|
||||||
browser = await puppeteer.connect(this.options.puppeteer);
|
...this.options.puppeteer,
|
||||||
|
userDataDir: this.options.useDeprecatedSessionAuth ? undefined : this.dataDir
|
||||||
|
};
|
||||||
|
if (puppeteerOpts && puppeteerOpts.browserWSEndpoint) {
|
||||||
|
browser = await puppeteer.connect(puppeteerOpts);
|
||||||
page = await browser.newPage();
|
page = await browser.newPage();
|
||||||
} else {
|
} else {
|
||||||
browser = await puppeteer.launch(this.options.puppeteer);
|
browser = await puppeteer.launch(puppeteerOpts);
|
||||||
page = (await browser.pages())[0];
|
page = (await browser.pages())[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,14 +95,8 @@ class Client extends EventEmitter {
|
|||||||
this.pupBrowser = browser;
|
this.pupBrowser = browser;
|
||||||
this.pupPage = page;
|
this.pupPage = page;
|
||||||
|
|
||||||
// remember me
|
if (this.options.useDeprecatedSessionAuth && this.options.session) {
|
||||||
await page.evaluateOnNewDocument(() => {
|
await page.evaluateOnNewDocument(session => {
|
||||||
localStorage.setItem('remember-me', 'true');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.options.session) {
|
|
||||||
await page.evaluateOnNewDocument(
|
|
||||||
session => {
|
|
||||||
if (document.referrer === 'https://whatsapp.com/') {
|
if (document.referrer === 'https://whatsapp.com/') {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
localStorage.setItem('WABrowserId', session.WABrowserId);
|
localStorage.setItem('WABrowserId', session.WABrowserId);
|
||||||
@@ -96,6 +104,8 @@ class Client extends EventEmitter {
|
|||||||
localStorage.setItem('WAToken1', session.WAToken1);
|
localStorage.setItem('WAToken1', session.WAToken1);
|
||||||
localStorage.setItem('WAToken2', session.WAToken2);
|
localStorage.setItem('WAToken2', session.WAToken2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('remember-me', 'true');
|
||||||
}, this.options.session);
|
}, this.options.session);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,56 +119,55 @@ class Client extends EventEmitter {
|
|||||||
referer: 'https://whatsapp.com/'
|
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';
|
||||||
|
|
||||||
|
// 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) {
|
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
|
* Emitted when there has been an error while trying to restore an existing session
|
||||||
* @event Client#auth_failure
|
* @event Client#auth_failure
|
||||||
* @param {string} message
|
* @param {string} message
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
this.emit(Events.AUTHENTICATION_FAILURE, 'Unable to log in. Are the session details valid?');
|
this.emit(Events.AUTHENTICATION_FAILURE, 'Unable to log in. Are the session details valid?');
|
||||||
browser.close();
|
await this.destroy();
|
||||||
if (this.options.restartOnAuthFail) {
|
if (this.options.restartOnAuthFail) {
|
||||||
// session restore failed so try again but without session to force new authentication
|
// session restore failed so try again but without session to force new authentication
|
||||||
this.options.session = null;
|
this.options.session = null;
|
||||||
this.initialize();
|
return this.initialize();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw err;
|
const QR_CONTAINER = 'div[data-ref]';
|
||||||
}
|
const QR_RETRY_BUTTON = 'div[data-ref] > span > button';
|
||||||
|
|
||||||
} else {
|
|
||||||
let qrRetries = 0;
|
let qrRetries = 0;
|
||||||
|
await page.exposeFunction('qrChanged', async (qr) => {
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the QR code is received
|
* Emitted when a QR code is received
|
||||||
* @event Client#qr
|
* @event Client#qr
|
||||||
* @param {string} qr QR Code
|
* @param {string} qr QR Code
|
||||||
*/
|
*/
|
||||||
this.emit(Events.QR_RECEIVED, qr);
|
this.emit(Events.QR_RECEIVED, qr);
|
||||||
|
|
||||||
if (this.options.qrMaxRetries > 0) {
|
if (this.options.qrMaxRetries > 0) {
|
||||||
qrRetries++;
|
qrRetries++;
|
||||||
if (qrRetries > this.options.qrMaxRetries) {
|
if (qrRetries > this.options.qrMaxRetries) {
|
||||||
@@ -166,15 +175,39 @@ class Client extends EventEmitter {
|
|||||||
await this.destroy();
|
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
|
// Wait for code scan
|
||||||
try {
|
try {
|
||||||
await page.waitForSelector(KEEP_PHONE_CONNECTED_IMG_SELECTOR, { timeout: 0 });
|
await page.waitForSelector(INTRO_IMG_SELECTOR, { timeout: 0 });
|
||||||
clearInterval(this._qrRefreshInterval);
|
|
||||||
this._qrRefreshInterval = undefined;
|
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
if (
|
if (
|
||||||
error.name === 'ProtocolError' &&
|
error.name === 'ProtocolError' &&
|
||||||
@@ -187,32 +220,30 @@ class Client extends EventEmitter {
|
|||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.evaluate(ExposeStore, moduleRaid.toString());
|
await page.evaluate(ExposeStore, moduleRaid.toString());
|
||||||
|
let authEventPayload = undefined;
|
||||||
|
if (this.options.useDeprecatedSessionAuth) {
|
||||||
// Get session tokens
|
// Get session tokens
|
||||||
const localStorage = JSON.parse(await page.evaluate(() => {
|
const localStorage = JSON.parse(await page.evaluate(() => {
|
||||||
return JSON.stringify(window.localStorage);
|
return JSON.stringify(window.localStorage);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const session = {
|
authEventPayload = {
|
||||||
WABrowserId: localStorage.WABrowserId,
|
WABrowserId: localStorage.WABrowserId,
|
||||||
WASecretBundle: localStorage.WASecretBundle,
|
WASecretBundle: localStorage.WASecretBundle,
|
||||||
WAToken1: localStorage.WAToken1,
|
WAToken1: localStorage.WAToken1,
|
||||||
WAToken2: localStorage.WAToken2
|
WAToken2: localStorage.WAToken2
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when authentication is successful
|
* Emitted when authentication is successful
|
||||||
* @event Client#authenticated
|
* @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
|
// Check window.Store Injection
|
||||||
await page.waitForFunction('window.Store != undefined');
|
await page.waitForFunction('window.Store != undefined');
|
||||||
@@ -221,8 +252,17 @@ class Client extends EventEmitter {
|
|||||||
return window.Store.Features.features.MD_BACKEND;
|
return window.Store.Features.features.MD_BACKEND;
|
||||||
});
|
});
|
||||||
|
|
||||||
if(isMD) {
|
await page.evaluate(async () => {
|
||||||
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.');
|
// 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)
|
//Load util functions (serializers, helper functions)
|
||||||
@@ -234,7 +274,7 @@ class Client extends EventEmitter {
|
|||||||
* @type {ClientInfo}
|
* @type {ClientInfo}
|
||||||
*/
|
*/
|
||||||
this.info = new ClientInfo(this, await page.evaluate(() => {
|
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
|
// Add InterfaceController
|
||||||
@@ -398,11 +438,12 @@ class Client extends EventEmitter {
|
|||||||
if (battery === undefined) return;
|
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
|
* @event Client#change_battery
|
||||||
* @param {object} batteryInfo
|
* @param {object} batteryInfo
|
||||||
* @param {number} batteryInfo.battery - The current battery percentage
|
* @param {number} batteryInfo.battery - The current battery percentage
|
||||||
* @param {boolean} batteryInfo.plugged - Indicates if the phone is plugged in (true) or not (false)
|
* @param {boolean} batteryInfo.plugged - Indicates if the phone is plugged in (true) or not (false)
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
this.emit(Events.BATTERY_CHANGED, { battery, plugged });
|
this.emit(Events.BATTERY_CHANGED, { battery, plugged });
|
||||||
});
|
});
|
||||||
@@ -466,9 +507,6 @@ class Client extends EventEmitter {
|
|||||||
* Closes the client
|
* Closes the client
|
||||||
*/
|
*/
|
||||||
async destroy() {
|
async destroy() {
|
||||||
if (this._qrRefreshInterval) {
|
|
||||||
clearInterval(this._qrRefreshInterval);
|
|
||||||
}
|
|
||||||
await this.pupBrowser.close();
|
await this.pupBrowser.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,9 +514,13 @@ class Client extends EventEmitter {
|
|||||||
* Logs out the client, closing the current session
|
* Logs out the client, closing the current session
|
||||||
*/
|
*/
|
||||||
async logout() {
|
async logout() {
|
||||||
return await this.pupPage.evaluate(() => {
|
await this.pupPage.evaluate(() => {
|
||||||
return window.Store.AppState.logout();
|
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.
|
* Message options.
|
||||||
* @typedef {Object} MessageSendOptions
|
* @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} [sendAudioAsVoice=false] - Send audio as voice message
|
||||||
* @property {boolean} [sendVideoAsGif=false] - Send video as gif
|
* @property {boolean} [sendVideoAsGif=false] - Send video as gif
|
||||||
* @property {boolean} [sendMediaAsSticker=false] - Send media as a sticker
|
* @property {boolean} [sendMediaAsSticker=false] - Send media as a sticker
|
||||||
@@ -574,18 +616,20 @@ class Client extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (internalOptions.sendMediaAsSticker && internalOptions.attachment) {
|
if (internalOptions.sendMediaAsSticker && internalOptions.attachment) {
|
||||||
internalOptions.attachment =
|
internalOptions.attachment = await Util.formatToWebpSticker(
|
||||||
await Util.formatToWebpSticker(internalOptions.attachment, {
|
internalOptions.attachment, {
|
||||||
name: options.stickerName,
|
name: options.stickerName,
|
||||||
author: options.stickerAuthor,
|
author: options.stickerAuthor,
|
||||||
categories: options.stickerCategories
|
categories: options.stickerCategories
|
||||||
});
|
}, this.pupPage
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newMessage = await this.pupPage.evaluate(async (chatId, message, options, sendSeen) => {
|
const newMessage = await this.pupPage.evaluate(async (chatId, message, options, sendSeen) => {
|
||||||
const chatWid = window.Store.WidFactory.createWid(chatId);
|
const chatWid = window.Store.WidFactory.createWid(chatId);
|
||||||
const chat = await window.Store.Chat.find(chatWid);
|
const chat = await window.Store.Chat.find(chatWid);
|
||||||
|
|
||||||
|
|
||||||
if (sendSeen) {
|
if (sendSeen) {
|
||||||
window.WWebJS.sendSeen(chatId);
|
window.WWebJS.sendSeen(chatId);
|
||||||
}
|
}
|
||||||
@@ -672,7 +716,7 @@ class Client extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
async getInviteInfo(inviteCode) {
|
async getInviteInfo(inviteCode) {
|
||||||
return await this.pupPage.evaluate(inviteCode => {
|
return await this.pupPage.evaluate(inviteCode => {
|
||||||
return window.Store.Wap.groupInviteInfo(inviteCode);
|
return window.Store.InviteInfo.sendQueryGroupInvite(inviteCode);
|
||||||
}, inviteCode);
|
}, inviteCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,15 +735,15 @@ class Client extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Accepts a private invitation to join a group
|
* Accepts a private invitation to join a group
|
||||||
* @param {object} inviteV4 Invite V4 Info
|
* @param {object} inviteInfo Invite V4 Info
|
||||||
* @returns {Promise<Object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
async acceptGroupV4Invite(inviteInfo) {
|
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';
|
if (inviteInfo.inviteCodeExp == 0) throw 'Expired invite code';
|
||||||
return await this.pupPage.evaluate(async inviteInfo => {
|
return this.pupPage.evaluate(async inviteInfo => {
|
||||||
let { groupId, fromId, inviteCode, inviteCodeExp, toId } = inviteInfo;
|
let { groupId, fromId, inviteCode, inviteCodeExp } = inviteInfo;
|
||||||
return await window.Store.Wap.acceptGroupV4Invite(groupId, fromId, inviteCode, String(inviteCodeExp), toId);
|
return await window.Store.JoinInviteV4.sendJoinGroupViaInviteV4(inviteCode, String(inviteCodeExp), groupId, fromId);
|
||||||
}, inviteInfo);
|
}, inviteInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,7 +753,7 @@ class Client extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
async setStatus(status) {
|
async setStatus(status) {
|
||||||
await this.pupPage.evaluate(async status => {
|
await this.pupPage.evaluate(async status => {
|
||||||
return await window.Store.Wap.sendSetStatus(status);
|
return await window.Store.StatusUtils.setMyStatus(status);
|
||||||
}, status);
|
}, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,11 +761,22 @@ class Client extends EventEmitter {
|
|||||||
* Sets the current user's display name.
|
* 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.
|
* 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
|
* @param {string} displayName New display name
|
||||||
|
* @returns {Promise<Boolean>}
|
||||||
*/
|
*/
|
||||||
async setDisplayName(displayName) {
|
async setDisplayName(displayName) {
|
||||||
await this.pupPage.evaluate(async displayName => {
|
const couldSet = await this.pupPage.evaluate(async displayName => {
|
||||||
return await window.Store.Wap.setPushname(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);
|
}, displayName);
|
||||||
|
|
||||||
|
return couldSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -740,7 +795,16 @@ class Client extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
async sendPresenceAvailable() {
|
async sendPresenceAvailable() {
|
||||||
return await this.pupPage.evaluate(() => {
|
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) {
|
async getProfilePicUrl(contactId) {
|
||||||
const profilePic = await this.pupPage.evaluate((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);
|
}, contactId);
|
||||||
|
|
||||||
return profilePic ? profilePic.eurl : undefined;
|
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
|
* Force reset of connection state for the client
|
||||||
*/
|
*/
|
||||||
@@ -868,10 +957,7 @@ class Client extends EventEmitter {
|
|||||||
* @returns {Promise<Boolean>}
|
* @returns {Promise<Boolean>}
|
||||||
*/
|
*/
|
||||||
async isRegisteredUser(id) {
|
async isRegisteredUser(id) {
|
||||||
return await this.pupPage.evaluate(async (id) => {
|
return Boolean(await this.getNumberId(id));
|
||||||
let result = await window.Store.Wap.queryExist(id);
|
|
||||||
return result.jid !== undefined;
|
|
||||||
}, id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -881,14 +967,15 @@ class Client extends EventEmitter {
|
|||||||
* @returns {Promise<Object|null>}
|
* @returns {Promise<Object|null>}
|
||||||
*/
|
*/
|
||||||
async getNumberId(number) {
|
async getNumberId(number) {
|
||||||
if (!number.endsWith('@c.us')) number += '@c.us';
|
if (!number.endsWith('@c.us')) {
|
||||||
try {
|
number += '@c.us';
|
||||||
return await this.pupPage.evaluate(async numberId => {
|
|
||||||
return window.WWebJS.getNumberId(numberId);
|
|
||||||
}, number);
|
|
||||||
} catch(_) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -936,12 +1023,9 @@ class Client extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createRes = await this.pupPage.evaluate(async (name, participantIds) => {
|
const createRes = await this.pupPage.evaluate(async (name, participantIds) => {
|
||||||
const res = await window.Store.Wap.createGroup(name, participantIds);
|
const participantWIDs = participantIds.map(p => window.Store.WidFactory.createWid(p));
|
||||||
console.log(res);
|
const id = window.Store.genId();
|
||||||
if (!res.status === 200) {
|
const res = await window.Store.GroupUtils.sendCreateGroup(name, participantWIDs, undefined, id);
|
||||||
throw 'An error occurred while creating the group!';
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}, name, participants);
|
}, name, participants);
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,6 @@ class ClientInfo extends Base {
|
|||||||
*/
|
*/
|
||||||
this.pushname = data.pushname;
|
this.pushname = data.pushname;
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {object}
|
|
||||||
* @deprecated Use .wid instead
|
|
||||||
*/
|
|
||||||
this.me = data.wid;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current user ID
|
* Current user ID
|
||||||
* @type {object}
|
* @type {object}
|
||||||
@@ -33,18 +27,19 @@ class ClientInfo extends Base {
|
|||||||
this.wid = data.wid;
|
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}
|
* @type {object}
|
||||||
* @property {string} wa_version WhatsApp Version running on the phone
|
* @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} os_version OS Version running on the phone (iOS or Android version)
|
||||||
* @property {string} device_manufacturer Device manufacturer
|
* @property {string} device_manufacturer Device manufacturer
|
||||||
* @property {string} device_model Device model
|
* @property {string} device_model Device model
|
||||||
* @property {string} os_build_number OS build number
|
* @property {string} os_build_number OS build number
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
this.phone = data.phone;
|
this.phone = data.phone;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform the phone is running on
|
* Platform WhatsApp is running on
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
this.platform = data.platform;
|
this.platform = data.platform;
|
||||||
@@ -57,6 +52,7 @@ class ClientInfo extends Base {
|
|||||||
* @returns {object} batteryStatus
|
* @returns {object} batteryStatus
|
||||||
* @returns {number} batteryStatus.battery - The current battery percentage
|
* @returns {number} batteryStatus.battery - The current battery percentage
|
||||||
* @returns {boolean} batteryStatus.plugged - Indicates if the phone is plugged in (true) or not (false)
|
* @returns {boolean} batteryStatus.plugged - Indicates if the phone is plugged in (true) or not (false)
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
async getBatteryStatus() {
|
async getBatteryStatus() {
|
||||||
return await this.client.pupPage.evaluate(() => {
|
return await this.client.pupPage.evaluate(() => {
|
||||||
@@ -64,7 +60,6 @@ class ClientInfo extends Base {
|
|||||||
return { battery, plugged };
|
return { battery, plugged };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ClientInfo;
|
module.exports = ClientInfo;
|
||||||
@@ -183,7 +183,8 @@ class Contact extends Base {
|
|||||||
*/
|
*/
|
||||||
async getAbout() {
|
async getAbout() {
|
||||||
const about = await this.client.pupPage.evaluate(async (contactId) => {
|
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);
|
}, this.id._serialized);
|
||||||
|
|
||||||
if (typeof about.status !== 'string')
|
if (typeof about.status !== 'string')
|
||||||
@@ -192,6 +193,14 @@ class Contact extends Base {
|
|||||||
return about.status;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Contact;
|
module.exports = Contact;
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ class GroupChat extends Chat {
|
|||||||
*/
|
*/
|
||||||
async addParticipants(participantIds) {
|
async addParticipants(participantIds) {
|
||||||
return await this.client.pupPage.evaluate((chatId, 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);
|
}, this.id._serialized, participantIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +73,9 @@ class GroupChat extends Chat {
|
|||||||
*/
|
*/
|
||||||
async removeParticipants(participantIds) {
|
async removeParticipants(participantIds) {
|
||||||
return await this.client.pupPage.evaluate((chatId, 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);
|
}, this.id._serialized, participantIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +86,9 @@ class GroupChat extends Chat {
|
|||||||
*/
|
*/
|
||||||
async promoteParticipants(participantIds) {
|
async promoteParticipants(participantIds) {
|
||||||
return await this.client.pupPage.evaluate((chatId, 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);
|
}, this.id._serialized, participantIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,39 +99,53 @@ class GroupChat extends Chat {
|
|||||||
*/
|
*/
|
||||||
async demoteParticipants(participantIds) {
|
async demoteParticipants(participantIds) {
|
||||||
return await this.client.pupPage.evaluate((chatId, 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);
|
}, this.id._serialized, participantIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the group subject
|
* Updates the group subject
|
||||||
* @param {string} 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) {
|
async setSubject(subject) {
|
||||||
let res = await this.client.pupPage.evaluate((chatId, subject) => {
|
const success = await this.client.pupPage.evaluate(async (chatId, subject) => {
|
||||||
return window.Store.Wap.changeSubject(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);
|
}, this.id._serialized, subject);
|
||||||
|
|
||||||
if(res.status == 200) {
|
if(!success) return false;
|
||||||
this.name = subject;
|
this.name = subject;
|
||||||
}
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the group description
|
* Updates the group description
|
||||||
* @param {string} 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) {
|
async setDescription(description) {
|
||||||
let res = await this.client.pupPage.evaluate((chatId, description) => {
|
const success = await this.client.pupPage.evaluate(async (chatId, description) => {
|
||||||
let descId = window.Store.GroupMetadata.get(chatId).descId;
|
const chatWid = window.Store.WidFactory.createWid(chatId);
|
||||||
return window.Store.Wap.setGroupDescription(chatId, description, window.Store.genId(), descId);
|
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);
|
}, this.id._serialized, description);
|
||||||
|
|
||||||
if (res.status == 200) {
|
if(!success) return false;
|
||||||
this.groupMetadata.desc = description;
|
this.groupMetadata.desc = description;
|
||||||
}
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,11 +154,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.
|
* @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) {
|
async setMessagesAdminsOnly(adminsOnly=true) {
|
||||||
let res = await this.client.pupPage.evaluate((chatId, value) => {
|
const success = await this.client.pupPage.evaluate(async (chatId, adminsOnly) => {
|
||||||
return window.Store.Wap.setGroupProperty(chatId, 'announcement', value);
|
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);
|
}, this.id._serialized, adminsOnly);
|
||||||
|
|
||||||
if (res.status !== 200) return false;
|
if(!success) return false;
|
||||||
|
|
||||||
this.groupMetadata.announce = adminsOnly;
|
this.groupMetadata.announce = adminsOnly;
|
||||||
return true;
|
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.
|
* @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) {
|
async setInfoAdminsOnly(adminsOnly=true) {
|
||||||
let res = await this.client.pupPage.evaluate((chatId, value) => {
|
const success = await this.client.pupPage.evaluate(async (chatId, adminsOnly) => {
|
||||||
return window.Store.Wap.setGroupProperty(chatId, 'restrict', value);
|
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);
|
}, this.id._serialized, adminsOnly);
|
||||||
|
|
||||||
if (res.status !== 200) return false;
|
if(!success) return false;
|
||||||
|
|
||||||
this.groupMetadata.restrict = adminsOnly;
|
this.groupMetadata.restrict = adminsOnly;
|
||||||
return true;
|
return true;
|
||||||
@@ -165,25 +197,25 @@ class GroupChat extends Chat {
|
|||||||
* @returns {Promise<string>} Group's invite code
|
* @returns {Promise<string>} Group's invite code
|
||||||
*/
|
*/
|
||||||
async getInviteCode() {
|
async getInviteCode() {
|
||||||
let res = await this.client.pupPage.evaluate(chatId => {
|
const code = await this.client.pupPage.evaluate(async chatId => {
|
||||||
return window.Store.Wap.groupInviteCode(chatId);
|
const chatWid = window.Store.WidFactory.createWid(chatId);
|
||||||
|
return window.Store.Invite.sendQueryGroupInviteCode(chatWid);
|
||||||
}, this.id._serialized);
|
}, this.id._serialized);
|
||||||
|
|
||||||
if (res.status == 200) {
|
return code;
|
||||||
return res.code;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Not authorized');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidates the current group invite code and generates a new one
|
* Invalidates the current group invite code and generates a new one
|
||||||
* @returns {Promise}
|
* @returns {Promise<string>} New invite code
|
||||||
*/
|
*/
|
||||||
async revokeInvite() {
|
async revokeInvite() {
|
||||||
return await this.client.pupPage.evaluate(chatId => {
|
const code = await this.client.pupPage.evaluate(chatId => {
|
||||||
return window.Store.Wap.revokeGroupInvite(chatId);
|
const chatWid = window.Store.WidFactory.createWid(chatId);
|
||||||
|
return window.Store.Invite.sendRevokeGroupInviteCode(chatWid);
|
||||||
}, this.id._serialized);
|
}, this.id._serialized);
|
||||||
|
|
||||||
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -191,8 +223,9 @@ class GroupChat extends Chat {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
async leave() {
|
async leave() {
|
||||||
return await this.client.pupPage.evaluate(chatId => {
|
await this.client.pupPage.evaluate(chatId => {
|
||||||
return window.Store.Wap.leaveGroup(chatId);
|
const chatWid = window.Store.WidFactory.createWid(chatId);
|
||||||
|
return window.Store.GroupUtils.sendExitGroup(chatWid);
|
||||||
}, this.id._serialized);
|
}, this.id._serialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,14 @@ class Message extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_patch(data) {
|
_patch(data) {
|
||||||
|
this._data = data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MediaKey that represents the sticker 'ID'
|
* MediaKey that represents the sticker 'ID'
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
this.mediaKey = data.mediaKey;
|
this.mediaKey = data.mediaKey;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ID that represents the message
|
* ID that represents the message
|
||||||
* @type {object}
|
* @type {object}
|
||||||
@@ -240,6 +241,32 @@ class Message extends Base {
|
|||||||
return this.fromMe ? this.to : this.from;
|
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 the Chat this message was sent in
|
||||||
* @returns {Promise<Chat>}
|
* @returns {Promise<Chat>}
|
||||||
@@ -442,13 +469,9 @@ class Message extends Base {
|
|||||||
const msg = window.Store.Msg.get(msgId);
|
const msg = window.Store.Msg.get(msgId);
|
||||||
if (!msg) return null;
|
if (!msg) return null;
|
||||||
|
|
||||||
return await window.Store.Wap.queryMsgInfo(msg.id);
|
return await window.Store.MessageInfo.sendQueryMsgInfo(msg.id);
|
||||||
}, this.id._serialized);
|
}, this.id._serialized);
|
||||||
|
|
||||||
if(info.status) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ exports.DefaultOptions = {
|
|||||||
headless: true,
|
headless: true,
|
||||||
defaultViewport: null
|
defaultViewport: null
|
||||||
},
|
},
|
||||||
session: false,
|
dataPath: './WWebJS/',
|
||||||
qrTimeoutMs: 45000,
|
useDeprecatedSessionAuth: false,
|
||||||
qrRefreshIntervalMs: 20000,
|
authTimeoutMs: 0,
|
||||||
authTimeoutMs: 45000,
|
|
||||||
qrMaxRetries: 0,
|
qrMaxRetries: 0,
|
||||||
takeoverOnConflict: false,
|
takeoverOnConflict: false,
|
||||||
takeoverTimeoutMs: 0,
|
takeoverTimeoutMs: 0,
|
||||||
|
|||||||
@@ -8,34 +8,57 @@ exports.ExposeStore = (moduleRaidStr) => {
|
|||||||
window.Store = Object.assign({}, window.mR.findModule(m => m.default && m.default.Chat)[0].default);
|
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.AppState = window.mR.findModule('Socket')[0].Socket;
|
||||||
window.Store.Conn = window.mR.findModule('Conn')[0].Conn;
|
window.Store.Conn = window.mR.findModule('Conn')[0].Conn;
|
||||||
window.Store.Wap = window.mR.findModule('queryLinkPreview')[0].default;
|
window.Store.BlockContact = window.mR.findModule('blockContact')[0];
|
||||||
window.Store.SendSeen = window.mR.findModule('sendSeen')[0];
|
window.Store.Call = window.mR.findModule('CallCollection')[0].CallCollection;
|
||||||
window.Store.SendClear = window.mR.findModule('sendClear')[0];
|
window.Store.Cmd = window.mR.findModule('Cmd')[0].Cmd;
|
||||||
window.Store.SendDelete = window.mR.findModule('sendDelete')[0];
|
window.Store.CryptoLib = window.mR.findModule('decryptE2EMedia')[0];
|
||||||
window.Store.genId = window.mR.findModule('randomId')[0].randomId;
|
window.Store.DownloadManager = window.mR.findModule('downloadManager')[0].downloadManager;
|
||||||
window.Store.SendMessage = window.mR.findModule('addAndSendMsgToChat')[0];
|
window.Store.Features = window.mR.findModule('FEATURE_CHANGE_EVENT')[0].GK;
|
||||||
window.Store.MsgKey = window.mR.findModule((module) => module.default && module.default.fromString)[0].default;
|
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.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.MediaPrep = window.mR.findModule('MediaPrep')[0];
|
||||||
window.Store.MediaObject = window.mR.findModule('getOrCreateMediaObject')[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.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.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.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.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.WidFactory = window.mR.findModule('createWid')[0];
|
||||||
window.Store.BlockContact = window.mR.findModule('blockContact')[0];
|
window.Store.getProfilePicFull = window.mR.findModule('getProfilePicFull')[0].getProfilePicFull;
|
||||||
window.Store.GroupMetadata = window.mR.findModule((module) => module.default && module.default.handlePendingInvite)[0].default;
|
window.Store.PresenceUtils = window.mR.findModule('sendPresenceAvailable')[0];
|
||||||
window.Store.UploadUtils = window.mR.findModule((module) => (module.default && module.default.encryptAndUpload) ? module.default : null)[0].default;
|
window.Store.ChatState = window.mR.findModule('sendChatStateComposing')[0];
|
||||||
window.Store.Label = window.mR.findModule('LabelCollection')[0].LabelCollection;
|
window.Store.GroupParticipants = window.mR.findModule('sendPromoteParticipants')[0];
|
||||||
window.Store.Features = window.mR.findModule('FEATURE_CHANGE_EVENT')[0].GK;
|
window.Store.JoinInviteV4 = window.mR.findModule('sendJoinGroupViaInviteV4')[0];
|
||||||
window.Store.QueryOrder = window.mR.findModule('queryOrder')[0];
|
window.Store.findCommonGroups = window.mR.findModule('findCommonGroups')[0].findCommonGroups;
|
||||||
window.Store.QueryProduct = window.mR.findModule('queryProduct')[0];
|
window.Store.StatusUtils = window.mR.findModule('setMyStatus')[0];
|
||||||
window.Store.DownloadManager = window.mR.findModule('downloadManager')[0].downloadManager;
|
window.Store.StickerTools = {
|
||||||
window.Store.Call = window.mR.findModule('CallCollection')[0].CallCollection;
|
...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 => {
|
window.Store.Chat._find = e => {
|
||||||
@@ -50,14 +73,6 @@ exports.ExposeStore = (moduleRaidStr) => {
|
|||||||
exports.LoadUtils = () => {
|
exports.LoadUtils = () => {
|
||||||
window.WWebJS = {};
|
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) => {
|
window.WWebJS.sendSeen = async (chatId) => {
|
||||||
let chat = window.Store.Chat.get(chatId);
|
let chat = window.Store.Chat.get(chatId);
|
||||||
if (chat !== undefined) {
|
if (chat !== undefined) {
|
||||||
@@ -144,6 +159,9 @@ exports.LoadUtils = () => {
|
|||||||
|
|
||||||
if (options.linkPreview) {
|
if (options.linkPreview) {
|
||||||
delete options.linkPreview;
|
delete options.linkPreview;
|
||||||
|
|
||||||
|
// Not supported yet by WhatsApp Web on MD
|
||||||
|
if(!window.Store.Features.features.MD_BACKEND) {
|
||||||
const link = window.Store.Validators.findLink(content);
|
const link = window.Store.Validators.findLink(content);
|
||||||
if (link) {
|
if (link) {
|
||||||
const preview = await window.Store.Wap.queryLinkPreview(link.url);
|
const preview = await window.Store.Wap.queryLinkPreview(link.url);
|
||||||
@@ -152,6 +170,7 @@ exports.LoadUtils = () => {
|
|||||||
options = { ...options, ...preview };
|
options = { ...options, ...preview };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let buttonOptions = {};
|
let buttonOptions = {};
|
||||||
if(options.buttons){
|
if(options.buttons){
|
||||||
@@ -193,10 +212,15 @@ exports.LoadUtils = () => {
|
|||||||
delete listOptions.list.footer;
|
delete listOptions.list.footer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const meUser = window.Store.User.getMaybeMeUser();
|
||||||
|
const isMD = window.Store.Features.features.MD_BACKEND;
|
||||||
|
|
||||||
const newMsgId = new window.Store.MsgKey({
|
const newMsgId = new window.Store.MsgKey({
|
||||||
fromMe: true,
|
from: meUser,
|
||||||
remote: chat.id,
|
to: chat.id,
|
||||||
id: window.Store.genId(),
|
id: window.Store.genId(),
|
||||||
|
participant: isMD && chat.id.isGroup() ? meUser : undefined,
|
||||||
|
selfDir: 'out',
|
||||||
});
|
});
|
||||||
|
|
||||||
const extraOptions = options.extraOptions || {};
|
const extraOptions = options.extraOptions || {};
|
||||||
@@ -213,7 +237,7 @@ exports.LoadUtils = () => {
|
|||||||
id: newMsgId,
|
id: newMsgId,
|
||||||
ack: 0,
|
ack: 0,
|
||||||
body: content,
|
body: content,
|
||||||
from: window.Store.Conn.wid,
|
from: meUser,
|
||||||
to: chat.id,
|
to: chat.id,
|
||||||
local: true,
|
local: true,
|
||||||
self: 'out',
|
self: 'out',
|
||||||
@@ -234,6 +258,20 @@ exports.LoadUtils = () => {
|
|||||||
return window.Store.Msg.get(newMsgId._serialized);
|
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) => {
|
window.WWebJS.processStickerData = async (mediaInfo) => {
|
||||||
if (mediaInfo.mimetype !== 'image/webp') throw new Error('Invalid media type');
|
if (mediaInfo.mimetype !== 'image/webp') throw new Error('Invalid media type');
|
||||||
|
|
||||||
@@ -353,13 +391,15 @@ exports.LoadUtils = () => {
|
|||||||
|
|
||||||
|
|
||||||
window.WWebJS.getChatModel = async chat => {
|
window.WWebJS.getChatModel = async chat => {
|
||||||
|
|
||||||
let res = chat.serialize();
|
let res = chat.serialize();
|
||||||
res.isGroup = chat.isGroup;
|
res.isGroup = chat.isGroup;
|
||||||
res.formattedTitle = chat.formattedTitle;
|
res.formattedTitle = chat.formattedTitle;
|
||||||
res.isMuted = chat.mute && chat.mute.isMuted;
|
res.isMuted = chat.mute && chat.mute.isMuted;
|
||||||
|
|
||||||
if (chat.groupMetadata) {
|
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();
|
res.groupMetadata = chat.groupMetadata.serialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +454,7 @@ exports.LoadUtils = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.WWebJS.mediaInfoToFile = ({ data, mimetype, filename }) => {
|
window.WWebJS.mediaInfoToFile = ({ data, mimetype, filename }) => {
|
||||||
const binaryData = atob(data);
|
const binaryData = window.atob(data);
|
||||||
|
|
||||||
const buffer = new ArrayBuffer(binaryData.length);
|
const buffer = new ArrayBuffer(binaryData.length);
|
||||||
const view = new Uint8Array(buffer);
|
const view = new Uint8Array(buffer);
|
||||||
@@ -474,15 +514,18 @@ exports.LoadUtils = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.WWebJS.sendChatstate = async (state, chatId) => {
|
window.WWebJS.sendChatstate = async (state, chatId) => {
|
||||||
|
if (window.Store.Features.features.MD_BACKEND) {
|
||||||
|
chatId = window.Store.WidFactory.createWid(chatId);
|
||||||
|
}
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case 'typing':
|
case 'typing':
|
||||||
await window.Store.Wap.sendChatstateComposing(chatId);
|
await window.Store.ChatState.sendChatStateComposing(chatId);
|
||||||
break;
|
break;
|
||||||
case 'recording':
|
case 'recording':
|
||||||
await window.Store.Wap.sendChatstateRecording(chatId);
|
await window.Store.ChatState.sendChatStateRecording(chatId);
|
||||||
break;
|
break;
|
||||||
case 'stop':
|
case 'stop':
|
||||||
await window.Store.Wap.sendChatstatePaused(chatId);
|
await window.Store.ChatState.sendChatStatePaused(chatId);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw 'Invalid chatstate';
|
throw 'Invalid chatstate';
|
||||||
@@ -527,20 +570,3 @@ exports.LoadUtils = () => {
|
|||||||
return undefined;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const sharp = require('sharp');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const Crypto = require('crypto');
|
const Crypto = require('crypto');
|
||||||
const { tmpdir } = require('os');
|
const { tmpdir } = require('os');
|
||||||
@@ -14,7 +13,6 @@ const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|||||||
* Utility methods
|
* Utility methods
|
||||||
*/
|
*/
|
||||||
class Util {
|
class Util {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
throw new Error(`The ${this.constructor.name} class may not be instantiated.`);
|
throw new Error(`The ${this.constructor.name} class may not be instantiated.`);
|
||||||
}
|
}
|
||||||
@@ -55,7 +53,7 @@ class Util {
|
|||||||
*
|
*
|
||||||
* @returns {Promise<MessageMedia>} media in webp format
|
* @returns {Promise<MessageMedia>} media in webp format
|
||||||
*/
|
*/
|
||||||
static async formatImageToWebpSticker(media) {
|
static async formatImageToWebpSticker(media, pupPage) {
|
||||||
if (!media.mimetype.includes('image'))
|
if (!media.mimetype.includes('image'))
|
||||||
throw new Error('media is not a image');
|
throw new Error('media is not a image');
|
||||||
|
|
||||||
@@ -63,23 +61,9 @@ class Util {
|
|||||||
return media;
|
return media;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buff = Buffer.from(media.data, 'base64');
|
return pupPage.evaluate((media) => {
|
||||||
|
return window.WWebJS.toStickerData(media);
|
||||||
let sharpImg = sharp(buff);
|
}, media);
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,11 +145,11 @@ class Util {
|
|||||||
*
|
*
|
||||||
* @returns {Promise<MessageMedia>} media in webp format
|
* @returns {Promise<MessageMedia>} media in webp format
|
||||||
*/
|
*/
|
||||||
static async formatToWebpSticker(media, metadata) {
|
static async formatToWebpSticker(media, metadata, pupPage) {
|
||||||
let webpMedia;
|
let webpMedia;
|
||||||
|
|
||||||
if (media.mimetype.includes('image'))
|
if (media.mimetype.includes('image'))
|
||||||
webpMedia = await this.formatImageToWebpSticker(media);
|
webpMedia = await this.formatImageToWebpSticker(media, pupPage);
|
||||||
else if (media.mimetype.includes('video'))
|
else if (media.mimetype.includes('video'))
|
||||||
webpMedia = await this.formatVideoToWebpSticker(media);
|
webpMedia = await this.formatVideoToWebpSticker(media);
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
These tests require an authenticated WhatsApp Web session, as well as an additional phone that you can send messages to.
|
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:
|
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`: 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 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.
|
- `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`.
|
||||||
177
tests/client.js
177
tests/client.js
@@ -1,4 +1,5 @@
|
|||||||
const {expect} = require('chai');
|
const chai = require('chai');
|
||||||
|
const chaiAsPromised = require('chai-as-promised');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
const helper = require('./helper');
|
const helper = require('./helper');
|
||||||
@@ -9,7 +10,11 @@ const MessageMedia = require('../src/structures/MessageMedia');
|
|||||||
const Location = require('../src/structures/Location');
|
const Location = require('../src/structures/Location');
|
||||||
const { MessageTypes, WAState } = require('../src/util/Constants');
|
const { MessageTypes, WAState } = require('../src/util/Constants');
|
||||||
|
|
||||||
|
const expect = chai.expect;
|
||||||
|
chai.use(chaiAsPromised);
|
||||||
|
|
||||||
const remoteId = helper.remoteId;
|
const remoteId = helper.remoteId;
|
||||||
|
const isMD = helper.isMD();
|
||||||
|
|
||||||
describe('Client', function() {
|
describe('Client', function() {
|
||||||
describe('Authentication', function() {
|
describe('Authentication', function() {
|
||||||
@@ -47,6 +52,82 @@ describe('Client', function() {
|
|||||||
expect(disconnectedCallback.calledOnceWith('Max qrcode retries reached')).to.eql(true);
|
expect(disconnectedCallback.calledOnceWith('Max qrcode retries reached')).to.eql(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should authenticate with existing session', async function() {
|
||||||
|
this.timeout(40000);
|
||||||
|
|
||||||
|
const authenticatedCallback = sinon.spy();
|
||||||
|
const qrCallback = sinon.spy();
|
||||||
|
const readyCallback = sinon.spy();
|
||||||
|
|
||||||
|
const client = helper.createClient({
|
||||||
|
authenticated: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('qr', qrCallback);
|
||||||
|
client.on('authenticated', authenticatedCallback);
|
||||||
|
client.on('ready', readyCallback);
|
||||||
|
|
||||||
|
await client.initialize();
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
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() {
|
it('should fail auth if session is invalid', async function() {
|
||||||
this.timeout(40000);
|
this.timeout(40000);
|
||||||
|
|
||||||
@@ -63,7 +144,8 @@ describe('Client', function() {
|
|||||||
WAToken2: 'invalid'
|
WAToken2: 'invalid'
|
||||||
},
|
},
|
||||||
authTimeoutMs: 10000,
|
authTimeoutMs: 10000,
|
||||||
restartOnAuthFail: false
|
restartOnAuthFail: false,
|
||||||
|
useDeprecatedSessionAuth: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,7 +181,8 @@ describe('Client', function() {
|
|||||||
WAToken2: 'invalid'
|
WAToken2: 'invalid'
|
||||||
},
|
},
|
||||||
authTimeoutMs: 10000,
|
authTimeoutMs: 10000,
|
||||||
restartOnAuthFail: true
|
restartOnAuthFail: true,
|
||||||
|
useDeprecatedSessionAuth: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,74 +199,7 @@ describe('Client', function() {
|
|||||||
|
|
||||||
await client.destroy();
|
await client.destroy();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
it('should authenticate with existing session', async function() {
|
|
||||||
this.timeout(40000);
|
|
||||||
|
|
||||||
const authenticatedCallback = sinon.spy();
|
|
||||||
const qrCallback = sinon.spy();
|
|
||||||
const readyCallback = sinon.spy();
|
|
||||||
|
|
||||||
const client = helper.createClient({withSession: true});
|
|
||||||
client.on('qr', qrCallback);
|
|
||||||
client.on('authenticated', authenticatedCallback);
|
|
||||||
client.on('ready', readyCallback);
|
|
||||||
|
|
||||||
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);
|
|
||||||
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();
|
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,7 +208,7 @@ describe('Client', function() {
|
|||||||
|
|
||||||
before(async function() {
|
before(async function() {
|
||||||
this.timeout(35000);
|
this.timeout(35000);
|
||||||
client = helper.createClient({withSession: true});
|
client = helper.createClient({authenticated: true});
|
||||||
await client.initialize();
|
await client.initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -221,27 +237,38 @@ describe('Client', function() {
|
|||||||
'BlockContact',
|
'BlockContact',
|
||||||
'Call',
|
'Call',
|
||||||
'Chat',
|
'Chat',
|
||||||
|
'ChatState',
|
||||||
'Cmd',
|
'Cmd',
|
||||||
'Conn',
|
'Conn',
|
||||||
'Contact',
|
'Contact',
|
||||||
'DownloadManager',
|
'DownloadManager',
|
||||||
'Features',
|
'Features',
|
||||||
'GroupMetadata',
|
'GroupMetadata',
|
||||||
|
'GroupParticipants',
|
||||||
|
'GroupUtils',
|
||||||
'Invite',
|
'Invite',
|
||||||
|
'InviteInfo',
|
||||||
|
'JoinInviteV4',
|
||||||
'Label',
|
'Label',
|
||||||
'MediaObject',
|
'MediaObject',
|
||||||
'MediaPrep',
|
'MediaPrep',
|
||||||
'MediaTypes',
|
'MediaTypes',
|
||||||
'MediaUpload',
|
'MediaUpload',
|
||||||
|
'MessageInfo',
|
||||||
'Msg',
|
'Msg',
|
||||||
'MsgKey',
|
'MsgKey',
|
||||||
'OpaqueData',
|
'OpaqueData',
|
||||||
'QueryOrder',
|
'QueryOrder',
|
||||||
'QueryProduct',
|
'QueryProduct',
|
||||||
|
'PresenceUtils',
|
||||||
|
'QueryExist',
|
||||||
|
'QueryProduct',
|
||||||
|
'QueryOrder',
|
||||||
'SendClear',
|
'SendClear',
|
||||||
'SendDelete',
|
'SendDelete',
|
||||||
'SendMessage',
|
'SendMessage',
|
||||||
'SendSeen',
|
'SendSeen',
|
||||||
|
'StatusUtils',
|
||||||
'Sticker',
|
'Sticker',
|
||||||
'UploadUtils',
|
'UploadUtils',
|
||||||
'UserConstructor',
|
'UserConstructor',
|
||||||
@@ -249,7 +276,9 @@ describe('Client', function() {
|
|||||||
'Validators',
|
'Validators',
|
||||||
'Wap',
|
'Wap',
|
||||||
'WidFactory',
|
'WidFactory',
|
||||||
'genId'
|
'findCommonGroups',
|
||||||
|
'genId',
|
||||||
|
'getProfilePicFull',
|
||||||
];
|
];
|
||||||
|
|
||||||
const loadedModules = await client.pupPage.evaluate((expectedModules) => {
|
const loadedModules = await client.pupPage.evaluate((expectedModules) => {
|
||||||
@@ -535,8 +564,6 @@ END:VCARD`;
|
|||||||
|
|
||||||
describe('Search messages', function () {
|
describe('Search messages', function () {
|
||||||
it('can search for messages', async 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 m1 = await client.sendMessage(remoteId, 'I\'m searching for Super Mario Brothers');
|
||||||
const m2 = await client.sendMessage(remoteId, 'This also contains Mario');
|
const m2 = await client.sendMessage(remoteId, 'This also contains Mario');
|
||||||
const m3 = await client.sendMessage(remoteId, 'Nothing of interest here, just Luigi');
|
const m3 = await client.sendMessage(remoteId, 'Nothing of interest here, just Luigi');
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
const Client = require('../src/Client');
|
const Client = require('../src/Client');
|
||||||
const Util = require('../src/util/Util');
|
|
||||||
|
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const remoteId = process.env.WWEBJS_TEST_REMOTE_ID;
|
const remoteId = process.env.WWEBJS_TEST_REMOTE_ID;
|
||||||
if(!remoteId) throw new Error('The WWEBJS_TEST_REMOTE_ID environment variable has not been set.');
|
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() {
|
function getSessionFromEnv() {
|
||||||
|
if (!isUsingDeprecatedSession()) return null;
|
||||||
|
|
||||||
const envSession = process.env.WWEBJS_TEST_SESSION;
|
const envSession = process.env.WWEBJS_TEST_SESSION;
|
||||||
if(envSession) return JSON.parse(envSession);
|
if(envSession) return JSON.parse(envSession);
|
||||||
|
|
||||||
@@ -16,17 +28,27 @@ function getSessionFromEnv() {
|
|||||||
const absPath = path.resolve(process.cwd(), envSessionPath);
|
const absPath = path.resolve(process.cwd(), envSessionPath);
|
||||||
return require(absPath);
|
return require(absPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('No session found in environment.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createClient({withSession, options: additionalOpts}={}) {
|
function createClient({authenticated, options: additionalOpts}={}) {
|
||||||
const options = {};
|
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) {
|
function sleep(ms) {
|
||||||
@@ -36,5 +58,7 @@ function sleep(ms) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
sleep,
|
sleep,
|
||||||
createClient,
|
createClient,
|
||||||
remoteId
|
isUsingDeprecatedSession,
|
||||||
|
isMD,
|
||||||
|
remoteId,
|
||||||
};
|
};
|
||||||
@@ -13,7 +13,7 @@ describe('Chat', function () {
|
|||||||
|
|
||||||
before(async function() {
|
before(async function() {
|
||||||
this.timeout(35000);
|
this.timeout(35000);
|
||||||
client = helper.createClient({ withSession: true });
|
client = helper.createClient({ authenticated: true });
|
||||||
await client.initialize();
|
await client.initialize();
|
||||||
chat = await client.getChatById(remoteId);
|
chat = await client.getChatById(remoteId);
|
||||||
});
|
});
|
||||||
@@ -32,9 +32,9 @@ describe('Chat', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can fetch messages sent in a chat', async function () {
|
it('can fetch messages sent in a chat', async function () {
|
||||||
this.timeout(5000);
|
|
||||||
await helper.sleep(1000);
|
await helper.sleep(1000);
|
||||||
const msg = await chat.sendMessage('another message');
|
const msg = await chat.sendMessage('another message');
|
||||||
|
await helper.sleep(500);
|
||||||
|
|
||||||
const messages = await chat.fetchMessages();
|
const messages = await chat.fetchMessages();
|
||||||
expect(messages.length).to.be.greaterThanOrEqual(2);
|
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 () {
|
it('can use a limit when fetching messages sent in a chat', async function () {
|
||||||
await helper.sleep(1000);
|
await helper.sleep(1000);
|
||||||
const msg = await chat.sendMessage('yet another message');
|
const msg = await chat.sendMessage('yet another message');
|
||||||
|
await helper.sleep(500);
|
||||||
|
|
||||||
const messages = await chat.fetchMessages({limit: 1});
|
const messages = await chat.fetchMessages({limit: 1});
|
||||||
expect(messages).to.have.lengthOf(1);
|
expect(messages).to.have.lengthOf(1);
|
||||||
@@ -80,6 +81,8 @@ describe('Chat', function () {
|
|||||||
const res = await chat.sendSeen();
|
const res = await chat.sendSeen();
|
||||||
expect(res).to.equal(true);
|
expect(res).to.equal(true);
|
||||||
|
|
||||||
|
await helper.sleep(1000);
|
||||||
|
|
||||||
// refresh chat
|
// refresh chat
|
||||||
chat = await client.getChatById(remoteId);
|
chat = await client.getChatById(remoteId);
|
||||||
expect(chat.unreadCount).to.equal(0);
|
expect(chat.unreadCount).to.equal(0);
|
||||||
@@ -137,6 +140,8 @@ describe('Chat', function () {
|
|||||||
it('can mute a chat forever', async function() {
|
it('can mute a chat forever', async function() {
|
||||||
await chat.mute();
|
await chat.mute();
|
||||||
|
|
||||||
|
await helper.sleep(1000);
|
||||||
|
|
||||||
// refresh chat
|
// refresh chat
|
||||||
chat = await client.getChatById(remoteId);
|
chat = await client.getChatById(remoteId);
|
||||||
expect(chat.isMuted).to.equal(true);
|
expect(chat.isMuted).to.equal(true);
|
||||||
@@ -147,6 +152,8 @@ describe('Chat', function () {
|
|||||||
const unmuteDate = new Date(new Date().getTime() + (1000*60*60));
|
const unmuteDate = new Date(new Date().getTime() + (1000*60*60));
|
||||||
await chat.mute(unmuteDate);
|
await chat.mute(unmuteDate);
|
||||||
|
|
||||||
|
await helper.sleep(1000);
|
||||||
|
|
||||||
// refresh chat
|
// refresh chat
|
||||||
chat = await client.getChatById(remoteId);
|
chat = await client.getChatById(remoteId);
|
||||||
expect(chat.isMuted).to.equal(true);
|
expect(chat.isMuted).to.equal(true);
|
||||||
@@ -169,8 +176,6 @@ describe('Chat', function () {
|
|||||||
// eslint-disable-next-line mocha/no-skipped-tests
|
// eslint-disable-next-line mocha/no-skipped-tests
|
||||||
describe.skip('Destructive operations', function () {
|
describe.skip('Destructive operations', function () {
|
||||||
it('can clear all messages from chat', async function () {
|
it('can clear all messages from chat', async function () {
|
||||||
this.timeout(5000);
|
|
||||||
|
|
||||||
const res = await chat.clearMessages();
|
const res = await chat.clearMessages();
|
||||||
expect(res).to.equal(true);
|
expect(res).to.equal(true);
|
||||||
|
|
||||||
|
|||||||
227
tests/structures/group.js
Normal file
227
tests/structures/group.js
Normal 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
112
tests/structures/message.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user