mirror of
https://github.com/cheveguerra/whatsapp-web.js.git
synced 2026-04-20 20:49:14 +00:00
Merge branch 'main' into patch-participants
This commit is contained in:
@@ -480,7 +480,7 @@ class Message extends Base {
|
|||||||
let msg = window.Store.Msg.get(msgId);
|
let msg = window.Store.Msg.get(msgId);
|
||||||
|
|
||||||
if (msg.canStar()) {
|
if (msg.canStar()) {
|
||||||
return msg.chat.sendStarMsgs([msg], true);
|
return window.Store.Cmd.sendStarMsgs(msg.chat, [msg], false);
|
||||||
}
|
}
|
||||||
}, this.id._serialized);
|
}, this.id._serialized);
|
||||||
}
|
}
|
||||||
@@ -493,7 +493,7 @@ class Message extends Base {
|
|||||||
let msg = window.Store.Msg.get(msgId);
|
let msg = window.Store.Msg.get(msgId);
|
||||||
|
|
||||||
if (msg.canStar()) {
|
if (msg.canStar()) {
|
||||||
return msg.chat.sendStarMsgs([msg], false);
|
return window.Store.Cmd.sendUnstarMsgs(msg.chat, [msg], false);
|
||||||
}
|
}
|
||||||
}, this.id._serialized);
|
}, this.id._serialized);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ const client = new Client({
|
|||||||
|
|
||||||
client.initialize();
|
client.initialize();
|
||||||
|
|
||||||
|
client.on('loading_screen', (percent, message) => {
|
||||||
|
console.log('LOADING SCREEN', percent, message);
|
||||||
|
});
|
||||||
|
|
||||||
client.on('qr', (qr) => {
|
client.on('qr', (qr) => {
|
||||||
// NOTE: This event will not be fired if a session is specified.
|
// NOTE: This event will not be fired if a session is specified.
|
||||||
console.log('QR RECEIVED', qr);
|
console.log('QR RECEIVED', qr);
|
||||||
|
|||||||
61
index.d.ts
vendored
61
index.d.ts
vendored
@@ -241,6 +241,15 @@ declare namespace WAWebJS {
|
|||||||
message: Message
|
message: Message
|
||||||
) => void): this
|
) => void): this
|
||||||
|
|
||||||
|
/** Emitted when a reaction is sent, received, updated or removed */
|
||||||
|
on(event: 'message_reaction', listener: (
|
||||||
|
/** The reaction object */
|
||||||
|
reaction: Reaction
|
||||||
|
) => void): this
|
||||||
|
|
||||||
|
/** Emitted when loading screen is appearing */
|
||||||
|
on(event: 'loading_screen', listener: (percent: string, message: string) => void): this
|
||||||
|
|
||||||
/** Emitted when the QR code is received */
|
/** Emitted when the QR code is received */
|
||||||
on(event: 'qr', listener: (
|
on(event: 'qr', listener: (
|
||||||
/** qr code string
|
/** qr code string
|
||||||
@@ -256,6 +265,9 @@ declare namespace WAWebJS {
|
|||||||
|
|
||||||
/** Emitted when the client has initialized and is ready to receive messages */
|
/** Emitted when the client has initialized and is ready to receive messages */
|
||||||
on(event: 'ready', listener: () => void): this
|
on(event: 'ready', listener: () => void): this
|
||||||
|
|
||||||
|
/** Emitted when the RemoteAuth session is saved successfully on the external Database */
|
||||||
|
on(event: 'remote_session_saved', listener: () => void): this
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Current connection information */
|
/** Current connection information */
|
||||||
@@ -345,6 +357,9 @@ declare namespace WAWebJS {
|
|||||||
failureEventPayload?: any
|
failureEventPayload?: any
|
||||||
}>;
|
}>;
|
||||||
getAuthEventPayload: () => Promise<any>;
|
getAuthEventPayload: () => Promise<any>;
|
||||||
|
afterAuthReady: () => Promise<void>;
|
||||||
|
disconnect: () => Promise<void>;
|
||||||
|
destroy: () => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,6 +380,30 @@ declare namespace WAWebJS {
|
|||||||
dataPath?: string
|
dataPath?: string
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote-based authentication
|
||||||
|
*/
|
||||||
|
export class RemoteAuth extends AuthStrategy {
|
||||||
|
public clientId?: string;
|
||||||
|
public dataPath?: string;
|
||||||
|
constructor(options?: {
|
||||||
|
store: Store,
|
||||||
|
clientId?: string,
|
||||||
|
dataPath?: string,
|
||||||
|
backupSyncIntervalMs: number
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote store interface
|
||||||
|
*/
|
||||||
|
export interface Store {
|
||||||
|
sessionExists: ({session: string}) => Promise<boolean> | boolean,
|
||||||
|
delete: ({session: string}) => Promise<any> | any,
|
||||||
|
save: ({session: string}) => Promise<any> | any,
|
||||||
|
extract: ({session: string, path: string}) => Promise<any> | any,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy session auth strategy
|
* Legacy session auth strategy
|
||||||
@@ -463,9 +502,11 @@ declare namespace WAWebJS {
|
|||||||
GROUP_LEAVE = 'group_leave',
|
GROUP_LEAVE = 'group_leave',
|
||||||
GROUP_UPDATE = 'group_update',
|
GROUP_UPDATE = 'group_update',
|
||||||
QR_RECEIVED = 'qr',
|
QR_RECEIVED = 'qr',
|
||||||
|
LOADING_SCREEN = 'loading_screen',
|
||||||
DISCONNECTED = 'disconnected',
|
DISCONNECTED = 'disconnected',
|
||||||
STATE_CHANGED = 'change_state',
|
STATE_CHANGED = 'change_state',
|
||||||
BATTERY_CHANGED = 'change_battery',
|
BATTERY_CHANGED = 'change_battery',
|
||||||
|
REMOTE_SESSION_SAVED = 'remote_session_saved'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Group notification types */
|
/** Group notification types */
|
||||||
@@ -708,7 +749,7 @@ declare namespace WAWebJS {
|
|||||||
/** React to this message with an emoji*/
|
/** React to this message with an emoji*/
|
||||||
react: (reaction: string) => Promise<void>,
|
react: (reaction: string) => Promise<void>,
|
||||||
/**
|
/**
|
||||||
* Forwards this message to another chat
|
* Forwards this message to another chat (that you chatted before, otherwise it will fail)
|
||||||
*/
|
*/
|
||||||
forward: (chat: Chat | string) => Promise<void>,
|
forward: (chat: Chat | string) => Promise<void>,
|
||||||
/** Star this message */
|
/** Star this message */
|
||||||
@@ -805,13 +846,16 @@ declare namespace WAWebJS {
|
|||||||
data: string
|
data: string
|
||||||
/** Document file name. Value can be null */
|
/** Document file name. Value can be null */
|
||||||
filename?: string | null
|
filename?: string | null
|
||||||
|
/** Document file size in bytes. Value can be null. */
|
||||||
|
filesize?: number | null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} mimetype MIME type of the attachment
|
* @param {string} mimetype MIME type of the attachment
|
||||||
* @param {string} data Base64-encoded data of the file
|
* @param {string} data Base64-encoded data of the file
|
||||||
* @param {?string} filename Document file name. Value can be null
|
* @param {?string} filename Document file name. Value can be null
|
||||||
|
* @param {?number} filesize Document file size in bytes. Value can be null.
|
||||||
*/
|
*/
|
||||||
constructor(mimetype: string, data: string, filename?: string | null)
|
constructor(mimetype: string, data: string, filename?: string | null, filesize?: number | null)
|
||||||
|
|
||||||
/** Creates a MessageMedia instance from a local file path */
|
/** Creates a MessageMedia instance from a local file path */
|
||||||
static fromFilePath: (filePath: string) => MessageMedia
|
static fromFilePath: (filePath: string) => MessageMedia
|
||||||
@@ -1300,6 +1344,19 @@ declare namespace WAWebJS {
|
|||||||
|
|
||||||
constructor(body: string, buttons: Array<{ id?: string; body: string }>, title?: string | null, footer?: string | null)
|
constructor(body: string, buttons: Array<{ id?: string; body: string }>, title?: string | null, footer?: string | null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Message type Reaction */
|
||||||
|
export class Reaction {
|
||||||
|
id: MessageId
|
||||||
|
orphan: number
|
||||||
|
orphanReason?: string
|
||||||
|
timestamp: number
|
||||||
|
reaction: string
|
||||||
|
read: boolean
|
||||||
|
msgId: MessageId
|
||||||
|
senderId: string
|
||||||
|
ack?: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export = WAWebJS
|
export = WAWebJS
|
||||||
|
|||||||
1
index.js
1
index.js
@@ -25,6 +25,7 @@ module.exports = {
|
|||||||
// Auth Strategies
|
// Auth Strategies
|
||||||
NoAuth: require('./src/authStrategies/NoAuth'),
|
NoAuth: require('./src/authStrategies/NoAuth'),
|
||||||
LocalAuth: require('./src/authStrategies/LocalAuth'),
|
LocalAuth: require('./src/authStrategies/LocalAuth'),
|
||||||
|
RemoteAuth: require('./src/authStrategies/RemoteAuth'),
|
||||||
LegacySessionAuth: require('./src/authStrategies/LegacySessionAuth'),
|
LegacySessionAuth: require('./src/authStrategies/LegacySessionAuth'),
|
||||||
|
|
||||||
...Constants
|
...Constants
|
||||||
|
|||||||
@@ -51,5 +51,10 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"archiver": "^5.3.1",
|
||||||
|
"fs-extra": "^10.1.0",
|
||||||
|
"unzipper": "^0.10.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const { WhatsWebURL, DefaultOptions, Events, WAState } = require('./util/Constan
|
|||||||
const { ExposeStore, LoadUtils } = require('./util/Injected');
|
const { ExposeStore, LoadUtils } = require('./util/Injected');
|
||||||
const ChatFactory = require('./factories/ChatFactory');
|
const ChatFactory = require('./factories/ChatFactory');
|
||||||
const ContactFactory = require('./factories/ContactFactory');
|
const ContactFactory = require('./factories/ContactFactory');
|
||||||
const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List} = require('./structures');
|
const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List, Reaction } = require('./structures');
|
||||||
const LegacySessionAuth = require('./authStrategies/LegacySessionAuth');
|
const LegacySessionAuth = require('./authStrategies/LegacySessionAuth');
|
||||||
const NoAuth = require('./authStrategies/NoAuth');
|
const NoAuth = require('./authStrategies/NoAuth');
|
||||||
|
|
||||||
@@ -115,6 +115,52 @@ class Client extends EventEmitter {
|
|||||||
referer: 'https://whatsapp.com/'
|
referer: 'https://whatsapp.com/'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await page.evaluate(`function getElementByXpath(path) {
|
||||||
|
return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||||
|
}`);
|
||||||
|
|
||||||
|
let lastPercent = null,
|
||||||
|
lastPercentMessage = null;
|
||||||
|
|
||||||
|
await page.exposeFunction('loadingScreen', async (percent, message) => {
|
||||||
|
if (lastPercent !== percent || lastPercentMessage !== message) {
|
||||||
|
this.emit(Events.LOADING_SCREEN, percent, message);
|
||||||
|
lastPercent = percent;
|
||||||
|
lastPercentMessage = message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.evaluate(
|
||||||
|
async function (selectors) {
|
||||||
|
var observer = new MutationObserver(function () {
|
||||||
|
let progressBar = window.getElementByXpath(
|
||||||
|
selectors.PROGRESS
|
||||||
|
);
|
||||||
|
let progressMessage = window.getElementByXpath(
|
||||||
|
selectors.PROGRESS_MESSAGE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (progressBar) {
|
||||||
|
window.loadingScreen(
|
||||||
|
progressBar.value,
|
||||||
|
progressMessage.innerText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document, {
|
||||||
|
attributes: true,
|
||||||
|
childList: true,
|
||||||
|
characterData: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PROGRESS: '//*[@id=\'app\']/div/div/div[2]/progress',
|
||||||
|
PROGRESS_MESSAGE: '//*[@id=\'app\']/div/div/div[3]',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
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_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';
|
const INTRO_QRCODE_SELECTOR = 'div[data-ref] canvas';
|
||||||
|
|
||||||
@@ -373,7 +419,7 @@ class Client extends EventEmitter {
|
|||||||
this.emit(Events.MEDIA_UPLOADED, message);
|
this.emit(Events.MEDIA_UPLOADED, message);
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.exposeFunction('onAppStateChangedEvent', (state) => {
|
await page.exposeFunction('onAppStateChangedEvent', async (state) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the connection state changes
|
* Emitted when the connection state changes
|
||||||
@@ -400,6 +446,7 @@ class Client extends EventEmitter {
|
|||||||
* @event Client#disconnected
|
* @event Client#disconnected
|
||||||
* @param {WAState|"NAVIGATION"} reason reason that caused the disconnect
|
* @param {WAState|"NAVIGATION"} reason reason that caused the disconnect
|
||||||
*/
|
*/
|
||||||
|
await this.authStrategy.disconnect();
|
||||||
this.emit(Events.DISCONNECTED, state);
|
this.emit(Events.DISCONNECTED, state);
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}
|
}
|
||||||
@@ -439,6 +486,27 @@ class Client extends EventEmitter {
|
|||||||
this.emit(Events.INCOMING_CALL, cll);
|
this.emit(Events.INCOMING_CALL, cll);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await page.exposeFunction('onReaction', (reactions) => {
|
||||||
|
for (const reaction of reactions) {
|
||||||
|
/**
|
||||||
|
* Emitted when a reaction is sent, received, updated or removed
|
||||||
|
* @event Client#message_reaction
|
||||||
|
* @param {object} reaction
|
||||||
|
* @param {object} reaction.id - Reaction id
|
||||||
|
* @param {number} reaction.orphan - Orphan
|
||||||
|
* @param {?string} reaction.orphanReason - Orphan reason
|
||||||
|
* @param {number} reaction.timestamp - Timestamp
|
||||||
|
* @param {string} reaction.reaction - Reaction
|
||||||
|
* @param {boolean} reaction.read - Read
|
||||||
|
* @param {object} reaction.msgId - Parent message id
|
||||||
|
* @param {string} reaction.senderId - Sender id
|
||||||
|
* @param {?number} reaction.ack - Ack
|
||||||
|
*/
|
||||||
|
|
||||||
|
this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(window.WWebJS.getMessageModel(msg)); });
|
window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(window.WWebJS.getMessageModel(msg)); });
|
||||||
window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(window.WWebJS.getMessageModel(msg)); });
|
window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(window.WWebJS.getMessageModel(msg)); });
|
||||||
@@ -458,6 +526,22 @@ class Client extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
const module = window.Store.createOrUpdateReactionsModule;
|
||||||
|
const ogMethod = module.createOrUpdateReactions;
|
||||||
|
module.createOrUpdateReactions = ((...args) => {
|
||||||
|
window.onReaction(args[0].map(reaction => {
|
||||||
|
const msgKey = window.Store.MsgKey.fromString(reaction.msgKey);
|
||||||
|
const parentMsgKey = window.Store.MsgKey.fromString(reaction.parentMsgKey);
|
||||||
|
const timestamp = reaction.timestamp / 1000;
|
||||||
|
|
||||||
|
return {...reaction, msgKey, parentMsgKey, timestamp };
|
||||||
|
}));
|
||||||
|
|
||||||
|
return ogMethod(...args);
|
||||||
|
}).bind(module);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -465,11 +549,13 @@ class Client extends EventEmitter {
|
|||||||
* @event Client#ready
|
* @event Client#ready
|
||||||
*/
|
*/
|
||||||
this.emit(Events.READY);
|
this.emit(Events.READY);
|
||||||
|
this.authStrategy.afterAuthReady();
|
||||||
|
|
||||||
// Disconnect when navigating away when in PAIRING state (detect logout)
|
// Disconnect when navigating away when in PAIRING state (detect logout)
|
||||||
this.pupPage.on('framenavigated', async () => {
|
this.pupPage.on('framenavigated', async () => {
|
||||||
const appState = await this.getState();
|
const appState = await this.getState();
|
||||||
if(!appState || appState === WAState.PAIRING) {
|
if(!appState || appState === WAState.PAIRING) {
|
||||||
|
await this.authStrategy.disconnect();
|
||||||
this.emit(Events.DISCONNECTED, 'NAVIGATION');
|
this.emit(Events.DISCONNECTED, 'NAVIGATION');
|
||||||
await this.destroy();
|
await this.destroy();
|
||||||
}
|
}
|
||||||
@@ -481,6 +567,7 @@ class Client extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
async destroy() {
|
async destroy() {
|
||||||
await this.pupBrowser.close();
|
await this.pupBrowser.close();
|
||||||
|
await this.authStrategy.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ class BaseAuthStrategy {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
async getAuthEventPayload() {}
|
async getAuthEventPayload() {}
|
||||||
|
async afterAuthReady() {}
|
||||||
|
async disconnect() {}
|
||||||
|
async destroy() {}
|
||||||
async logout() {}
|
async logout() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
204
src/authStrategies/RemoteAuth.js
Normal file
204
src/authStrategies/RemoteAuth.js
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* Require Optional Dependencies */
|
||||||
|
try {
|
||||||
|
var fs = require('fs-extra');
|
||||||
|
var unzipper = require('unzipper');
|
||||||
|
var archiver = require('archiver');
|
||||||
|
} catch {
|
||||||
|
fs = undefined;
|
||||||
|
unzipper = undefined;
|
||||||
|
archiver = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const { Events } = require('./../util/Constants');
|
||||||
|
const BaseAuthStrategy = require('./BaseAuthStrategy');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote-based authentication
|
||||||
|
* @param {object} options - options
|
||||||
|
* @param {object} options.store - Remote database store instance
|
||||||
|
* @param {string} options.clientId - Client id to distinguish instances if you are using multiple, otherwise keep null if you are using only one instance
|
||||||
|
* @param {string} options.dataPath - Change the default path for saving session files, default is: "./.wwebjs_auth/"
|
||||||
|
* @param {number} options.backupSyncIntervalMs - Sets the time interval for periodic session backups. Accepts values starting from 60000ms {1 minute}
|
||||||
|
*/
|
||||||
|
class RemoteAuth extends BaseAuthStrategy {
|
||||||
|
constructor({ clientId, dataPath, store, backupSyncIntervalMs } = {}) {
|
||||||
|
if (!fs && !unzipper && !archiver) throw new Error('Optional Dependencies [fs-extra, unzipper, archiver] are required to use RemoteAuth. Make sure to run npm install correctly and remove the --no-optional flag');
|
||||||
|
super();
|
||||||
|
|
||||||
|
const idRegex = /^[-_\w]+$/i;
|
||||||
|
if (clientId && !idRegex.test(clientId)) {
|
||||||
|
throw new Error('Invalid clientId. Only alphanumeric characters, underscores and hyphens are allowed.');
|
||||||
|
}
|
||||||
|
if (!backupSyncIntervalMs || backupSyncIntervalMs < 60000) {
|
||||||
|
throw new Error('Invalid backupSyncIntervalMs. Accepts values starting from 60000ms {1 minute}.');
|
||||||
|
}
|
||||||
|
if(!store) throw new Error('Remote database store is required.');
|
||||||
|
|
||||||
|
this.store = store;
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.backupSyncIntervalMs = backupSyncIntervalMs;
|
||||||
|
this.dataPath = path.resolve(dataPath || './.wwebjs_auth/');
|
||||||
|
this.tempDir = `${this.dataPath}/wwebjs_temp_session`;
|
||||||
|
this.requiredDirs = ['Default', 'IndexedDB', 'Local Storage']; /* => Required Files & Dirs in WWebJS to restore session */
|
||||||
|
}
|
||||||
|
|
||||||
|
async beforeBrowserInitialized() {
|
||||||
|
const puppeteerOpts = this.client.options.puppeteer;
|
||||||
|
const sessionDirName = this.clientId ? `RemoteAuth-${this.clientId}` : 'RemoteAuth';
|
||||||
|
const dirPath = path.join(this.dataPath, sessionDirName);
|
||||||
|
|
||||||
|
if (puppeteerOpts.userDataDir && puppeteerOpts.userDataDir !== dirPath) {
|
||||||
|
throw new Error('RemoteAuth is not compatible with a user-supplied userDataDir.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.userDataDir = dirPath;
|
||||||
|
this.sessionName = sessionDirName;
|
||||||
|
|
||||||
|
await this.extractRemoteSession();
|
||||||
|
|
||||||
|
this.client.options.puppeteer = {
|
||||||
|
...puppeteerOpts,
|
||||||
|
userDataDir: dirPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
await this.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy() {
|
||||||
|
clearInterval(this.backupSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
await this.deleteRemoteSession();
|
||||||
|
|
||||||
|
let pathExists = await this.isValidPath(this.userDataDir);
|
||||||
|
if (pathExists) {
|
||||||
|
await fs.promises.rm(this.userDataDir, {
|
||||||
|
recursive: true,
|
||||||
|
force: true
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
clearInterval(this.backupSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterAuthReady() {
|
||||||
|
const sessionExists = await this.store.sessionExists({session: this.sessionName});
|
||||||
|
if(!sessionExists) {
|
||||||
|
await this.delay(60000); /* Initial delay sync required for session to be stable enough to recover */
|
||||||
|
await this.storeRemoteSession({emit: true});
|
||||||
|
}
|
||||||
|
var self = this;
|
||||||
|
this.backupSync = setInterval(async function () {
|
||||||
|
await self.storeRemoteSession();
|
||||||
|
}, this.backupSyncIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeRemoteSession(options) {
|
||||||
|
/* Compress & Store Session */
|
||||||
|
const pathExists = await this.isValidPath(this.userDataDir);
|
||||||
|
if (pathExists) {
|
||||||
|
await this.compressSession();
|
||||||
|
await this.store.save({session: this.sessionName});
|
||||||
|
await fs.promises.unlink(`${this.sessionName}.zip`);
|
||||||
|
await fs.promises.rm(`${this.tempDir}`, {
|
||||||
|
recursive: true,
|
||||||
|
force: true
|
||||||
|
}).catch(() => {});
|
||||||
|
if(options && options.emit) this.client.emit(Events.REMOTE_SESSION_SAVED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async extractRemoteSession() {
|
||||||
|
const pathExists = await this.isValidPath(this.userDataDir);
|
||||||
|
const compressedSessionPath = `${this.sessionName}.zip`;
|
||||||
|
const sessionExists = await this.store.sessionExists({session: this.sessionName});
|
||||||
|
if (pathExists) {
|
||||||
|
await fs.promises.rm(this.userDataDir, {
|
||||||
|
recursive: true,
|
||||||
|
force: true
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
if (sessionExists) {
|
||||||
|
await this.store.extract({session: this.sessionName, path: compressedSessionPath});
|
||||||
|
await this.unCompressSession(compressedSessionPath);
|
||||||
|
} else {
|
||||||
|
fs.mkdirSync(this.userDataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRemoteSession() {
|
||||||
|
const sessionExists = await this.store.sessionExists({session: this.sessionName});
|
||||||
|
if (sessionExists) await this.store.delete({session: this.sessionName});
|
||||||
|
}
|
||||||
|
|
||||||
|
async compressSession() {
|
||||||
|
const archive = archiver('zip');
|
||||||
|
const stream = fs.createWriteStream(`${this.sessionName}.zip`);
|
||||||
|
|
||||||
|
await fs.copy(this.userDataDir, this.tempDir).catch(() => {});
|
||||||
|
await this.deleteMetadata();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
archive
|
||||||
|
.directory(this.tempDir, false)
|
||||||
|
.on('error', err => reject(err))
|
||||||
|
.pipe(stream);
|
||||||
|
|
||||||
|
stream.on('close', () => resolve());
|
||||||
|
archive.finalize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async unCompressSession(compressedSessionPath) {
|
||||||
|
var stream = fs.createReadStream(compressedSessionPath);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
stream.pipe(unzipper.Extract({
|
||||||
|
path: this.userDataDir
|
||||||
|
}))
|
||||||
|
.on('error', err => reject(err))
|
||||||
|
.on('finish', () => resolve());
|
||||||
|
});
|
||||||
|
await fs.promises.unlink(compressedSessionPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMetadata() {
|
||||||
|
const sessionDirs = [this.tempDir, path.join(this.tempDir, 'Default')];
|
||||||
|
for (const dir of sessionDirs) {
|
||||||
|
const sessionFiles = await fs.promises.readdir(dir);
|
||||||
|
for (const element of sessionFiles) {
|
||||||
|
if (!this.requiredDirs.includes(element)) {
|
||||||
|
const dirElement = path.join(dir, element);
|
||||||
|
const stats = await fs.promises.lstat(dirElement);
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
await fs.promises.rm(dirElement, {
|
||||||
|
recursive: true,
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await fs.promises.unlink(dirElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isValidPath(path) {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(path);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delay(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RemoteAuth;
|
||||||
@@ -342,6 +342,8 @@ class Message extends Base {
|
|||||||
*/
|
*/
|
||||||
async react(reaction){
|
async react(reaction){
|
||||||
await this.client.pupPage.evaluate(async (messageId, reaction) => {
|
await this.client.pupPage.evaluate(async (messageId, reaction) => {
|
||||||
|
if (!messageId) { return undefined; }
|
||||||
|
|
||||||
const msg = await window.Store.Msg.get(messageId);
|
const msg = await window.Store.Msg.get(messageId);
|
||||||
await window.Store.sendReactionToMsg(msg, reaction);
|
await window.Store.sendReactionToMsg(msg, reaction);
|
||||||
}, this.id._serialized, reaction);
|
}, this.id._serialized, reaction);
|
||||||
@@ -356,7 +358,7 @@ class Message extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forwards this message to another chat
|
* Forwards this message to another chat (that you chatted before, otherwise it will fail)
|
||||||
*
|
*
|
||||||
* @param {string|Chat} chat Chat model or chat ID to which the message will be forwarded
|
* @param {string|Chat} chat Chat model or chat ID to which the message will be forwarded
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
@@ -408,12 +410,13 @@ class Message extends Base {
|
|||||||
signal: (new AbortController).signal
|
signal: (new AbortController).signal
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = window.WWebJS.arrayBufferToBase64(decryptedMedia);
|
const data = await window.WWebJS.arrayBufferToBase64Async(decryptedMedia);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
mimetype: msg.mimetype,
|
mimetype: msg.mimetype,
|
||||||
filename: msg.filename
|
filename: msg.filename,
|
||||||
|
filesize: msg.size
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if(e.status && e.status === 404) return undefined;
|
if(e.status && e.status === 404) return undefined;
|
||||||
@@ -422,7 +425,7 @@ class Message extends Base {
|
|||||||
}, this.id._serialized);
|
}, this.id._serialized);
|
||||||
|
|
||||||
if (!result) return undefined;
|
if (!result) return undefined;
|
||||||
return new MessageMedia(result.mimetype, result.data, result.filename);
|
return new MessageMedia(result.mimetype, result.data, result.filename, result.filesize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -449,7 +452,7 @@ class Message extends Base {
|
|||||||
let msg = window.Store.Msg.get(msgId);
|
let msg = window.Store.Msg.get(msgId);
|
||||||
|
|
||||||
if (msg.canStar()) {
|
if (msg.canStar()) {
|
||||||
return msg.chat.sendStarMsgs([msg], true);
|
return window.Store.Cmd.sendStarMsgs(msg.chat, [msg], false);
|
||||||
}
|
}
|
||||||
}, this.id._serialized);
|
}, this.id._serialized);
|
||||||
}
|
}
|
||||||
@@ -462,7 +465,7 @@ class Message extends Base {
|
|||||||
let msg = window.Store.Msg.get(msgId);
|
let msg = window.Store.Msg.get(msgId);
|
||||||
|
|
||||||
if (msg.canStar()) {
|
if (msg.canStar()) {
|
||||||
return msg.chat.sendStarMsgs([msg], false);
|
return window.Store.Cmd.sendUnstarMsgs(msg.chat, [msg], false);
|
||||||
}
|
}
|
||||||
}, this.id._serialized);
|
}, this.id._serialized);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ const { URL } = require('url');
|
|||||||
* Media attached to a message
|
* Media attached to a message
|
||||||
* @param {string} mimetype MIME type of the attachment
|
* @param {string} mimetype MIME type of the attachment
|
||||||
* @param {string} data Base64-encoded data of the file
|
* @param {string} data Base64-encoded data of the file
|
||||||
* @param {?string} filename Document file name
|
* @param {?string} filename Document file name. Value can be null
|
||||||
|
* @param {?number} filesize Document file size in bytes. Value can be null
|
||||||
*/
|
*/
|
||||||
class MessageMedia {
|
class MessageMedia {
|
||||||
constructor(mimetype, data, filename) {
|
constructor(mimetype, data, filename, filesize) {
|
||||||
/**
|
/**
|
||||||
* MIME type of the attachment
|
* MIME type of the attachment
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -27,10 +28,16 @@ class MessageMedia {
|
|||||||
this.data = data;
|
this.data = data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the file (for documents)
|
* Document file name. Value can be null
|
||||||
* @type {?string}
|
* @type {?string}
|
||||||
*/
|
*/
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Document file size in bytes. Value can be null
|
||||||
|
* @type {?number}
|
||||||
|
*/
|
||||||
|
this.filesize = filesize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,6 +75,7 @@ class MessageMedia {
|
|||||||
const reqOptions = Object.assign({ headers: { accept: 'image/* video/* text/* audio/*' } }, options);
|
const reqOptions = Object.assign({ headers: { accept: 'image/* video/* text/* audio/*' } }, options);
|
||||||
const response = await fetch(url, reqOptions);
|
const response = await fetch(url, reqOptions);
|
||||||
const mime = response.headers.get('Content-Type');
|
const mime = response.headers.get('Content-Type');
|
||||||
|
const size = response.headers.get('Content-Length');
|
||||||
|
|
||||||
const contentDisposition = response.headers.get('Content-Disposition');
|
const contentDisposition = response.headers.get('Content-Disposition');
|
||||||
const name = contentDisposition ? contentDisposition.match(/((?<=filename=")(.*)(?="))/) : null;
|
const name = contentDisposition ? contentDisposition.match(/((?<=filename=")(.*)(?="))/) : null;
|
||||||
@@ -83,7 +91,7 @@ class MessageMedia {
|
|||||||
data = btoa(data);
|
data = btoa(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data, mime, name };
|
return { data, mime, name, size };
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = options.client
|
const res = options.client
|
||||||
@@ -96,7 +104,7 @@ class MessageMedia {
|
|||||||
if (!mimetype)
|
if (!mimetype)
|
||||||
mimetype = res.mime;
|
mimetype = res.mime;
|
||||||
|
|
||||||
return new MessageMedia(mimetype, res.data, filename);
|
return new MessageMedia(mimetype, res.data, filename, res.size || null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
69
src/structures/Reaction.js
Normal file
69
src/structures/Reaction.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Base = require('./Base');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Reaction on WhatsApp
|
||||||
|
* @extends {Base}
|
||||||
|
*/
|
||||||
|
class Reaction extends Base {
|
||||||
|
constructor(client, data) {
|
||||||
|
super(client);
|
||||||
|
|
||||||
|
if (data) this._patch(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
_patch(data) {
|
||||||
|
/**
|
||||||
|
* Reaction ID
|
||||||
|
* @type {object}
|
||||||
|
*/
|
||||||
|
this.id = data.msgKey;
|
||||||
|
/**
|
||||||
|
* Orphan
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.orphan = data.orphan;
|
||||||
|
/**
|
||||||
|
* Orphan reason
|
||||||
|
* @type {?string}
|
||||||
|
*/
|
||||||
|
this.orphanReason = data.orphanReason;
|
||||||
|
/**
|
||||||
|
* Unix timestamp for when the reaction was created
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.timestamp = data.timestamp;
|
||||||
|
/**
|
||||||
|
* Reaction
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.reaction = data.reactionText;
|
||||||
|
/**
|
||||||
|
* Read
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this.read = data.read;
|
||||||
|
/**
|
||||||
|
* Message ID
|
||||||
|
* @type {object}
|
||||||
|
*/
|
||||||
|
this.msgId = data.parentMsgKey;
|
||||||
|
/**
|
||||||
|
* Sender ID
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.senderId = data.senderUserJid;
|
||||||
|
/**
|
||||||
|
* ACK
|
||||||
|
* @type {?number}
|
||||||
|
*/
|
||||||
|
this.ack = data.ack;
|
||||||
|
|
||||||
|
|
||||||
|
return super._patch(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Reaction;
|
||||||
@@ -18,4 +18,5 @@ module.exports = {
|
|||||||
Buttons: require('./Buttons'),
|
Buttons: require('./Buttons'),
|
||||||
List: require('./List'),
|
List: require('./List'),
|
||||||
Payment: require('./Payment'),
|
Payment: require('./Payment'),
|
||||||
|
Reaction: require('./Reaction'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ exports.DefaultOptions = {
|
|||||||
qrMaxRetries: 0,
|
qrMaxRetries: 0,
|
||||||
takeoverOnConflict: false,
|
takeoverOnConflict: false,
|
||||||
takeoverTimeoutMs: 0,
|
takeoverTimeoutMs: 0,
|
||||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36',
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36',
|
||||||
ffmpegPath: 'ffmpeg',
|
ffmpegPath: 'ffmpeg',
|
||||||
bypassCSP: false
|
bypassCSP: false
|
||||||
};
|
};
|
||||||
@@ -41,15 +41,18 @@ exports.Events = {
|
|||||||
MESSAGE_REVOKED_EVERYONE: 'message_revoke_everyone',
|
MESSAGE_REVOKED_EVERYONE: 'message_revoke_everyone',
|
||||||
MESSAGE_REVOKED_ME: 'message_revoke_me',
|
MESSAGE_REVOKED_ME: 'message_revoke_me',
|
||||||
MESSAGE_ACK: 'message_ack',
|
MESSAGE_ACK: 'message_ack',
|
||||||
|
MESSAGE_REACTION: 'message_reaction',
|
||||||
MEDIA_UPLOADED: 'media_uploaded',
|
MEDIA_UPLOADED: 'media_uploaded',
|
||||||
GROUP_JOIN: 'group_join',
|
GROUP_JOIN: 'group_join',
|
||||||
GROUP_LEAVE: 'group_leave',
|
GROUP_LEAVE: 'group_leave',
|
||||||
GROUP_UPDATE: 'group_update',
|
GROUP_UPDATE: 'group_update',
|
||||||
QR_RECEIVED: 'qr',
|
QR_RECEIVED: 'qr',
|
||||||
|
LOADING_SCREEN: 'loading_screen',
|
||||||
DISCONNECTED: 'disconnected',
|
DISCONNECTED: 'disconnected',
|
||||||
STATE_CHANGED: 'change_state',
|
STATE_CHANGED: 'change_state',
|
||||||
BATTERY_CHANGED: 'change_battery',
|
BATTERY_CHANGED: 'change_battery',
|
||||||
INCOMING_CALL: 'incoming_call'
|
INCOMING_CALL: 'incoming_call',
|
||||||
|
REMOTE_SESSION_SAVED: 'remote_session_saved'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ exports.ExposeStore = (moduleRaidStr) => {
|
|||||||
window.Store.StatusUtils = window.mR.findModule('setMyStatus')[0];
|
window.Store.StatusUtils = window.mR.findModule('setMyStatus')[0];
|
||||||
window.Store.ConversationMsgs = window.mR.findModule('loadEarlierMsgs')[0];
|
window.Store.ConversationMsgs = window.mR.findModule('loadEarlierMsgs')[0];
|
||||||
window.Store.sendReactionToMsg = window.mR.findModule('sendReactionToMsg')[0].sendReactionToMsg;
|
window.Store.sendReactionToMsg = window.mR.findModule('sendReactionToMsg')[0].sendReactionToMsg;
|
||||||
|
window.Store.createOrUpdateReactionsModule = window.mR.findModule('createOrUpdateReactions')[0];
|
||||||
window.Store.StickerTools = {
|
window.Store.StickerTools = {
|
||||||
...window.mR.findModule('toWebpSticker')[0],
|
...window.mR.findModule('toWebpSticker')[0],
|
||||||
...window.mR.findModule('addWebpMetadata')[0]
|
...window.mR.findModule('addWebpMetadata')[0]
|
||||||
@@ -479,6 +480,20 @@ exports.LoadUtils = () => {
|
|||||||
return window.btoa(binary);
|
return window.btoa(binary);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.WWebJS.arrayBufferToBase64Async = (arrayBuffer) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const blob = new Blob([arrayBuffer], {
|
||||||
|
type: 'application/octet-stream',
|
||||||
|
});
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onload = () => {
|
||||||
|
const [, data] = fileReader.result.split(',');
|
||||||
|
resolve(data);
|
||||||
|
};
|
||||||
|
fileReader.onerror = (e) => reject(e);
|
||||||
|
fileReader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
|
||||||
window.WWebJS.getFileHash = async (data) => {
|
window.WWebJS.getFileHash = async (data) => {
|
||||||
let buffer = await data.arrayBuffer();
|
let buffer = await data.arrayBuffer();
|
||||||
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
|
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
|
||||||
|
|||||||
Reference in New Issue
Block a user