diff --git a/docs/structures_Message.js.html b/docs/structures_Message.js.html
index 125c4f3..d937dd5 100644
--- a/docs/structures_Message.js.html
+++ b/docs/structures_Message.js.html
@@ -480,7 +480,7 @@ class Message extends Base {
let msg = window.Store.Msg.get(msgId);
if (msg.canStar()) {
- return msg.chat.sendStarMsgs([msg], true);
+ return window.Store.Cmd.sendStarMsgs(msg.chat, [msg], false);
}
}, this.id._serialized);
}
@@ -493,7 +493,7 @@ class Message extends Base {
let msg = window.Store.Msg.get(msgId);
if (msg.canStar()) {
- return msg.chat.sendStarMsgs([msg], false);
+ return window.Store.Cmd.sendUnstarMsgs(msg.chat, [msg], false);
}
}, this.id._serialized);
}
diff --git a/example.js b/example.js
index 3c84bab..99dc7ea 100644
--- a/example.js
+++ b/example.js
@@ -7,6 +7,10 @@ const client = new Client({
client.initialize();
+client.on('loading_screen', (percent, message) => {
+ console.log('LOADING SCREEN', percent, message);
+});
+
client.on('qr', (qr) => {
// NOTE: This event will not be fired if a session is specified.
console.log('QR RECEIVED', qr);
diff --git a/index.d.ts b/index.d.ts
index 040d918..63b8625 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -241,6 +241,15 @@ declare namespace WAWebJS {
message: Message
) => 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 */
on(event: 'qr', listener: (
/** qr code string
@@ -256,6 +265,9 @@ declare namespace WAWebJS {
/** Emitted when the client has initialized and is ready to receive messages */
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 */
@@ -345,6 +357,9 @@ declare namespace WAWebJS {
failureEventPayload?: any
}>;
getAuthEventPayload: () => Promise;
+ afterAuthReady: () => Promise;
+ disconnect: () => Promise;
+ destroy: () => Promise;
logout: () => Promise;
}
@@ -365,6 +380,30 @@ declare namespace WAWebJS {
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,
+ delete: ({session: string}) => Promise | any,
+ save: ({session: string}) => Promise | any,
+ extract: ({session: string, path: string}) => Promise | any,
+ }
/**
* Legacy session auth strategy
@@ -463,9 +502,11 @@ declare namespace WAWebJS {
GROUP_LEAVE = 'group_leave',
GROUP_UPDATE = 'group_update',
QR_RECEIVED = 'qr',
+ LOADING_SCREEN = 'loading_screen',
DISCONNECTED = 'disconnected',
STATE_CHANGED = 'change_state',
BATTERY_CHANGED = 'change_battery',
+ REMOTE_SESSION_SAVED = 'remote_session_saved'
}
/** Group notification types */
@@ -708,7 +749,7 @@ declare namespace WAWebJS {
/** React to this message with an emoji*/
react: (reaction: string) => Promise,
/**
- * 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,
/** Star this message */
@@ -805,13 +846,16 @@ declare namespace WAWebJS {
data: string
/** Document file name. Value can be 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} data Base64-encoded data of the file
* @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 */
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)
}
+
+ /** 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
diff --git a/index.js b/index.js
index 5a149e4..b498064 100644
--- a/index.js
+++ b/index.js
@@ -25,6 +25,7 @@ module.exports = {
// Auth Strategies
NoAuth: require('./src/authStrategies/NoAuth'),
LocalAuth: require('./src/authStrategies/LocalAuth'),
+ RemoteAuth: require('./src/authStrategies/RemoteAuth'),
LegacySessionAuth: require('./src/authStrategies/LegacySessionAuth'),
...Constants
diff --git a/package.json b/package.json
index e33dfe4..13f41c0 100644
--- a/package.json
+++ b/package.json
@@ -51,5 +51,10 @@
},
"engines": {
"node": ">=12.0.0"
+ },
+ "optionalDependencies": {
+ "archiver": "^5.3.1",
+ "fs-extra": "^10.1.0",
+ "unzipper": "^0.10.11"
}
}
diff --git a/src/Client.js b/src/Client.js
index d30390b..b36eb96 100644
--- a/src/Client.js
+++ b/src/Client.js
@@ -10,7 +10,7 @@ const { WhatsWebURL, DefaultOptions, Events, WAState } = require('./util/Constan
const { ExposeStore, LoadUtils } = require('./util/Injected');
const ChatFactory = require('./factories/ChatFactory');
const ContactFactory = require('./factories/ContactFactory');
-const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List} = require('./structures');
+const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List, Reaction } = require('./structures');
const LegacySessionAuth = require('./authStrategies/LegacySessionAuth');
const NoAuth = require('./authStrategies/NoAuth');
@@ -115,6 +115,52 @@ class Client extends EventEmitter {
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_QRCODE_SELECTOR = 'div[data-ref] canvas';
@@ -373,7 +419,7 @@ class Client extends EventEmitter {
this.emit(Events.MEDIA_UPLOADED, message);
});
- await page.exposeFunction('onAppStateChangedEvent', (state) => {
+ await page.exposeFunction('onAppStateChangedEvent', async (state) => {
/**
* Emitted when the connection state changes
@@ -400,6 +446,7 @@ class Client extends EventEmitter {
* @event Client#disconnected
* @param {WAState|"NAVIGATION"} reason reason that caused the disconnect
*/
+ await this.authStrategy.disconnect();
this.emit(Events.DISCONNECTED, state);
this.destroy();
}
@@ -439,6 +486,27 @@ class Client extends EventEmitter {
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(() => {
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)); });
@@ -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
*/
this.emit(Events.READY);
+ this.authStrategy.afterAuthReady();
// Disconnect when navigating away when in PAIRING state (detect logout)
this.pupPage.on('framenavigated', async () => {
const appState = await this.getState();
if(!appState || appState === WAState.PAIRING) {
+ await this.authStrategy.disconnect();
this.emit(Events.DISCONNECTED, 'NAVIGATION');
await this.destroy();
}
@@ -481,6 +567,7 @@ class Client extends EventEmitter {
*/
async destroy() {
await this.pupBrowser.close();
+ await this.authStrategy.destroy();
}
/**
diff --git a/src/authStrategies/BaseAuthStrategy.js b/src/authStrategies/BaseAuthStrategy.js
index 0c7a7c9..0344026 100644
--- a/src/authStrategies/BaseAuthStrategy.js
+++ b/src/authStrategies/BaseAuthStrategy.js
@@ -18,6 +18,9 @@ class BaseAuthStrategy {
};
}
async getAuthEventPayload() {}
+ async afterAuthReady() {}
+ async disconnect() {}
+ async destroy() {}
async logout() {}
}
diff --git a/src/authStrategies/RemoteAuth.js b/src/authStrategies/RemoteAuth.js
new file mode 100644
index 0000000..5ae85cc
--- /dev/null
+++ b/src/authStrategies/RemoteAuth.js
@@ -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;
diff --git a/src/structures/Message.js b/src/structures/Message.js
index 94893cc..dc4acaf 100644
--- a/src/structures/Message.js
+++ b/src/structures/Message.js
@@ -342,6 +342,8 @@ class Message extends Base {
*/
async react(reaction){
await this.client.pupPage.evaluate(async (messageId, reaction) => {
+ if (!messageId) { return undefined; }
+
const msg = await window.Store.Msg.get(messageId);
await window.Store.sendReactionToMsg(msg, 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
* @returns {Promise}
@@ -408,12 +410,13 @@ class Message extends Base {
signal: (new AbortController).signal
});
- const data = window.WWebJS.arrayBufferToBase64(decryptedMedia);
+ const data = await window.WWebJS.arrayBufferToBase64Async(decryptedMedia);
return {
data,
mimetype: msg.mimetype,
- filename: msg.filename
+ filename: msg.filename,
+ filesize: msg.size
};
} catch (e) {
if(e.status && e.status === 404) return undefined;
@@ -422,7 +425,7 @@ class Message extends Base {
}, this.id._serialized);
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);
if (msg.canStar()) {
- return msg.chat.sendStarMsgs([msg], true);
+ return window.Store.Cmd.sendStarMsgs(msg.chat, [msg], false);
}
}, this.id._serialized);
}
@@ -462,7 +465,7 @@ class Message extends Base {
let msg = window.Store.Msg.get(msgId);
if (msg.canStar()) {
- return msg.chat.sendStarMsgs([msg], false);
+ return window.Store.Cmd.sendUnstarMsgs(msg.chat, [msg], false);
}
}, this.id._serialized);
}
diff --git a/src/structures/MessageMedia.js b/src/structures/MessageMedia.js
index 450f78b..9be15be 100644
--- a/src/structures/MessageMedia.js
+++ b/src/structures/MessageMedia.js
@@ -10,10 +10,11 @@ const { URL } = require('url');
* Media attached to a message
* @param {string} mimetype MIME type of the attachment
* @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 {
- constructor(mimetype, data, filename) {
+ constructor(mimetype, data, filename, filesize) {
/**
* MIME type of the attachment
* @type {string}
@@ -27,10 +28,16 @@ class MessageMedia {
this.data = data;
/**
- * Name of the file (for documents)
+ * Document file name. Value can be null
* @type {?string}
*/
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 response = await fetch(url, reqOptions);
const mime = response.headers.get('Content-Type');
+ const size = response.headers.get('Content-Length');
const contentDisposition = response.headers.get('Content-Disposition');
const name = contentDisposition ? contentDisposition.match(/((?<=filename=")(.*)(?="))/) : null;
@@ -83,7 +91,7 @@ class MessageMedia {
data = btoa(data);
}
- return { data, mime, name };
+ return { data, mime, name, size };
}
const res = options.client
@@ -96,7 +104,7 @@ class MessageMedia {
if (!mimetype)
mimetype = res.mime;
- return new MessageMedia(mimetype, res.data, filename);
+ return new MessageMedia(mimetype, res.data, filename, res.size || null);
}
}
diff --git a/src/structures/Reaction.js b/src/structures/Reaction.js
new file mode 100644
index 0000000..bb30881
--- /dev/null
+++ b/src/structures/Reaction.js
@@ -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;
\ No newline at end of file
diff --git a/src/structures/index.js b/src/structures/index.js
index fc5b212..ec449f0 100644
--- a/src/structures/index.js
+++ b/src/structures/index.js
@@ -18,4 +18,5 @@ module.exports = {
Buttons: require('./Buttons'),
List: require('./List'),
Payment: require('./Payment'),
+ Reaction: require('./Reaction'),
};
diff --git a/src/util/Constants.js b/src/util/Constants.js
index 83a030f..d062c7a 100644
--- a/src/util/Constants.js
+++ b/src/util/Constants.js
@@ -11,7 +11,7 @@ exports.DefaultOptions = {
qrMaxRetries: 0,
takeoverOnConflict: false,
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',
bypassCSP: false
};
@@ -41,15 +41,18 @@ exports.Events = {
MESSAGE_REVOKED_EVERYONE: 'message_revoke_everyone',
MESSAGE_REVOKED_ME: 'message_revoke_me',
MESSAGE_ACK: 'message_ack',
+ MESSAGE_REACTION: 'message_reaction',
MEDIA_UPLOADED: 'media_uploaded',
GROUP_JOIN: 'group_join',
GROUP_LEAVE: 'group_leave',
GROUP_UPDATE: 'group_update',
QR_RECEIVED: 'qr',
+ LOADING_SCREEN: 'loading_screen',
DISCONNECTED: 'disconnected',
STATE_CHANGED: 'change_state',
BATTERY_CHANGED: 'change_battery',
- INCOMING_CALL: 'incoming_call'
+ INCOMING_CALL: 'incoming_call',
+ REMOTE_SESSION_SAVED: 'remote_session_saved'
};
/**
diff --git a/src/util/Injected.js b/src/util/Injected.js
index 7eea752..b09b27e 100644
--- a/src/util/Injected.js
+++ b/src/util/Injected.js
@@ -50,6 +50,7 @@ exports.ExposeStore = (moduleRaidStr) => {
window.Store.StatusUtils = window.mR.findModule('setMyStatus')[0];
window.Store.ConversationMsgs = window.mR.findModule('loadEarlierMsgs')[0];
window.Store.sendReactionToMsg = window.mR.findModule('sendReactionToMsg')[0].sendReactionToMsg;
+ window.Store.createOrUpdateReactionsModule = window.mR.findModule('createOrUpdateReactions')[0];
window.Store.StickerTools = {
...window.mR.findModule('toWebpSticker')[0],
...window.mR.findModule('addWebpMetadata')[0]
@@ -479,6 +480,20 @@ exports.LoadUtils = () => {
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) => {
let buffer = await data.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);