Advertisement
CzarSys

wbjs

Oct 11th, 2023
1,452
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. 'use strict';
  2.  
  3. const EventEmitter = require('events');
  4. const puppeteer = require('puppeteer');
  5. const moduleRaid = require('@pedroslopez/moduleraid/moduleraid');
  6.  
  7. const Util = require('./util/Util');
  8. const InterfaceController = require('./util/InterfaceController');
  9. const { WhatsWebURL, DefaultOptions, Events, WAState } = require('./util/Constants');
  10. const { ExposeStore, LoadUtils } = require('./util/Injected');
  11. const ChatFactory = require('./factories/ChatFactory');
  12. const ContactFactory = require('./factories/ContactFactory');
  13. const WebCacheFactory = require('./webCache/WebCacheFactory');
  14. const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List, Reaction, Chat } = require('./structures');
  15. const LegacySessionAuth = require('./authStrategies/LegacySessionAuth');
  16. const NoAuth = require('./authStrategies/NoAuth');
  17.  
  18. /**
  19.  * Starting point for interacting with the WhatsApp Web API
  20.  * @extends {EventEmitter}
  21.  * @param {object} options - Client options
  22.  * @param {AuthStrategy} options.authStrategy - Determines how to save and restore sessions. Will use LegacySessionAuth if options.session is set. Otherwise, NoAuth will be used.
  23.  * @param {string} options.webVersion - The version of WhatsApp Web to use. Use options.webVersionCache to configure how the version is retrieved.
  24.  * @param {object} options.webVersionCache - Determines how to retrieve the WhatsApp Web version. Defaults to a local cache (LocalWebCache) that falls back to latest if the requested version is not found.
  25.  * @param {number} options.authTimeoutMs - Timeout for authentication selector in puppeteer
  26.  * @param {object} options.puppeteer - Puppeteer launch options. View docs here: https://github.com/puppeteer/puppeteer/
  27.  * @param {number} options.qrMaxRetries - How many times should the qrcode be refreshed before giving up
  28.  * @param {string} options.restartOnAuthFail  - @deprecated This option should be set directly on the LegacySessionAuth.
  29.  * @param {object} options.session - @deprecated Only here for backwards-compatibility. You should move to using LocalAuth, or set the authStrategy to LegacySessionAuth explicitly.
  30.  * @param {number} options.takeoverOnConflict - If another whatsapp web session is detected (another browser), take over the session in the current browser
  31.  * @param {number} options.takeoverTimeoutMs - How much time to wait before taking over the session
  32.  * @param {string} options.userAgent - User agent to use in puppeteer
  33.  * @param {string} options.ffmpegPath - Ffmpeg path to use when formating videos to webp while sending stickers
  34.  * @param {boolean} options.bypassCSP - Sets bypassing of page's Content-Security-Policy.
  35.  * @param {object} options.proxyAuthentication - Proxy Authentication object.
  36.  *
  37.  * @fires Client#qr
  38.  * @fires Client#authenticated
  39.  * @fires Client#auth_failure
  40.  * @fires Client#ready
  41.  * @fires Client#message
  42.  * @fires Client#message_ack
  43.  * @fires Client#message_create
  44.  * @fires Client#message_revoke_me
  45.  * @fires Client#message_revoke_everyone
  46.  * @fires Client#media_uploaded
  47.  * @fires Client#group_join
  48.  * @fires Client#group_leave
  49.  * @fires Client#group_update
  50.  * @fires Client#disconnected
  51.  * @fires Client#change_state
  52.  * @fires Client#contact_changed
  53.  * @fires Client#group_admin_changed
  54.  */
  55. class Client extends EventEmitter {
  56.     constructor(options = {}) {
  57.         super();
  58.  
  59.         this.options = Util.mergeDefault(DefaultOptions, options);
  60.        
  61.         if(!this.options.authStrategy) {
  62.             if(Object.prototype.hasOwnProperty.call(this.options, 'session')) {
  63.                 process.emitWarning(
  64.                     'options.session is deprecated and will be removed in a future release due to incompatibility with multi-device. ' +
  65.                     'Use the LocalAuth authStrategy, don\'t pass in a session as an option, or suppress this warning by using the LegacySessionAuth strategy explicitly (see https://wwebjs.dev/guide/authentication.html#legacysessionauth-strategy).',
  66.                     'DeprecationWarning'
  67.                 );
  68.  
  69.                 this.authStrategy = new LegacySessionAuth({
  70.                     session: this.options.session,
  71.                     restartOnAuthFail: this.options.restartOnAuthFail
  72.                 });
  73.             } else {
  74.                 this.authStrategy = new NoAuth();
  75.             }
  76.         } else {
  77.             this.authStrategy = this.options.authStrategy;
  78.         }
  79.  
  80.         this.authStrategy.setup(this);
  81.  
  82.         this.pupBrowser = null;
  83.         this.pupPage = null;
  84.  
  85.         Util.setFfmpegPath(this.options.ffmpegPath);
  86.     }
  87.  
  88.     /**
  89.      * Sets up events and requirements, kicks off authentication request
  90.      */
  91.     async initialize() {
  92.         let [browser, page] = [null, null];
  93.  
  94.         await this.authStrategy.beforeBrowserInitialized();
  95.  
  96.         const puppeteerOpts = this.options.puppeteer;
  97.         if (puppeteerOpts && puppeteerOpts.browserWSEndpoint) {
  98.             browser = await puppeteer.connect(puppeteerOpts);
  99.             page = await browser.newPage();
  100.         } else {
  101.             const browserArgs = [...(puppeteerOpts.args || [])];
  102.             if(!browserArgs.find(arg => arg.includes('--user-agent'))) {
  103.                 browserArgs.push(`--user-agent=${this.options.userAgent}`);
  104.             }
  105.  
  106.             browser = await puppeteer.launch({...puppeteerOpts, args: browserArgs});
  107.             page = (await browser.pages())[0];
  108.         }
  109.  
  110.         if (this.options.proxyAuthentication !== undefined) {
  111.             await page.authenticate(this.options.proxyAuthentication);
  112.         }
  113.      
  114.         await page.setUserAgent(this.options.userAgent);
  115.         if (this.options.bypassCSP) await page.setBypassCSP(true);
  116.  
  117.         this.pupBrowser = browser;
  118.         this.pupPage = page;
  119.  
  120.         await this.authStrategy.afterBrowserInitialized();
  121.         await this.initWebVersionCache();
  122.  
  123.         await page.goto(WhatsWebURL, {
  124.             waitUntil: 'load',
  125.             timeout: 0,
  126.             referer: 'https://whatsapp.com/'
  127.         });
  128.  
  129.         await page.evaluate(`function getElementByXpath(path) {
  130.             return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  131.           }`);
  132.  
  133.         let lastPercent = null,
  134.             lastPercentMessage = null;
  135.  
  136.         await page.exposeFunction('loadingScreen', async (percent, message) => {
  137.             if (lastPercent !== percent || lastPercentMessage !== message) {
  138.                 this.emit(Events.LOADING_SCREEN, percent, message);
  139.                 lastPercent = percent;
  140.                 lastPercentMessage = message;
  141.             }
  142.         });
  143.  
  144.         await page.evaluate(
  145.             async function (selectors) {
  146.                 var observer = new MutationObserver(function () {
  147.                     let progressBar = window.getElementByXpath(
  148.                         selectors.PROGRESS
  149.                     );
  150.                     let progressMessage = window.getElementByXpath(
  151.                         selectors.PROGRESS_MESSAGE
  152.                     );
  153.  
  154.                     if (progressBar) {
  155.                         window.loadingScreen(
  156.                             progressBar.value,
  157.                             progressMessage.innerText
  158.                         );
  159.                     }
  160.                 });
  161.  
  162.                 observer.observe(document, {
  163.                     attributes: true,
  164.                     childList: true,
  165.                     characterData: true,
  166.                     subtree: true,
  167.                 });
  168.             },
  169.             {
  170.                 PROGRESS: '//*[@id=\'app\']/div/div/div[2]/progress',
  171.                 PROGRESS_MESSAGE: '//*[@id=\'app\']/div/div/div[3]',
  172.             }
  173.         );
  174.  
  175.         const INTRO_IMG_SELECTOR = '[data-icon=\'search\']';
  176.         const INTRO_QRCODE_SELECTOR = 'div[data-ref] canvas';
  177.  
  178.         // Checks which selector appears first
  179.         const needAuthentication = await Promise.race([
  180.             new Promise(resolve => {
  181.                 page.waitForSelector(INTRO_IMG_SELECTOR, { timeout: this.options.authTimeoutMs })
  182.                     .then(() => resolve(false))
  183.                     .catch((err) => resolve(err));
  184.             }),
  185.             new Promise(resolve => {
  186.                 page.waitForSelector(INTRO_QRCODE_SELECTOR, { timeout: this.options.authTimeoutMs })
  187.                     .then(() => resolve(true))
  188.                     .catch((err) => resolve(err));
  189.             })
  190.         ]);
  191.  
  192.         // Checks if an error occurred on the first found selector. The second will be discarded and ignored by .race;
  193.         if (needAuthentication instanceof Error) throw needAuthentication;
  194.  
  195.         // Scan-qrcode selector was found. Needs authentication
  196.         if (needAuthentication) {
  197.             const { failed, failureEventPayload, restart } = await this.authStrategy.onAuthenticationNeeded();
  198.             if(failed) {
  199.                 /**
  200.                  * Emitted when there has been an error while trying to restore an existing session
  201.                  * @event Client#auth_failure
  202.                  * @param {string} message
  203.                  */
  204.                 this.emit(Events.AUTHENTICATION_FAILURE, failureEventPayload);
  205.                 await this.destroy();
  206.                 if (restart) {
  207.                     // session restore failed so try again but without session to force new authentication
  208.                     return this.initialize();
  209.                 }
  210.                 return;
  211.             }
  212.  
  213.             const QR_CONTAINER = 'div[data-ref]';
  214.             const QR_RETRY_BUTTON = 'div[data-ref] > span > button';
  215.             let qrRetries = 0;
  216.             await page.exposeFunction('qrChanged', async (qr) => {
  217.                 /**
  218.                 * Emitted when a QR code is received
  219.                 * @event Client#qr
  220.                 * @param {string} qr QR Code
  221.                 */
  222.                 this.emit(Events.QR_RECEIVED, qr);
  223.                 if (this.options.qrMaxRetries > 0) {
  224.                     qrRetries++;
  225.                     if (qrRetries > this.options.qrMaxRetries) {
  226.                         this.emit(Events.DISCONNECTED, 'Max qrcode retries reached');
  227.                         await this.destroy();
  228.                     }
  229.                 }
  230.             });
  231.  
  232.             await page.evaluate(function (selectors) {
  233.                 const qr_container = document.querySelector(selectors.QR_CONTAINER);
  234.                 window.qrChanged(qr_container.dataset.ref);
  235.  
  236.                 const obs = new MutationObserver((muts) => {
  237.                     muts.forEach(mut => {
  238.                         // Listens to qr token change
  239.                         if (mut.type === 'attributes' && mut.attributeName === 'data-ref') {
  240.                             window.qrChanged(mut.target.dataset.ref);
  241.                         }
  242.                         // Listens to retry button, when found, click it
  243.                         else if (mut.type === 'childList') {
  244.                             const retry_button = document.querySelector(selectors.QR_RETRY_BUTTON);
  245.                             if (retry_button) retry_button.click();
  246.                         }
  247.                     });
  248.                 });
  249.                 obs.observe(qr_container.parentElement, {
  250.                     subtree: true,
  251.                     childList: true,
  252.                     attributes: true,
  253.                     attributeFilter: ['data-ref'],
  254.                 });
  255.             }, {
  256.                 QR_CONTAINER,
  257.                 QR_RETRY_BUTTON
  258.             });
  259.  
  260.             // Wait for code scan
  261.             try {
  262.                 await page.waitForSelector(INTRO_IMG_SELECTOR, { timeout: 0 });
  263.             } catch(error) {
  264.                 if (
  265.                     error.name === 'ProtocolError' &&
  266.                     error.message &&
  267.                     error.message.match(/Target closed/)
  268.                 ) {
  269.                     // something has called .destroy() while waiting
  270.                     return;
  271.                 }
  272.  
  273.                 throw error;
  274.             }
  275.  
  276.         }
  277.  
  278.         await page.evaluate(() => {
  279.             /**
  280.              * Helper function that compares between two WWeb versions. Its purpose is to help the developer to choose the correct code implementation depending on the comparison value and the WWeb version.
  281.              * @param {string} lOperand The left operand for the WWeb version string to compare with
  282.              * @param {string} operator The comparison operator
  283.              * @param {string} rOperand The right operand for the WWeb version string to compare with
  284.              * @returns {boolean} Boolean value that indicates the result of the comparison
  285.              */
  286.             window.compareWwebVersions = (lOperand, operator, rOperand) => {
  287.                 if (!['>', '>=', '<', '<=', '='].includes(operator)) {
  288.                     throw class _ extends Error {
  289.                         constructor(m) { super(m); this.name = 'CompareWwebVersionsError'; }
  290.                     }('Invalid comparison operator is provided');
  291.  
  292.                 }
  293.                 if (typeof lOperand !== 'string' || typeof rOperand !== 'string') {
  294.                     throw class _ extends Error {
  295.                         constructor(m) { super(m); this.name = 'CompareWwebVersionsError'; }
  296.                     }('A non-string WWeb version type is provided');
  297.                 }
  298.  
  299.                 lOperand = lOperand.replace(/-beta$/, '');
  300.                 rOperand = rOperand.replace(/-beta$/, '');
  301.  
  302.                 while (lOperand.length !== rOperand.length) {
  303.                     lOperand.length > rOperand.length
  304.                         ? rOperand = rOperand.concat('0')
  305.                         : lOperand = lOperand.concat('0');
  306.                 }
  307.  
  308.                 lOperand = Number(lOperand.replace(/\./g, ''));
  309.                 rOperand = Number(rOperand.replace(/\./g, ''));
  310.  
  311.                 return (
  312.                     operator === '>' ? lOperand > rOperand :
  313.                         operator === '>=' ? lOperand >= rOperand :
  314.                             operator === '<' ? lOperand < rOperand :
  315.                                 operator === '<=' ? lOperand <= rOperand :
  316.                                     operator === '=' ? lOperand === rOperand :
  317.                                         false
  318.                 );
  319.             };
  320.         });
  321.  
  322.         await page.evaluate(ExposeStore, moduleRaid.toString());
  323.         const authEventPayload = await this.authStrategy.getAuthEventPayload();
  324.  
  325.         /**
  326.          * Emitted when authentication is successful
  327.          * @event Client#authenticated
  328.          */
  329.         this.emit(Events.AUTHENTICATED, authEventPayload);
  330.  
  331.         // Check window.Store Injection
  332.         await page.waitForFunction('window.Store != undefined');
  333.  
  334.         await page.evaluate(async () => {
  335.             // safely unregister service workers
  336.             const registrations = await navigator.serviceWorker.getRegistrations();
  337.             for (let registration of registrations) {
  338.                 registration.unregister();
  339.             }
  340.         });
  341.  
  342.         //Load util functions (serializers, helper functions)
  343.         await page.evaluate(LoadUtils);
  344.  
  345.         // Expose client info
  346.         /**
  347.          * Current connection information
  348.          * @type {ClientInfo}
  349.          */
  350.         this.info = new ClientInfo(this, await page.evaluate(() => {
  351.             return { ...window.Store.Conn.serialize(), wid: window.Store.User.getMeUser() };
  352.         }));
  353.  
  354.         // Add InterfaceController
  355.         this.interface = new InterfaceController(this);
  356.  
  357.         // Register events
  358.         await page.exposeFunction('onAddMessageEvent', msg => {
  359.             if (msg.type === 'gp2') {
  360.                 const notification = new GroupNotification(this, msg);
  361.                 if (['add', 'invite', 'linked_group_join'].includes(msg.subtype)) {
  362.                     /**
  363.                      * Emitted when a user joins the chat via invite link or is added by an admin.
  364.                      * @event Client#group_join
  365.                      * @param {GroupNotification} notification GroupNotification with more information about the action
  366.                      */
  367.                     this.emit(Events.GROUP_JOIN, notification);
  368.                 } else if (msg.subtype === 'remove' || msg.subtype === 'leave') {
  369.                     /**
  370.                      * Emitted when a user leaves the chat or is removed by an admin.
  371.                      * @event Client#group_leave
  372.                      * @param {GroupNotification} notification GroupNotification with more information about the action
  373.                      */
  374.                     this.emit(Events.GROUP_LEAVE, notification);
  375.                 } else if (msg.subtype === 'promote' || msg.subtype === 'demote') {
  376.                     /**
  377.                      * Emitted when a current user is promoted to an admin or demoted to a regular user.
  378.                      * @event Client#group_admin_changed
  379.                      * @param {GroupNotification} notification GroupNotification with more information about the action
  380.                      */
  381.                     this.emit(Events.GROUP_ADMIN_CHANGED, notification);
  382.                 } else {
  383.                     /**
  384.                      * Emitted when group settings are updated, such as subject, description or picture.
  385.                      * @event Client#group_update
  386.                      * @param {GroupNotification} notification GroupNotification with more information about the action
  387.                      */
  388.                     this.emit(Events.GROUP_UPDATE, notification);
  389.                 }
  390.                 return;
  391.             }
  392.  
  393.             const message = new Message(this, msg);
  394.  
  395.             /**
  396.              * Emitted when a new message is created, which may include the current user's own messages.
  397.              * @event Client#message_create
  398.              * @param {Message} message The message that was created
  399.              */
  400.             this.emit(Events.MESSAGE_CREATE, message);
  401.  
  402.             if (msg.id.fromMe) return;
  403.  
  404.             /**
  405.              * Emitted when a new message is received.
  406.              * @event Client#message
  407.              * @param {Message} message The message that was received
  408.              */
  409.             this.emit(Events.MESSAGE_RECEIVED, message);
  410.         });
  411.  
  412.         let last_message;
  413.  
  414.         await page.exposeFunction('onChangeMessageTypeEvent', (msg) => {
  415.  
  416.             if (msg.type === 'revoked') {
  417.                 const message = new Message(this, msg);
  418.                 let revoked_msg;
  419.                 if (last_message && msg.id.id === last_message.id.id) {
  420.                     revoked_msg = new Message(this, last_message);
  421.                 }
  422.  
  423.                 /**
  424.                  * Emitted when a message is deleted for everyone in the chat.
  425.                  * @event Client#message_revoke_everyone
  426.                  * @param {Message} message The message that was revoked, in its current state. It will not contain the original message's data.
  427.                  * @param {?Message} revoked_msg The message that was revoked, before it was revoked. It will contain the message's original data.
  428.                  * Note that due to the way this data is captured, it may be possible that this param will be undefined.
  429.                  */
  430.                 this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg);
  431.             }
  432.  
  433.         });
  434.  
  435.         await page.exposeFunction('onChangeMessageEvent', (msg) => {
  436.  
  437.             if (msg.type !== 'revoked') {
  438.                 last_message = msg;
  439.             }
  440.  
  441.             /**
  442.              * The event notification that is received when one of
  443.              * the group participants changes their phone number.
  444.              */
  445.             const isParticipant = msg.type === 'gp2' && msg.subtype === 'modify';
  446.  
  447.             /**
  448.              * The event notification that is received when one of
  449.              * the contacts changes their phone number.
  450.              */
  451.             const isContact = msg.type === 'notification_template' && msg.subtype === 'change_number';
  452.  
  453.             if (isParticipant || isContact) {
  454.                 /** @type {GroupNotification} object does not provide enough information about this event, so a @type {Message} object is used. */
  455.                 const message = new Message(this, msg);
  456.  
  457.                 const newId = isParticipant ? msg.recipients[0] : msg.to;
  458.                 const oldId = isParticipant ? msg.author : msg.templateParams.find(id => id !== newId);
  459.  
  460.                 /**
  461.                  * Emitted when a contact or a group participant changes their phone number.
  462.                  * @event Client#contact_changed
  463.                  * @param {Message} message Message with more information about the event.
  464.                  * @param {String} oldId The user's id (an old one) who changed their phone number
  465.                  * and who triggered the notification.
  466.                  * @param {String} newId The user's new id after the change.
  467.                  * @param {Boolean} isContact Indicates if a contact or a group participant changed their phone number.
  468.                  */
  469.                 this.emit(Events.CONTACT_CHANGED, message, oldId, newId, isContact);
  470.             }
  471.         });
  472.  
  473.         await page.exposeFunction('onRemoveMessageEvent', (msg) => {
  474.  
  475.             if (!msg.isNewMsg) return;
  476.  
  477.             const message = new Message(this, msg);
  478.  
  479.             /**
  480.              * Emitted when a message is deleted by the current user.
  481.              * @event Client#message_revoke_me
  482.              * @param {Message} message The message that was revoked
  483.              */
  484.             this.emit(Events.MESSAGE_REVOKED_ME, message);
  485.  
  486.         });
  487.  
  488.         await page.exposeFunction('onMessageAckEvent', (msg, ack) => {
  489.  
  490.             const message = new Message(this, msg);
  491.  
  492.             /**
  493.              * Emitted when an ack event occurrs on message type.
  494.              * @event Client#message_ack
  495.              * @param {Message} message The message that was affected
  496.              * @param {MessageAck} ack The new ACK value
  497.              */
  498.             this.emit(Events.MESSAGE_ACK, message, ack);
  499.  
  500.         });
  501.  
  502.         await page.exposeFunction('onChatUnreadCountEvent', async (data) =>{
  503.             const chat = await this.getChatById(data.id);
  504.            
  505.             /**
  506.              * Emitted when the chat unread count changes
  507.              */
  508.             this.emit(Events.UNREAD_COUNT, chat);
  509.         });
  510.  
  511.         await page.exposeFunction('onMessageMediaUploadedEvent', (msg) => {
  512.  
  513.             const message = new Message(this, msg);
  514.  
  515.             /**
  516.              * Emitted when media has been uploaded for a message sent by the client.
  517.              * @event Client#media_uploaded
  518.              * @param {Message} message The message with media that was uploaded
  519.              */
  520.             this.emit(Events.MEDIA_UPLOADED, message);
  521.         });
  522.  
  523.         await page.exposeFunction('onAppStateChangedEvent', async (state) => {
  524.  
  525.             /**
  526.              * Emitted when the connection state changes
  527.              * @event Client#change_state
  528.              * @param {WAState} state the new connection state
  529.              */
  530.             this.emit(Events.STATE_CHANGED, state);
  531.  
  532.             const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT];
  533.  
  534.             if (this.options.takeoverOnConflict) {
  535.                 ACCEPTED_STATES.push(WAState.CONFLICT);
  536.  
  537.                 if (state === WAState.CONFLICT) {
  538.                     setTimeout(() => {
  539.                         this.pupPage.evaluate(() => window.Store.AppState.takeover());
  540.                     }, this.options.takeoverTimeoutMs);
  541.                 }
  542.             }
  543.  
  544.             if (!ACCEPTED_STATES.includes(state)) {
  545.                 /**
  546.                  * Emitted when the client has been disconnected
  547.                  * @event Client#disconnected
  548.                  * @param {WAState|"NAVIGATION"} reason reason that caused the disconnect
  549.                  */
  550.                 await this.authStrategy.disconnect();
  551.                 this.emit(Events.DISCONNECTED, state);
  552.                 this.destroy();
  553.             }
  554.         });
  555.  
  556.         await page.exposeFunction('onBatteryStateChangedEvent', (state) => {
  557.             const { battery, plugged } = state;
  558.  
  559.             if (battery === undefined) return;
  560.  
  561.             /**
  562.              * Emitted when the battery percentage for the attached device changes. Will not be sent if using multi-device.
  563.              * @event Client#change_battery
  564.              * @param {object} batteryInfo
  565.              * @param {number} batteryInfo.battery - The current battery percentage
  566.              * @param {boolean} batteryInfo.plugged - Indicates if the phone is plugged in (true) or not (false)
  567.              * @deprecated
  568.              */
  569.             this.emit(Events.BATTERY_CHANGED, { battery, plugged });
  570.         });
  571.  
  572.         await page.exposeFunction('onIncomingCall', (call) => {
  573.             /**
  574.              * Emitted when a call is received
  575.              * @event Client#incoming_call
  576.              * @param {object} call
  577.              * @param {number} call.id - Call id
  578.              * @param {string} call.peerJid - Who called
  579.              * @param {boolean} call.isVideo - if is video
  580.              * @param {boolean} call.isGroup - if is group
  581.              * @param {boolean} call.canHandleLocally - if we can handle in waweb
  582.              * @param {boolean} call.outgoing - if is outgoing
  583.              * @param {boolean} call.webClientShouldHandle - If Waweb should handle
  584.              * @param {object} call.participants - Participants
  585.              */
  586.             const cll = new Call(this, call);
  587.             this.emit(Events.INCOMING_CALL, cll);
  588.         });
  589.  
  590.         await page.exposeFunction('onReaction', (reactions) => {
  591.             for (const reaction of reactions) {
  592.                 /**
  593.                  * Emitted when a reaction is sent, received, updated or removed
  594.                  * @event Client#message_reaction
  595.                  * @param {object} reaction
  596.                  * @param {object} reaction.id - Reaction id
  597.                  * @param {number} reaction.orphan - Orphan
  598.                  * @param {?string} reaction.orphanReason - Orphan reason
  599.                  * @param {number} reaction.timestamp - Timestamp
  600.                  * @param {string} reaction.reaction - Reaction
  601.                  * @param {boolean} reaction.read - Read
  602.                  * @param {object} reaction.msgId - Parent message id
  603.                  * @param {string} reaction.senderId - Sender id
  604.                  * @param {?number} reaction.ack - Ack
  605.                  */
  606.  
  607.                 this.emit(Events.MESSAGE_REACTION, new Reaction(this, reaction));
  608.             }
  609.         });
  610.  
  611.         await page.exposeFunction('onRemoveChatEvent', (chat) => {
  612.             /**
  613.              * Emitted when a chat is removed
  614.              * @event Client#chat_removed
  615.              * @param {Chat} chat
  616.              */
  617.             this.emit(Events.CHAT_REMOVED, new Chat(this, chat));
  618.         });
  619.        
  620.         await page.exposeFunction('onArchiveChatEvent', (chat, currState, prevState) => {
  621.             /**
  622.              * Emitted when a chat is archived/unarchived
  623.              * @event Client#chat_archived
  624.              * @param {Chat} chat
  625.              * @param {boolean} currState
  626.              * @param {boolean} prevState
  627.              */
  628.             this.emit(Events.CHAT_ARCHIVED, new Chat(this, chat), currState, prevState);
  629.         });
  630.  
  631.         await page.exposeFunction('onEditMessageEvent', (msg, newBody, prevBody) => {
  632.            
  633.             if(msg.type === 'revoked'){
  634.                 return;
  635.             }
  636.             /**
  637.              * Emitted when messages are edited
  638.              * @event Client#message_edit
  639.              * @param {Message} message
  640.              * @param {string} newBody
  641.              * @param {string} prevBody
  642.              */
  643.             this.emit(Events.MESSAGE_EDIT, new Message(this, msg), newBody, prevBody);
  644.         });
  645.  
  646.         await page.evaluate(() => {
  647.             window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(window.WWebJS.getMessageModel(msg)); });
  648.             window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(window.WWebJS.getMessageModel(msg)); });
  649.             window.Store.Msg.on('change:ack', (msg, ack) => { window.onMessageAckEvent(window.WWebJS.getMessageModel(msg), ack); });
  650.             window.Store.Msg.on('change:isUnsentMedia', (msg, unsent) => { if (msg.id.fromMe && !unsent) window.onMessageMediaUploadedEvent(window.WWebJS.getMessageModel(msg)); });
  651.             window.Store.Msg.on('remove', (msg) => { if (msg.isNewMsg) window.onRemoveMessageEvent(window.WWebJS.getMessageModel(msg)); });
  652.             window.Store.Msg.on('change:body', (msg, newBody, prevBody) => { window.onEditMessageEvent(window.WWebJS.getMessageModel(msg), newBody, prevBody); });
  653.             window.Store.AppState.on('change:state', (_AppState, state) => { window.onAppStateChangedEvent(state); });
  654.             window.Store.Conn.on('change:battery', (state) => { window.onBatteryStateChangedEvent(state); });
  655.             window.Store.Call.on('add', (call) => { window.onIncomingCall(call); });
  656.             window.Store.Chat.on('remove', async (chat) => { window.onRemoveChatEvent(await window.WWebJS.getChatModel(chat)); });
  657.             window.Store.Chat.on('change:archive', async (chat, currState, prevState) => { window.onArchiveChatEvent(await window.WWebJS.getChatModel(chat), currState, prevState); });
  658.             window.Store.Msg.on('add', (msg) => {
  659.                 if (msg.isNewMsg) {
  660.                     if(msg.type === 'ciphertext') {
  661.                         // defer message event until ciphertext is resolved (type changed)
  662.                         msg.once('change:type', (_msg) => window.onAddMessageEvent(window.WWebJS.getMessageModel(_msg)));
  663.                     } else {
  664.                         window.onAddMessageEvent(window.WWebJS.getMessageModel(msg));
  665.                     }
  666.                 }
  667.             });
  668.             window.Store.Chat.on('change:unreadCount', (chat) => {window.onChatUnreadCountEvent(chat);});
  669.  
  670.             {
  671.                 const module = window.Store.createOrUpdateReactionsModule;
  672.                 const ogMethod = module.createOrUpdateReactions;
  673.                 module.createOrUpdateReactions = ((...args) => {
  674.                     window.onReaction(args[0].map(reaction => {
  675.                         const msgKey = window.Store.MsgKey.fromString(reaction.msgKey);
  676.                         const parentMsgKey = window.Store.MsgKey.fromString(reaction.parentMsgKey);
  677.                         const timestamp = reaction.timestamp / 1000;
  678.  
  679.                         return {...reaction, msgKey, parentMsgKey, timestamp };
  680.                     }));
  681.  
  682.                     return ogMethod(...args);
  683.                 }).bind(module);
  684.             }
  685.         });
  686.  
  687.         /**
  688.          * Emitted when the client has initialized and is ready to receive messages.
  689.          * @event Client#ready
  690.          */
  691.         this.emit(Events.READY);
  692.         this.authStrategy.afterAuthReady();
  693.  
  694.         // Disconnect when navigating away when in PAIRING state (detect logout)
  695.         this.pupPage.on('framenavigated', async () => {
  696.             const appState = await this.getState();
  697.             if(!appState || appState === WAState.PAIRING) {
  698.                 await this.authStrategy.disconnect();
  699.                 this.emit(Events.DISCONNECTED, 'NAVIGATION');
  700.                 await this.destroy();
  701.             }
  702.         });
  703.     }
  704.  
  705.     async initWebVersionCache() {
  706.         const { type: webCacheType, ...webCacheOptions } = this.options.webVersionCache;
  707.         const webCache = WebCacheFactory.createWebCache(webCacheType, webCacheOptions);
  708.  
  709.         const requestedVersion = this.options.webVersion;
  710.         const versionContent = await webCache.resolve(requestedVersion);
  711.  
  712.         if(versionContent) {
  713.             await this.pupPage.setRequestInterception(true);
  714.             this.pupPage.on('request', async (req) => {
  715.                 if(req.url() === WhatsWebURL) {
  716.                     req.respond({
  717.                         status: 200,
  718.                         contentType: 'text/html',
  719.                         body: versionContent
  720.                     });
  721.                 } else {
  722.                     req.continue();
  723.                 }
  724.             });
  725.         } else {
  726.             this.pupPage.on('response', async (res) => {
  727.                 if(res.ok() && res.url() === WhatsWebURL) {
  728.                     await webCache.persist(await res.text());
  729.                 }
  730.             });
  731.         }
  732.     }
  733.  
  734.     /**
  735.      * Closes the client
  736.      */
  737.     async destroy() {
  738.         await this.pupBrowser.close();
  739.         await this.authStrategy.destroy();
  740.     }
  741.  
  742.     /**
  743.      * Logs out the client, closing the current session
  744.      */
  745.     async logout() {
  746.         await this.pupPage.evaluate(() => {
  747.             return window.Store.AppState.logout();
  748.         });
  749.         await this.pupBrowser.close();
  750.        
  751.         let maxDelay = 0;
  752.         while (this.pupBrowser.isConnected() && (maxDelay < 10)) { // waits a maximum of 1 second before calling the AuthStrategy
  753.             await new Promise(resolve => setTimeout(resolve, 100));
  754.             maxDelay++;
  755.         }
  756.        
  757.         await this.authStrategy.logout();
  758.     }
  759.  
  760.     /**
  761.      * Returns the version of WhatsApp Web currently being run
  762.      * @returns {Promise<string>}
  763.      */
  764.     async getWWebVersion() {
  765.         return await this.pupPage.evaluate(() => {
  766.             return window.Debug.VERSION;
  767.         });
  768.     }
  769.  
  770.     /**
  771.      * Mark as seen for the Chat
  772.      *  @param {string} chatId
  773.      *  @returns {Promise<boolean>} result
  774.      *
  775.      */
  776.     async sendSeen(chatId) {
  777.         const result = await this.pupPage.evaluate(async (chatId) => {
  778.             return window.WWebJS.sendSeen(chatId);
  779.  
  780.         }, chatId);
  781.         return result;
  782.     }
  783.  
  784.     /**
  785.      * Message options.
  786.      * @typedef {Object} MessageSendOptions
  787.      * @property {boolean} [linkPreview=true] - Show links preview. Has no effect on multi-device accounts.
  788.      * @property {boolean} [sendAudioAsVoice=false] - Send audio as voice message with a generated waveform
  789.      * @property {boolean} [sendVideoAsGif=false] - Send video as gif
  790.      * @property {boolean} [sendMediaAsSticker=false] - Send media as a sticker
  791.      * @property {boolean} [sendMediaAsDocument=false] - Send media as a document
  792.      * @property {boolean} [isViewOnce=false] - Send photo/video as a view once message
  793.      * @property {boolean} [parseVCards=true] - Automatically parse vCards and send them as contacts
  794.      * @property {string} [caption] - Image or video caption
  795.      * @property {string} [quotedMessageId] - Id of the message that is being quoted (or replied to)
  796.      * @property {Contact[]} [mentions] - Contacts that are being mentioned in the message
  797.      * @property {boolean} [sendSeen=true] - Mark the conversation as seen after sending the message
  798.      * @property {string} [stickerAuthor=undefined] - Sets the author of the sticker, (if sendMediaAsSticker is true).
  799.      * @property {string} [stickerName=undefined] - Sets the name of the sticker, (if sendMediaAsSticker is true).
  800.      * @property {string[]} [stickerCategories=undefined] - Sets the categories of the sticker, (if sendMediaAsSticker is true). Provide emoji char array, can be null.
  801.      * @property {MessageMedia} [media] - Media to be sent
  802.      */
  803.    
  804.     /**
  805.      * Send a message to a specific chatId
  806.      * @param {string} chatId
  807.      * @param {string|MessageMedia|Location|Contact|Array<Contact>|Buttons|List} content
  808.      * @param {MessageSendOptions} [options] - Options used when sending the message
  809.      *
  810.      * @returns {Promise<Message>} Message that was just sent
  811.      */
  812.     async sendMessage(chatId, content, options = {}) {
  813.         if (options.mentions && options.mentions.some(possiblyContact => possiblyContact instanceof Contact)) {
  814.             console.warn('Mentions with an array of Contact are now deprecated. See more at https://github.com/pedroslopez/whatsapp-web.js/pull/2166.');
  815.             options.mentions = options.mentions.map(a => a.id._serialized);
  816.         }
  817.         let internalOptions = {
  818.             linkPreview: options.linkPreview === false ? undefined : true,
  819.             sendAudioAsVoice: options.sendAudioAsVoice,
  820.             sendVideoAsGif: options.sendVideoAsGif,
  821.             sendMediaAsSticker: options.sendMediaAsSticker,
  822.             sendMediaAsDocument: options.sendMediaAsDocument,
  823.             caption: options.caption,
  824.             quotedMessageId: options.quotedMessageId,
  825.             parseVCards: options.parseVCards === false ? false : true,
  826.             mentionedJidList: Array.isArray(options.mentions) ? options.mentions : [],
  827.             extraOptions: options.extra
  828.         };
  829.  
  830.         const sendSeen = typeof options.sendSeen === 'undefined' ? true : options.sendSeen;
  831.  
  832.         if (content instanceof MessageMedia) {
  833.             internalOptions.attachment = content;
  834.             internalOptions.isViewOnce = options.isViewOnce,
  835.             content = '';
  836.         } else if (options.media instanceof MessageMedia) {
  837.             internalOptions.attachment = options.media;
  838.             internalOptions.caption = content;
  839.             internalOptions.isViewOnce = options.isViewOnce,
  840.             content = '';
  841.         } else if (content instanceof Location) {
  842.             internalOptions.location = content;
  843.             content = '';
  844.         } else if (content instanceof Contact) {
  845.             internalOptions.contactCard = content.id._serialized;
  846.             content = '';
  847.         } else if (Array.isArray(content) && content.length > 0 && content[0] instanceof Contact) {
  848.             internalOptions.contactCardList = content.map(contact => contact.id._serialized);
  849.             content = '';
  850.         } else if (content instanceof Buttons) {
  851.             if (content.type !== 'chat') { internalOptions.attachment = content.body; }
  852.             internalOptions.buttons = content;
  853.             content = '';
  854.         } else if (content instanceof List) {
  855.             internalOptions.list = content;
  856.             content = '';
  857.         }
  858.  
  859.         if (internalOptions.sendMediaAsSticker && internalOptions.attachment) {
  860.             internalOptions.attachment = await Util.formatToWebpSticker(
  861.                 internalOptions.attachment, {
  862.                     name: options.stickerName,
  863.                     author: options.stickerAuthor,
  864.                     categories: options.stickerCategories
  865.                 }, this.pupPage
  866.             );
  867.         }
  868.  
  869.         const newMessage = await this.pupPage.evaluate(async (chatId, message, options, sendSeen) => {
  870.             const chatWid = window.Store.WidFactory.createWid(chatId);
  871.             const chat = await window.Store.Chat.find(chatWid);
  872.  
  873.  
  874.             if (sendSeen) {
  875.                 window.WWebJS.sendSeen(chatId);
  876.             }
  877.  
  878.             const msg = await window.WWebJS.sendMessage(chat, message, options, sendSeen);
  879.             return msg.serialize();
  880.         }, chatId, content, internalOptions, sendSeen);
  881.  
  882.         return new Message(this, newMessage);
  883.     }
  884.    
  885.     /**
  886.      * Searches for messages
  887.      * @param {string} query
  888.      * @param {Object} [options]
  889.      * @param {number} [options.page]
  890.      * @param {number} [options.limit]
  891.      * @param {string} [options.chatId]
  892.      * @returns {Promise<Message[]>}
  893.      */
  894.     async searchMessages(query, options = {}) {
  895.         const messages = await this.pupPage.evaluate(async (query, page, count, remote) => {
  896.             const { messages } = await window.Store.Msg.search(query, page, count, remote);
  897.             return messages.map(msg => window.WWebJS.getMessageModel(msg));
  898.         }, query, options.page, options.limit, options.chatId);
  899.  
  900.         return messages.map(msg => new Message(this, msg));
  901.     }
  902.  
  903.     /**
  904.      * Get all current chat instances
  905.      * @returns {Promise<Array<Chat>>}
  906.      */
  907.     async getChats() {
  908.         let chats = await this.pupPage.evaluate(async () => {
  909.             return await window.WWebJS.getChats();
  910.         });
  911.  
  912.         return chats.map(chat => ChatFactory.create(this, chat));
  913.     }
  914.  
  915.     /**
  916.      * Get chat instance by ID
  917.      * @param {string} chatId
  918.      * @returns {Promise<Chat>}
  919.      */
  920.     async getChatById(chatId) {
  921.         let chat = await this.pupPage.evaluate(async chatId => {
  922.             return await window.WWebJS.getChat(chatId);
  923.         }, chatId);
  924.  
  925.         return ChatFactory.create(this, chat);
  926.     }
  927.  
  928.     /**
  929.      * Get all current contact instances
  930.      * @returns {Promise<Array<Contact>>}
  931.      */
  932.     async getContacts() {
  933.         let contacts = await this.pupPage.evaluate(() => {
  934.             return window.WWebJS.getContacts();
  935.         });
  936.  
  937.         return contacts.map(contact => ContactFactory.create(this, contact));
  938.     }
  939.  
  940.     /**
  941.      * Get contact instance by ID
  942.      * @param {string} contactId
  943.      * @returns {Promise<Contact>}
  944.      */
  945.     async getContactById(contactId) {
  946.         let contact = await this.pupPage.evaluate(contactId => {
  947.             return window.WWebJS.getContact(contactId);
  948.         }, contactId);
  949.  
  950.         return ContactFactory.create(this, contact);
  951.     }
  952.    
  953.     async getMessageById(messageId) {
  954.         const msg = await this.pupPage.evaluate(async messageId => {
  955.             let msg = window.Store.Msg.get(messageId);
  956.             if(msg) return window.WWebJS.getMessageModel(msg);
  957.  
  958.             const params = messageId.split('_');
  959.             if(params.length !== 3) throw new Error('Invalid serialized message id specified');
  960.  
  961.             let messagesObject = await window.Store.Msg.getMessagesById([messageId]);
  962.             if (messagesObject && messagesObject.messages.length) msg = messagesObject.messages[0];
  963.            
  964.             if(msg) return window.WWebJS.getMessageModel(msg);
  965.         }, messageId);
  966.  
  967.         if(msg) return new Message(this, msg);
  968.         return null;
  969.     }
  970.  
  971.     /**
  972.      * Returns an object with information about the invite code's group
  973.      * @param {string} inviteCode
  974.      * @returns {Promise<object>} Invite information
  975.      */
  976.     async getInviteInfo(inviteCode) {
  977.         return await this.pupPage.evaluate(inviteCode => {
  978.             return window.Store.InviteInfo.queryGroupInvite(inviteCode);
  979.         }, inviteCode);
  980.     }
  981.  
  982.     /**
  983.      * Accepts an invitation to join a group
  984.      * @param {string} inviteCode Invitation code
  985.      * @returns {Promise<string>} Id of the joined Chat
  986.      */
  987.     async acceptInvite(inviteCode) {
  988.         const res = await this.pupPage.evaluate(async inviteCode => {
  989.             return await window.Store.Invite.joinGroupViaInvite(inviteCode);
  990.         }, inviteCode);
  991.  
  992.         return res.gid._serialized;
  993.     }
  994.  
  995.     /**
  996.      * Accepts a private invitation to join a group
  997.      * @param {object} inviteInfo Invite V4 Info
  998.      * @returns {Promise<Object>}
  999.      */
  1000.     async acceptGroupV4Invite(inviteInfo) {
  1001.         if (!inviteInfo.inviteCode) throw 'Invalid invite code, try passing the message.inviteV4 object';
  1002.         if (inviteInfo.inviteCodeExp == 0) throw 'Expired invite code';
  1003.         return this.pupPage.evaluate(async inviteInfo => {
  1004.             let { groupId, fromId, inviteCode, inviteCodeExp } = inviteInfo;
  1005.             let userWid = window.Store.WidFactory.createWid(fromId);
  1006.             return await window.Store.JoinInviteV4.joinGroupViaInviteV4(inviteCode, String(inviteCodeExp), groupId, userWid);
  1007.         }, inviteInfo);
  1008.     }
  1009.  
  1010.     /**
  1011.      * Sets the current user's status message
  1012.      * @param {string} status New status message
  1013.      */
  1014.     async setStatus(status) {
  1015.         await this.pupPage.evaluate(async status => {
  1016.             return await window.Store.StatusUtils.setMyStatus(status);
  1017.         }, status);
  1018.     }
  1019.  
  1020.     /**
  1021.      * Sets the current user's display name.
  1022.      * This is the name shown to WhatsApp users that have not added you as a contact beside your number in groups and in your profile.
  1023.      * @param {string} displayName New display name
  1024.      * @returns {Promise<Boolean>}
  1025.      */
  1026.     async setDisplayName(displayName) {
  1027.         const couldSet = await this.pupPage.evaluate(async displayName => {
  1028.             if(!window.Store.Conn.canSetMyPushname()) return false;
  1029.  
  1030.             if(window.Store.MDBackend) {
  1031.                 // TODO
  1032.                 return false;
  1033.             } else {
  1034.                 const res = await window.Store.Wap.setPushname(displayName);
  1035.                 return !res.status || res.status === 200;
  1036.             }
  1037.         }, displayName);
  1038.  
  1039.         return couldSet;
  1040.     }
  1041.    
  1042.     /**
  1043.      * Gets the current connection state for the client
  1044.      * @returns {WAState}
  1045.      */
  1046.     async getState() {
  1047.         return await this.pupPage.evaluate(() => {
  1048.             if(!window.Store) return null;
  1049.             return window.Store.AppState.state;
  1050.         });
  1051.     }
  1052.  
  1053.     /**
  1054.      * Marks the client as online
  1055.      */
  1056.     async sendPresenceAvailable() {
  1057.         return await this.pupPage.evaluate(() => {
  1058.             return window.Store.PresenceUtils.sendPresenceAvailable();
  1059.         });
  1060.     }
  1061.  
  1062.     /**
  1063.      * Marks the client as unavailable
  1064.      */
  1065.     async sendPresenceUnavailable() {
  1066.         return await this.pupPage.evaluate(() => {
  1067.             return window.Store.PresenceUtils.sendPresenceUnavailable();
  1068.         });
  1069.     }
  1070.  
  1071.     /**
  1072.      * Enables and returns the archive state of the Chat
  1073.      * @returns {boolean}
  1074.      */
  1075.     async archiveChat(chatId) {
  1076.         return await this.pupPage.evaluate(async chatId => {
  1077.             let chat = await window.Store.Chat.get(chatId);
  1078.             await window.Store.Cmd.archiveChat(chat, true);
  1079.             return true;
  1080.         }, chatId);
  1081.     }
  1082.  
  1083.     /**
  1084.      * Changes and returns the archive state of the Chat
  1085.      * @returns {boolean}
  1086.      */
  1087.     async unarchiveChat(chatId) {
  1088.         return await this.pupPage.evaluate(async chatId => {
  1089.             let chat = await window.Store.Chat.get(chatId);
  1090.             await window.Store.Cmd.archiveChat(chat, false);
  1091.             return false;
  1092.         }, chatId);
  1093.     }
  1094.  
  1095.     /**
  1096.      * Pins the Chat
  1097.      * @returns {Promise<boolean>} New pin state. Could be false if the max number of pinned chats was reached.
  1098.      */
  1099.     async pinChat(chatId) {
  1100.         return this.pupPage.evaluate(async chatId => {
  1101.             let chat = window.Store.Chat.get(chatId);
  1102.             if (chat.pin) {
  1103.                 return true;
  1104.             }
  1105.             const MAX_PIN_COUNT = 3;
  1106.             const chatModels = window.Store.Chat.getModelsArray();
  1107.             if (chatModels.length > MAX_PIN_COUNT) {
  1108.                 let maxPinned = chatModels[MAX_PIN_COUNT - 1].pin;
  1109.                 if (maxPinned) {
  1110.                     return false;
  1111.                 }
  1112.             }
  1113.             await window.Store.Cmd.pinChat(chat, true);
  1114.             return true;
  1115.         }, chatId);
  1116.     }
  1117.  
  1118.     /**
  1119.      * Unpins the Chat
  1120.      * @returns {Promise<boolean>} New pin state
  1121.      */
  1122.     async unpinChat(chatId) {
  1123.         return this.pupPage.evaluate(async chatId => {
  1124.             let chat = window.Store.Chat.get(chatId);
  1125.             if (!chat.pin) {
  1126.                 return false;
  1127.             }
  1128.             await window.Store.Cmd.pinChat(chat, false);
  1129.             return false;
  1130.         }, chatId);
  1131.     }
  1132.  
  1133.     /**
  1134.      * Mutes this chat forever, unless a date is specified
  1135.      * @param {string} chatId ID of the chat that will be muted
  1136.      * @param {?Date} unmuteDate Date when the chat will be unmuted, leave as is to mute forever
  1137.      */
  1138.     async muteChat(chatId, unmuteDate) {
  1139.         unmuteDate = unmuteDate ? unmuteDate.getTime() / 1000 : -1;
  1140.         await this.pupPage.evaluate(async (chatId, timestamp) => {
  1141.             let chat = await window.Store.Chat.get(chatId);
  1142.             await chat.mute.mute({expiration: timestamp, sendDevice:!0});
  1143.         }, chatId, unmuteDate || -1);
  1144.     }
  1145.  
  1146.     /**
  1147.      * Unmutes the Chat
  1148.      * @param {string} chatId ID of the chat that will be unmuted
  1149.      */
  1150.     async unmuteChat(chatId) {
  1151.         await this.pupPage.evaluate(async chatId => {
  1152.             let chat = await window.Store.Chat.get(chatId);
  1153.             await window.Store.Cmd.muteChat(chat, false);
  1154.         }, chatId);
  1155.     }
  1156.  
  1157.     /**
  1158.      * Mark the Chat as unread
  1159.      * @param {string} chatId ID of the chat that will be marked as unread
  1160.      */
  1161.     async markChatUnread(chatId) {
  1162.         await this.pupPage.evaluate(async chatId => {
  1163.             let chat = await window.Store.Chat.get(chatId);
  1164.             await window.Store.Cmd.markChatUnread(chat, true);
  1165.         }, chatId);
  1166.     }
  1167.  
  1168.     /**
  1169.      * Returns the contact ID's profile picture URL, if privacy settings allow it
  1170.      * @param {string} contactId the whatsapp user's ID
  1171.      * @returns {Promise<string>}
  1172.      */
  1173.     async getProfilePicUrl(contactId) {
  1174.         const profilePic = await this.pupPage.evaluate(async contactId => {
  1175.             try {
  1176.                 const chatWid = window.Store.WidFactory.createWid(contactId);
  1177.                 return await window.Store.ProfilePic.profilePicFind(chatWid);
  1178.             } catch (err) {
  1179.                 if(err.name === 'ServerStatusCodeError') return undefined;
  1180.                 throw err;
  1181.             }
  1182.         }, contactId);
  1183.        
  1184.         return profilePic ? profilePic.eurl : undefined;
  1185.     }
  1186.  
  1187.     /**
  1188.      * Gets the Contact's common groups with you. Returns empty array if you don't have any common group.
  1189.      * @param {string} contactId the whatsapp user's ID (_serialized format)
  1190.      * @returns {Promise<WAWebJS.ChatId[]>}
  1191.      */
  1192.     async getCommonGroups(contactId) {
  1193.         const commonGroups = await this.pupPage.evaluate(async (contactId) => {
  1194.             let contact = window.Store.Contact.get(contactId);
  1195.             if (!contact) {
  1196.                 const wid = window.Store.WidFactory.createUserWid(contactId);
  1197.                 const chatConstructor = window.Store.Contact.getModelsArray().find(c=>!c.isGroup).constructor;
  1198.                 contact = new chatConstructor({id: wid});
  1199.             }
  1200.  
  1201.             if (contact.commonGroups) {
  1202.                 return contact.commonGroups.serialize();
  1203.             }
  1204.             const status = await window.Store.findCommonGroups(contact);
  1205.             if (status) {
  1206.                 return contact.commonGroups.serialize();
  1207.             }
  1208.             return [];
  1209.         }, contactId);
  1210.         const chats = [];
  1211.         for (const group of commonGroups) {
  1212.             chats.push(group.id);
  1213.         }
  1214.         return chats;
  1215.     }
  1216.  
  1217.     /**
  1218.      * Force reset of connection state for the client
  1219.     */
  1220.     async resetState() {
  1221.         await this.pupPage.evaluate(() => {
  1222.             window.Store.AppState.phoneWatchdog.shiftTimer.forceRunNow();
  1223.         });
  1224.     }
  1225.  
  1226.     /**
  1227.      * Check if a given ID is registered in whatsapp
  1228.      * @param {string} id the whatsapp user's ID
  1229.      * @returns {Promise<Boolean>}
  1230.      */
  1231.     async isRegisteredUser(id) {
  1232.         return Boolean(await this.getNumberId(id));
  1233.     }
  1234.  
  1235.     /**
  1236.      * Get the registered WhatsApp ID for a number.
  1237.      * Will return null if the number is not registered on WhatsApp.
  1238.      * @param {string} number Number or ID ("@c.us" will be automatically appended if not specified)
  1239.      * @returns {Promise<Object|null>}
  1240.      */
  1241.     async getNumberId(number) {
  1242.         if (!number.endsWith('@c.us')) {
  1243.             number += '@c.us';
  1244.         }
  1245.  
  1246.         return await this.pupPage.evaluate(async number => {
  1247.             const wid = window.Store.WidFactory.createWid(number);
  1248.             const result = await window.Store.QueryExist(wid);
  1249.             if (!result || result.wid === undefined) return null;
  1250.             return result.wid;
  1251.         }, number);
  1252.     }
  1253.  
  1254.     /**
  1255.      * Get the formatted number of a WhatsApp ID.
  1256.      * @param {string} number Number or ID
  1257.      * @returns {Promise<string>}
  1258.      */
  1259.     async getFormattedNumber(number) {
  1260.         if (!number.endsWith('@s.whatsapp.net')) number = number.replace('c.us', 's.whatsapp.net');
  1261.         if (!number.includes('@s.whatsapp.net')) number = `${number}@s.whatsapp.net`;
  1262.  
  1263.         return await this.pupPage.evaluate(async numberId => {
  1264.             return window.Store.NumberInfo.formattedPhoneNumber(numberId);
  1265.         }, number);
  1266.     }
  1267.  
  1268.     /**
  1269.      * Get the country code of a WhatsApp ID.
  1270.      * @param {string} number Number or ID
  1271.      * @returns {Promise<string>}
  1272.      */
  1273.     async getCountryCode(number) {
  1274.         number = number.replace(' ', '').replace('+', '').replace('@c.us', '');
  1275.  
  1276.         return await this.pupPage.evaluate(async numberId => {
  1277.             return window.Store.NumberInfo.findCC(numberId);
  1278.         }, number);
  1279.     }
  1280.  
  1281.     /**
  1282.      * Create a new group
  1283.      * @param {string} name group title
  1284.      * @param {Array<Contact|string>} participants an array of Contacts or contact IDs to add to the group
  1285.      * @returns {Object} createRes
  1286.      * @returns {string} createRes.gid - ID for the group that was just created
  1287.      * @returns {Object.<string,string>} createRes.missingParticipants - participants that were not added to the group. Keys represent the ID for participant that was not added and its value is a status code that represents the reason why participant could not be added. This is usually 403 if the user's privacy settings don't allow you to add them to groups.
  1288.      */
  1289.     async createGroup(name, participants) {
  1290.         if (!Array.isArray(participants) || participants.length == 0) {
  1291.             throw 'You need to add at least one other participant to the group';
  1292.         }
  1293.  
  1294.         if (participants.every(c => c instanceof Contact)) {
  1295.             participants = participants.map(c => c.id._serialized);
  1296.         }
  1297.  
  1298.         const createRes = await this.pupPage.evaluate(async (name, participantIds) => {
  1299.             const participantWIDs = participantIds.map(p => window.Store.WidFactory.createWid(p));
  1300.             return await window.Store.GroupUtils.createGroup(name, participantWIDs, 0);
  1301.         }, name, participants);
  1302.  
  1303.         const missingParticipants = createRes.participants.reduce(((missing, c) => {
  1304.             const id = c.wid._serialized;
  1305.             const statusCode = c.error ? c.error.toString() : '200';
  1306.             if (statusCode != 200) return Object.assign(missing, { [id]: statusCode });
  1307.             return missing;
  1308.         }), {});
  1309.  
  1310.         return { gid: createRes.wid, missingParticipants };
  1311.     }
  1312.  
  1313.     /**
  1314.      * Get all current Labels
  1315.      * @returns {Promise<Array<Label>>}
  1316.      */
  1317.     async getLabels() {
  1318.         const labels = await this.pupPage.evaluate(async () => {
  1319.             return window.WWebJS.getLabels();
  1320.         });
  1321.  
  1322.         return labels.map(data => new Label(this, data));
  1323.     }
  1324.  
  1325.     /**
  1326.      * Get Label instance by ID
  1327.      * @param {string} labelId
  1328.      * @returns {Promise<Label>}
  1329.      */
  1330.     async getLabelById(labelId) {
  1331.         const label = await this.pupPage.evaluate(async (labelId) => {
  1332.             return window.WWebJS.getLabel(labelId);
  1333.         }, labelId);
  1334.  
  1335.         return new Label(this, label);
  1336.     }
  1337.  
  1338.     /**
  1339.      * Get all Labels assigned to a chat
  1340.      * @param {string} chatId
  1341.      * @returns {Promise<Array<Label>>}
  1342.      */
  1343.     async getChatLabels(chatId) {
  1344.         const labels = await this.pupPage.evaluate(async (chatId) => {
  1345.             return window.WWebJS.getChatLabels(chatId);
  1346.         }, chatId);
  1347.  
  1348.         return labels.map(data => new Label(this, data));
  1349.     }
  1350.  
  1351.     /**
  1352.      * Get all Chats for a specific Label
  1353.      * @param {string} labelId
  1354.      * @returns {Promise<Array<Chat>>}
  1355.      */
  1356.     async getChatsByLabelId(labelId) {
  1357.         const chatIds = await this.pupPage.evaluate(async (labelId) => {
  1358.             const label = window.Store.Label.get(labelId);
  1359.             const labelItems = label.labelItemCollection.getModelsArray();
  1360.             return labelItems.reduce((result, item) => {
  1361.                 if (item.parentType === 'Chat') {
  1362.                     result.push(item.parentId);
  1363.                 }
  1364.                 return result;
  1365.             }, []);
  1366.         }, labelId);
  1367.  
  1368.         return Promise.all(chatIds.map(id => this.getChatById(id)));
  1369.     }
  1370.  
  1371.     /**
  1372.      * Gets all blocked contacts by host account
  1373.      * @returns {Promise<Array<Contact>>}
  1374.      */
  1375.     async getBlockedContacts() {
  1376.         const blockedContacts = await this.pupPage.evaluate(() => {
  1377.             let chatIds = window.Store.Blocklist.getModelsArray().map(a => a.id._serialized);
  1378.             return Promise.all(chatIds.map(id => window.WWebJS.getContact(id)));
  1379.         });
  1380.  
  1381.         return blockedContacts.map(contact => ContactFactory.create(this.client, contact));
  1382.     }
  1383.  
  1384.     /**
  1385.      * Sets the current user's profile picture.
  1386.      * @param {MessageMedia} media
  1387.      * @returns {Promise<boolean>} Returns true if the picture was properly updated.
  1388.      */
  1389.     async setProfilePicture(media) {
  1390.         const success = await this.pupPage.evaluate((chatid, media) => {
  1391.             return window.WWebJS.setPicture(chatid, media);
  1392.         }, this.info.wid._serialized, media);
  1393.  
  1394.         return success;
  1395.     }
  1396.  
  1397.     /**
  1398.      * Deletes the current user's profile picture.
  1399.      * @returns {Promise<boolean>} Returns true if the picture was properly deleted.
  1400.      */
  1401.     async deleteProfilePicture() {
  1402.         const success = await this.pupPage.evaluate((chatid) => {
  1403.             return window.WWebJS.deletePicture(chatid);
  1404.         }, this.info.wid._serialized);
  1405.  
  1406.         return success;
  1407.     }
  1408.    
  1409.     /**
  1410.      * Change labels in chats
  1411.      * @param {Array<number|string>} labelIds
  1412.      * @param {Array<string>} chatIds
  1413.      * @returns {Promise<void>}
  1414.      */
  1415.     async addOrRemoveLabels(labelIds, chatIds) {
  1416.  
  1417.         return this.pupPage.evaluate(async (labelIds, chatIds) => {
  1418.             if (['smba', 'smbi'].indexOf(window.Store.Conn.platform) === -1) {
  1419.                 throw '[LT01] Only Whatsapp business';
  1420.             }
  1421.             const labels = window.WWebJS.getLabels().filter(e => labelIds.find(l => l == e.id) !== undefined);
  1422.             const chats = window.Store.Chat.filter(e => chatIds.includes(e.id._serialized));
  1423.  
  1424.             let actions = labels.map(label => ({id: label.id, type: 'add'}));
  1425.  
  1426.             chats.forEach(chat => {
  1427.                 (chat.labels || []).forEach(n => {
  1428.                     if (!actions.find(e => e.id == n)) {
  1429.                         actions.push({id: n, type: 'remove'});
  1430.                     }
  1431.                 });
  1432.             });
  1433.  
  1434.             return await window.Store.Label.addOrRemoveLabels(actions, chats);
  1435.         }, labelIds, chatIds);
  1436.     }
  1437. }
  1438.  
  1439. module.exports = Client;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement