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);