Use idempotent exposeFunctionIfAbsent (no reinject flag) (#3233)

* Add exposeFunctionIfAbsent function

* use exposeFunctionIfAbsent function instead of original exposeFunction

* Remove "reinject" flag

* exposeFunctionIfAbsent - onQRChangedEvent

* exposeFunctionIfAbsent - onLogoutEvent
This commit is contained in:
devlikeapro 2024-09-25 22:31:35 +07:00 • committed by GitHub
commit e470c82237
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 235 additions and 227 deletions

View file

@ -17,6 +17,7 @@ const ContactFactory = require('./factories/ContactFactory');
const WebCacheFactory = require('./webCache/WebCacheFactory'); const WebCacheFactory = require('./webCache/WebCacheFactory');
const { ClientInfo, Message, MessageMedia, Contact, Location, Poll, PollVote, GroupNotification, Label, Call, Buttons, List, Reaction } = require('./structures'); const { ClientInfo, Message, MessageMedia, Contact, Location, Poll, PollVote, GroupNotification, Label, Call, Buttons, List, Reaction } = require('./structures');
const NoAuth = require('./authStrategies/NoAuth'); const NoAuth = require('./authStrategies/NoAuth');
const {exposeFunctionIfAbsent} = require('./util/Puppeteer');
/** /**
* Starting point for interacting with the WhatsApp Web API * Starting point for interacting with the WhatsApp Web API
@ -90,9 +91,8 @@ class Client extends EventEmitter {
/** /**
* Injection logic * Injection logic
* Private function * Private function
* @property {boolean} reinject is this a reinject?
*/ */
async inject(reinject = false) { async inject() {
await this.pupPage.waitForFunction('window.Debug?.VERSION != undefined', {timeout: this.options.authTimeoutMs}); await this.pupPage.waitForFunction('window.Debug?.VERSION != undefined', {timeout: this.options.authTimeoutMs});
const version = await this.getWWebVersion(); const version = await this.getWWebVersion();
@ -142,26 +142,21 @@ class Client extends EventEmitter {
// Register qr events // Register qr events
let qrRetries = 0; let qrRetries = 0;
const injected = await this.pupPage.evaluate(() => { await exposeFunctionIfAbsent(this.pupPage, 'onQRChangedEvent', async (qr) => {
return typeof window.onQRChangedEvent !== 'undefined'; /**
}); * Emitted when a QR code is received
if (!injected) { * @event Client#qr
await this.pupPage.exposeFunction('onQRChangedEvent', async (qr) => { * @param {string} qr QR Code
/** */
* Emitted when a QR code is received this.emit(Events.QR_RECEIVED, qr);
* @event Client#qr if (this.options.qrMaxRetries > 0) {
* @param {string} qr QR Code qrRetries++;
*/ if (qrRetries > this.options.qrMaxRetries) {
this.emit(Events.QR_RECEIVED, qr); this.emit(Events.DISCONNECTED, 'Max qrcode retries reached');
if (this.options.qrMaxRetries > 0) { await this.destroy();
qrRetries++;
if (qrRetries > this.options.qrMaxRetries) {
this.emit(Events.DISCONNECTED, 'Max qrcode retries reached');
await this.destroy();
}
} }
}); }
} });
await this.pupPage.evaluate(async () => { await this.pupPage.evaluate(async () => {
@ -178,86 +173,78 @@ class Client extends EventEmitter {
}); });
} }
if (!reinject) { await exposeFunctionIfAbsent(this.pupPage, 'onAuthAppStateChangedEvent', async (state) => {
await this.pupPage.exposeFunction('onAuthAppStateChangedEvent', async (state) => { if (state == 'UNPAIRED_IDLE') {
if (state == 'UNPAIRED_IDLE') { // refresh qr code
// refresh qr code window.Store.Cmd.refreshQR();
window.Store.Cmd.refreshQR(); }
} });
});
await this.pupPage.exposeFunction('onAppStateHasSyncedEvent', async () => { await exposeFunctionIfAbsent(this.pupPage, 'onAppStateHasSyncedEvent', async () => {
const authEventPayload = await this.authStrategy.getAuthEventPayload(); const authEventPayload = await this.authStrategy.getAuthEventPayload();
/** /**
* Emitted when authentication is successful * Emitted when authentication is successful
* @event Client#authenticated * @event Client#authenticated
*/ */
this.emit(Events.AUTHENTICATED, authEventPayload); this.emit(Events.AUTHENTICATED, authEventPayload);
const injected = await this.pupPage.evaluate(async () => { const injected = await this.pupPage.evaluate(async () => {
return typeof window.Store !== 'undefined' && typeof window.WWebJS !== 'undefined'; return typeof window.Store !== 'undefined' && typeof window.WWebJS !== 'undefined';
}); });
if (!injected) { if (!injected) {
if (this.options.webVersionCache.type === 'local' && this.currentIndexHtml) { if (this.options.webVersionCache.type === 'local' && this.currentIndexHtml) {
const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache; const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache;
const webCache = WebCacheFactory.createWebCache(webCacheType, webCacheOptions); const webCache = WebCacheFactory.createWebCache(webCacheType, webCacheOptions);
await webCache.persist(this.currentIndexHtml, version); await webCache.persist(this.currentIndexHtml, version);
} }
if (isCometOrAbove) { if (isCometOrAbove) {
await this.pupPage.evaluate(ExposeStore); await this.pupPage.evaluate(ExposeStore);
} else { } else {
// make sure all modules are ready before injection // make sure all modules are ready before injection
// 2 second delay after authentication makes sense and does not need to be made dyanmic or removed // 2 second delay after authentication makes sense and does not need to be made dyanmic or removed
await new Promise(r => setTimeout(r, 2000)); await new Promise(r => setTimeout(r, 2000));
await this.pupPage.evaluate(ExposeLegacyStore); await this.pupPage.evaluate(ExposeLegacyStore);
} }
// Check window.Store Injection // Check window.Store Injection
await this.pupPage.waitForFunction('window.Store != undefined'); await this.pupPage.waitForFunction('window.Store != undefined');
/** /**
* Current connection information * Current connection information
* @type {ClientInfo} * @type {ClientInfo}
*/ */
this.info = new ClientInfo(this, await this.pupPage.evaluate(() => { this.info = new ClientInfo(this, await this.pupPage.evaluate(() => {
return { ...window.Store.Conn.serialize(), wid: window.Store.User.getMeUser() }; return { ...window.Store.Conn.serialize(), wid: window.Store.User.getMeUser() };
})); }));
this.interface = new InterfaceController(this); this.interface = new InterfaceController(this);
//Load util functions (serializers, helper functions) //Load util functions (serializers, helper functions)
await this.pupPage.evaluate(LoadUtils); await this.pupPage.evaluate(LoadUtils);
await this.attachEventListeners(reinject); await this.attachEventListeners();
reinject = true; }
} /**
/**
* Emitted when the client has initialized and is ready to receive messages. * Emitted when the client has initialized and is ready to receive messages.
* @event Client#ready * @event Client#ready
*/ */
this.emit(Events.READY); this.emit(Events.READY);
this.authStrategy.afterAuthReady(); this.authStrategy.afterAuthReady();
}); });
let lastPercent = null; let lastPercent = null;
await this.pupPage.exposeFunction('onOfflineProgressUpdateEvent', async (percent) => { await exposeFunctionIfAbsent(this.pupPage, 'onOfflineProgressUpdateEvent', async (percent) => {
if (lastPercent !== percent) { if (lastPercent !== percent) {
lastPercent = percent; lastPercent = percent;
this.emit(Events.LOADING_SCREEN, percent, 'WhatsApp'); // Message is hardcoded as "WhatsApp" for now this.emit(Events.LOADING_SCREEN, percent, 'WhatsApp'); // Message is hardcoded as "WhatsApp" for now
} }
}); });
} await exposeFunctionIfAbsent(this.pupPage, 'onLogoutEvent', async () => {
const logoutCatchInjected = await this.pupPage.evaluate(() => { this.lastLoggedOut = true;
return typeof window.onLogoutEvent !== 'undefined'; await this.pupPage.waitForNavigation({waitUntil: 'load', timeout: 5000}).catch((_) => _);
}); });
if (!logoutCatchInjected) {
await this.pupPage.exposeFunction('onLogoutEvent', async () => {
this.lastLoggedOut = true;
await this.pupPage.waitForNavigation({waitUntil: 'load', timeout: 5000}).catch((_) => _);
});
}
await this.pupPage.evaluate(() => { await this.pupPage.evaluate(() => {
window.AuthStore.AppState.on('change:state', (_AppState, state) => { window.onAuthAppStateChangedEvent(state); }); window.AuthStore.AppState.on('change:state', (_AppState, state) => { window.onAuthAppStateChangedEvent(state); });
window.AuthStore.AppState.on('change:hasSynced', () => { window.onAppStateHasSyncedEvent(); }); window.AuthStore.AppState.on('change:hasSynced', () => { window.onAppStateHasSyncedEvent(); });
@ -349,7 +336,7 @@ class Client extends EventEmitter {
await this.authStrategy.afterBrowserInitialized(); await this.authStrategy.afterBrowserInitialized();
this.lastLoggedOut = false; this.lastLoggedOut = false;
} }
await this.inject(true); await this.inject();
}); });
} }
@ -372,34 +359,33 @@ class Client extends EventEmitter {
* Private function * Private function
* @property {boolean} reinject is this a reinject? * @property {boolean} reinject is this a reinject?
*/ */
async attachEventListeners(reinject = false) { async attachEventListeners() {
if (!reinject) { await exposeFunctionIfAbsent(this.pupPage, 'onAddMessageEvent', msg => {
await this.pupPage.exposeFunction('onAddMessageEvent', msg => { if (msg.type === 'gp2') {
if (msg.type === 'gp2') { const notification = new GroupNotification(this, msg);
const notification = new GroupNotification(this, msg); if (['add', 'invite', 'linked_group_join'].includes(msg.subtype)) {
if (['add', 'invite', 'linked_group_join'].includes(msg.subtype)) { /**
/**
* Emitted when a user joins the chat via invite link or is added by an admin. * Emitted when a user joins the chat via invite link or is added by an admin.
* @event Client#group_join * @event Client#group_join
* @param {GroupNotification} notification GroupNotification with more information about the action * @param {GroupNotification} notification GroupNotification with more information about the action
*/ */
this.emit(Events.GROUP_JOIN, notification); this.emit(Events.GROUP_JOIN, notification);
} else if (msg.subtype === 'remove' || msg.subtype === 'leave') { } else if (msg.subtype === 'remove' || msg.subtype === 'leave') {
/** /**
* Emitted when a user leaves the chat or is removed by an admin. * Emitted when a user leaves the chat or is removed by an admin.
* @event Client#group_leave * @event Client#group_leave
* @param {GroupNotification} notification GroupNotification with more information about the action * @param {GroupNotification} notification GroupNotification with more information about the action
*/ */
this.emit(Events.GROUP_LEAVE, notification); this.emit(Events.GROUP_LEAVE, notification);
} else if (msg.subtype === 'promote' || msg.subtype === 'demote') { } else if (msg.subtype === 'promote' || msg.subtype === 'demote') {
/** /**
* Emitted when a current user is promoted to an admin or demoted to a regular user. * Emitted when a current user is promoted to an admin or demoted to a regular user.
* @event Client#group_admin_changed * @event Client#group_admin_changed
* @param {GroupNotification} notification GroupNotification with more information about the action * @param {GroupNotification} notification GroupNotification with more information about the action
*/ */
this.emit(Events.GROUP_ADMIN_CHANGED, notification); this.emit(Events.GROUP_ADMIN_CHANGED, notification);
} else if (msg.subtype === 'membership_approval_request') { } else if (msg.subtype === 'membership_approval_request') {
/** /**
* Emitted when some user requested to join the group * Emitted when some user requested to join the group
* that has the membership approval mode turned on * that has the membership approval mode turned on
* @event Client#group_membership_request * @event Client#group_membership_request
@ -408,86 +394,86 @@ class Client extends EventEmitter {
* @param {string} notification.author The user ID that made a request * @param {string} notification.author The user ID that made a request
* @param {number} notification.timestamp The timestamp the request was made at * @param {number} notification.timestamp The timestamp the request was made at
*/ */
this.emit(Events.GROUP_MEMBERSHIP_REQUEST, notification); this.emit(Events.GROUP_MEMBERSHIP_REQUEST, notification);
} else { } else {
/** /**
* Emitted when group settings are updated, such as subject, description or picture. * Emitted when group settings are updated, such as subject, description or picture.
* @event Client#group_update * @event Client#group_update
* @param {GroupNotification} notification GroupNotification with more information about the action * @param {GroupNotification} notification GroupNotification with more information about the action
*/ */
this.emit(Events.GROUP_UPDATE, notification); this.emit(Events.GROUP_UPDATE, notification);
}
return;
} }
return;
}
const message = new Message(this, msg); const message = new Message(this, msg);
/** /**
* Emitted when a new message is created, which may include the current user's own messages. * Emitted when a new message is created, which may include the current user's own messages.
* @event Client#message_create * @event Client#message_create
* @param {Message} message The message that was created * @param {Message} message The message that was created
*/ */
this.emit(Events.MESSAGE_CREATE, message); this.emit(Events.MESSAGE_CREATE, message);
if (msg.id.fromMe) return; if (msg.id.fromMe) return;
/** /**
* Emitted when a new message is received. * Emitted when a new message is received.
* @event Client#message * @event Client#message
* @param {Message} message The message that was received * @param {Message} message The message that was received
*/ */
this.emit(Events.MESSAGE_RECEIVED, message); this.emit(Events.MESSAGE_RECEIVED, message);
}); });
let last_message; let last_message;
await this.pupPage.exposeFunction('onChangeMessageTypeEvent', (msg) => { await exposeFunctionIfAbsent(this.pupPage, 'onChangeMessageTypeEvent', (msg) => {
if (msg.type === 'revoked') { if (msg.type === 'revoked') {
const message = new Message(this, msg); const message = new Message(this, msg);
let revoked_msg; let revoked_msg;
if (last_message && msg.id.id === last_message.id.id) { if (last_message && msg.id.id === last_message.id.id) {
revoked_msg = new Message(this, last_message); revoked_msg = new Message(this, last_message);
} }
/** /**
* Emitted when a message is deleted for everyone in the chat. * Emitted when a message is deleted for everyone in the chat.
* @event Client#message_revoke_everyone * @event Client#message_revoke_everyone
* @param {Message} message The message that was revoked, in its current state. It will not contain the original message's data. * @param {Message} message The message that was revoked, in its current state. It will not contain the original message's data.
* @param {?Message} revoked_msg The message that was revoked, before it was revoked. It will contain the message's original data. * @param {?Message} revoked_msg The message that was revoked, before it was revoked. It will contain the message's original data.
* Note that due to the way this data is captured, it may be possible that this param will be undefined. * Note that due to the way this data is captured, it may be possible that this param will be undefined.
*/ */
this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg); this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg);
} }
}); });
await this.pupPage.exposeFunction('onChangeMessageEvent', (msg) => { await exposeFunctionIfAbsent(this.pupPage, 'onChangeMessageEvent', (msg) => {
if (msg.type !== 'revoked') { if (msg.type !== 'revoked') {
last_message = msg; last_message = msg;
} }
/** /**
* The event notification that is received when one of * The event notification that is received when one of
* the group participants changes their phone number. * the group participants changes their phone number.
*/ */
const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify'; const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify';
/** /**
* The event notification that is received when one of * The event notification that is received when one of
* the contacts changes their phone number. * the contacts changes their phone number.
*/ */
const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number'; const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number';
if (isParticipant || isContact) { if (isParticipant || isContact) {
/** @type {GroupNotification} object does not provide enough information about this event, so a @type {Message} object is used. */ /** @type {GroupNotification} object does not provide enough information about this event, so a @type {Message} object is used. */
const message = new Message(this, msg); const message = new Message(this, msg);
const newId = isParticipant ? msg.recipients[0] : msg.to; const newId = isParticipant ? msg.recipients[0] : msg.to;
const oldId = isParticipant ? msg.author : msg.templateParams.find(id => id !== newId); const oldId = isParticipant ? msg.author : msg.templateParams.find(id => id !== newId);
/** /**
* Emitted when a contact or a group participant changes their phone number. * Emitted when a contact or a group participant changes their phone number.
* @event Client#contact_changed * @event Client#contact_changed
* @param {Message} message Message with more information about the event. * @param {Message} message Message with more information about the event.
@ -496,98 +482,98 @@ class Client extends EventEmitter {
* @param {String} newId The user's new id after the change. * @param {String} newId The user's new id after the change.
* @param {Boolean} isContact Indicates if a contact or a group participant changed their phone number. * @param {Boolean} isContact Indicates if a contact or a group participant changed their phone number.
*/ */
this.emit(Events.CONTACT_CHANGED, message, oldId, newId, isContact); this.emit(Events.CONTACT_CHANGED, message, oldId, newId, isContact);
} }
}); });
await this.pupPage.exposeFunction('onRemoveMessageEvent', (msg) => { await exposeFunctionIfAbsent(this.pupPage, 'onRemoveMessageEvent', (msg) => {
if (!msg.isNewMsg) return; if (!msg.isNewMsg) return;
const message = new Message(this, msg); const message = new Message(this, msg);
/** /**
* Emitted when a message is deleted by the current user. * Emitted when a message is deleted by the current user.
* @event Client#message_revoke_me * @event Client#message_revoke_me
* @param {Message} message The message that was revoked * @param {Message} message The message that was revoked
*/ */
this.emit(Events.MESSAGE_REVOKED_ME, message); this.emit(Events.MESSAGE_REVOKED_ME, message);
}); });
await this.pupPage.exposeFunction('onMessageAckEvent', (msg, ack) => { await exposeFunctionIfAbsent(this.pupPage, 'onMessageAckEvent', (msg, ack) => {
const message = new Message(this, msg); const message = new Message(this, msg);
/** /**
* Emitted when an ack event occurrs on message type. * Emitted when an ack event occurrs on message type.
* @event Client#message_ack * @event Client#message_ack
* @param {Message} message The message that was affected * @param {Message} message The message that was affected
* @param {MessageAck} ack The new ACK value * @param {MessageAck} ack The new ACK value
*/ */
this.emit(Events.MESSAGE_ACK, message, ack); this.emit(Events.MESSAGE_ACK, message, ack);
}); });
await this.pupPage.exposeFunction('onChatUnreadCountEvent', async (data) =>{ await exposeFunctionIfAbsent(this.pupPage, 'onChatUnreadCountEvent', async (data) =>{
const chat = await this.getChatById(data.id); const chat = await this.getChatById(data.id);
/** /**
* Emitted when the chat unread count changes * Emitted when the chat unread count changes
*/ */
this.emit(Events.UNREAD_COUNT, chat); this.emit(Events.UNREAD_COUNT, chat);
}); });
await this.pupPage.exposeFunction('onMessageMediaUploadedEvent', (msg) => { await exposeFunctionIfAbsent(this.pupPage, 'onMessageMediaUploadedEvent', (msg) => {
const message = new Message(this, msg); const message = new Message(this, msg);
/** /**
* Emitted when media has been uploaded for a message sent by the client. * Emitted when media has been uploaded for a message sent by the client.
* @event Client#media_uploaded * @event Client#media_uploaded
* @param {Message} message The message with media that was uploaded * @param {Message} message The message with media that was uploaded
*/ */
this.emit(Events.MEDIA_UPLOADED, message); this.emit(Events.MEDIA_UPLOADED, message);
}); });
await this.pupPage.exposeFunction('onAppStateChangedEvent', async (state) => { await exposeFunctionIfAbsent(this.pupPage, 'onAppStateChangedEvent', async (state) => {
/** /**
* Emitted when the connection state changes * Emitted when the connection state changes
* @event Client#change_state * @event Client#change_state
* @param {WAState} state the new connection state * @param {WAState} state the new connection state
*/ */
this.emit(Events.STATE_CHANGED, state); this.emit(Events.STATE_CHANGED, state);
const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT]; const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT];
if (this.options.takeoverOnConflict) { if (this.options.takeoverOnConflict) {
ACCEPTED_STATES.push(WAState.CONFLICT); ACCEPTED_STATES.push(WAState.CONFLICT);
if (state === WAState.CONFLICT) { if (state === WAState.CONFLICT) {
setTimeout(() => { setTimeout(() => {
this.pupPage.evaluate(() => window.Store.AppState.takeover()); this.pupPage.evaluate(() => window.Store.AppState.takeover());
}, this.options.takeoverTimeoutMs); }, this.options.takeoverTimeoutMs);
}
} }
}
if (!ACCEPTED_STATES.includes(state)) { if (!ACCEPTED_STATES.includes(state)) {
/** /**
* Emitted when the client has been disconnected * Emitted when the client has been disconnected
* @event Client#disconnected * @event Client#disconnected
* @param {WAState|"LOGOUT"} reason reason that caused the disconnect * @param {WAState|"LOGOUT"} reason reason that caused the disconnect
*/ */
await this.authStrategy.disconnect(); await this.authStrategy.disconnect();
this.emit(Events.DISCONNECTED, state); this.emit(Events.DISCONNECTED, state);
this.destroy(); this.destroy();
} }
}); });
await this.pupPage.exposeFunction('onBatteryStateChangedEvent', (state) => { await exposeFunctionIfAbsent(this.pupPage, 'onBatteryStateChangedEvent', (state) => {
const { battery, plugged } = state; const { battery, plugged } = state;
if (battery === undefined) return; if (battery === undefined) return;
/** /**
* Emitted when the battery percentage for the attached device changes. Will not be sent if using multi-device. * 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
@ -595,11 +581,11 @@ class Client extends EventEmitter {
* @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 * @deprecated
*/ */
this.emit(Events.BATTERY_CHANGED, { battery, plugged }); this.emit(Events.BATTERY_CHANGED, { battery, plugged });
}); });
await this.pupPage.exposeFunction('onIncomingCall', (call) => { await exposeFunctionIfAbsent(this.pupPage, 'onIncomingCall', (call) => {
/** /**
* Emitted when a call is received * Emitted when a call is received
* @event Client#incoming_call * @event Client#incoming_call
* @param {object} call * @param {object} call
@ -612,13 +598,13 @@ class Client extends EventEmitter {
* @param {boolean} call.webClientShouldHandle - If Waweb should handle * @param {boolean} call.webClientShouldHandle - If Waweb should handle
* @param {object} call.participants - Participants * @param {object} call.participants - Participants
*/ */
const cll = new Call(this, call); const cll = new Call(this, call);
this.emit(Events.INCOMING_CALL, cll); this.emit(Events.INCOMING_CALL, cll);
}); });
await this.pupPage.exposeFunction('onReaction', (reactions) => { await exposeFunctionIfAbsent(this.pupPage, 'onReaction', (reactions) => {
for (const reaction of reactions) { for (const reaction of reactions) {
/** /**
* Emitted when a reaction is sent, received, updated or removed * Emitted when a reaction is sent, received, updated or removed
* @event Client#message_reaction * @event Client#message_reaction
* @param {object} reaction * @param {object} reaction
@ -633,69 +619,68 @@ class Client extends EventEmitter {
* @param {?number} reaction.ack - Ack * @param {?number} reaction.ack - Ack
*/ */
this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction)); this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction));
} }
}); });
await this.pupPage.exposeFunction('onRemoveChatEvent', async (chat) => { await exposeFunctionIfAbsent(this.pupPage, 'onRemoveChatEvent', async (chat) => {
const _chat = await this.getChatById(chat.id); const _chat = await this.getChatById(chat.id);
/** /**
* Emitted when a chat is removed * Emitted when a chat is removed
* @event Client#chat_removed * @event Client#chat_removed
* @param {Chat} chat * @param {Chat} chat
*/ */
this.emit(Events.CHAT_REMOVED, _chat); this.emit(Events.CHAT_REMOVED, _chat);
}); });
await this.pupPage.exposeFunction('onArchiveChatEvent', async (chat, currState, prevState) => { await exposeFunctionIfAbsent(this.pupPage, 'onArchiveChatEvent', async (chat, currState, prevState) => {
const _chat = await this.getChatById(chat.id); const _chat = await this.getChatById(chat.id);
/** /**
* Emitted when a chat is archived/unarchived * Emitted when a chat is archived/unarchived
* @event Client#chat_archived * @event Client#chat_archived
* @param {Chat} chat * @param {Chat} chat
* @param {boolean} currState * @param {boolean} currState
* @param {boolean} prevState * @param {boolean} prevState
*/ */
this.emit(Events.CHAT_ARCHIVED, _chat, currState, prevState); this.emit(Events.CHAT_ARCHIVED, _chat, currState, prevState);
}); });
await this.pupPage.exposeFunction('onEditMessageEvent', (msg, newBody, prevBody) => { await exposeFunctionIfAbsent(this.pupPage, 'onEditMessageEvent', (msg, newBody, prevBody) => {
if(msg.type === 'revoked'){ if(msg.type === 'revoked'){
return; return;
} }
/** /**
* Emitted when messages are edited * Emitted when messages are edited
* @event Client#message_edit * @event Client#message_edit
* @param {Message} message * @param {Message} message
* @param {string} newBody * @param {string} newBody
* @param {string} prevBody * @param {string} prevBody
*/ */
this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody); this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody);
}); });
await this.pupPage.exposeFunction('onAddMessageCiphertextEvent', msg => { await exposeFunctionIfAbsent(this.pupPage, 'onAddMessageCiphertextEvent', msg => {
/** /**
* Emitted when messages are edited * Emitted when messages are edited
* @event Client#message_ciphertext * @event Client#message_ciphertext
* @param {Message} message * @param {Message} message
*/ */
this.emit(Events.MESSAGE_CIPHERTEXT, new Message(this, msg)); this.emit(Events.MESSAGE_CIPHERTEXT, new Message(this, msg));
}); });
await this.pupPage.exposeFunction('onPollVoteEvent', (vote) => { await exposeFunctionIfAbsent(this.pupPage, 'onPollVoteEvent', (vote) => {
const _vote = new PollVote(this, vote); const _vote = new PollVote(this, vote);
/** /**
* Emitted when some poll option is selected or deselected, * Emitted when some poll option is selected or deselected,
* shows a user's current selected option(s) on the poll * shows a user's current selected option(s) on the poll
* @event Client#vote_update * @event Client#vote_update
*/ */
this.emit(Events.VOTE_UPDATE, _vote); this.emit(Events.VOTE_UPDATE, _vote);
}); });
}
await this.pupPage.evaluate(() => { await this.pupPage.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)); });

23
src/util/Puppeteer.js Normal file
View file

@ -0,0 +1,23 @@
/**
* Expose a function to the page if it does not exist
*
* NOTE:
* Rewrite it to 'upsertFunction' after updating Puppeteer to 20.6 or higher
* using page.removeExposedFunction
* https://pptr.dev/api/puppeteer.page.removeExposedFunction
*
* @param {import(puppeteer).Page} page
* @param {string} name
* @param {Function} fn
*/
async function exposeFunctionIfAbsent(page, name, fn) {
const exist = await page.evaluate((name) => {
return !!window[name];
}, name);
if (exist) {
return;
}
await page.exposeFunction(name, fn);
}
module.exports = {exposeFunctionIfAbsent};