diff --git a/index.d.ts b/index.d.ts index fd7014f..b981bdb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,10 +7,7 @@ declare namespace WAWebJS { export class Client extends EventEmitter { constructor(options: ClientOptions) - /** - * Current connection information - * @todo add this in the official docs - */ + /** Current connection information */ public info: ClientInfo /**Accepts an invitation to join a group */ @@ -26,8 +23,6 @@ declare namespace WAWebJS { * Create a new group * @param name group title * @param participants an array of Contacts or contact IDs to add to the group - * - * @todo improve return type in the official docs */ createGroup(name: string, participants: Contact[] | string[]): Promise @@ -223,25 +218,32 @@ declare namespace WAWebJS { os_build_number: string } - /** - * Options for initializing the whatsapp client - * @todo add these in the official docs - */ + /** Options for initializing the whatsapp client */ export interface ClientOptions { - puppeteer?: puppeteer.LaunchOptions - /** Whatsapp session to restore. If not set, will start a new session */ - session?: ClientSession, - /** @default 45000 */ - qrTimeoutMs?: number, - /** @default 20000 */ - qrRefreshIntervalMs?: number, - /** @default 45000 */ + /** Timeout for authentication selector in puppeteer + * @default 45000 */ authTimeoutMs?: number, - /** @default false */ + /** Puppeteer launch options. View docs here: https://github.com/puppeteer/puppeteer/ */ + puppeteer?: puppeteer.LaunchOptions + /** Refresh interval for qr code (how much time to wait before checking if the qr code has changed) + * @default 20000 */ + qrRefreshIntervalMs?: number + /** Timeout for qr code selector in puppeteer + * @default 45000 */ + qrTimeoutMs?: number, + /** Restart client with a new session (i.e. use null 'session' var) if authentication fails + * @default false */ + restartOnAuthFail?: boolean + /** Whatsapp session to restore. If not set, will start a new session */ + session?: ClientSession + /** If another whatsapp web session is detected (another browser), take over the session in the current browser + * @default false */ takeoverOnConflict?: boolean, - /** @default 0 */ + /** How much time to wait before taking over the session + * @default 0 */ takeoverTimeoutMs?: number, - /** @default 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36' */ + /** User agent to use in puppeteer. + * @default 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36' */ userAgent?: string } @@ -451,7 +453,7 @@ declare namespace WAWebJS { */ to: string, /** Message type */ - type: string, + type: MessageTypes, /** Deletes the message from the chat */ delete: (everyone?: boolean) => Promise, @@ -487,10 +489,23 @@ declare namespace WAWebJS { longitude: string, } - /** - * Options for sending a message - * @todo add more specific type for the object */ - export type MessageSendOptions = object + /** Options for sending a message */ + export interface MessageSendOptions { + /** Show links preview */ + linkPreview?: boolean + /** Send audio as voice message */ + sendAudioAsVoice?: boolean + /** Image or videos caption */ + caption?: string + /** Id of the message that is being quoted (or replied to) */ + quotedMessageId?: string + /** Contacts that are being mentioned in the message */ + mentions?: Contact[] + /** Send 'seen' status */ + sendSeen?: boolean + /** Media to be sent */ + media?: MessageMedia + } export interface MessageMedia { data: string, diff --git a/src/Client.js b/src/Client.js index a8db45f..0acd447 100644 --- a/src/Client.js +++ b/src/Client.js @@ -15,7 +15,21 @@ const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification /** * Starting point for interacting with the WhatsApp Web API * @extends {EventEmitter} - * @param {object} options + * @param {object} options - Client options + * @param {number} options.authTimeoutMs - Timeout for authentication selector in puppeteer + * @param {object} options.puppeteer - Puppeteer launch options. View docs here: https://github.com/puppeteer/puppeteer/ + * @param {number} options.qrRefreshIntervalMs - Refresh interval for qr code (how much time to wait before checking if the qr code has changed) + * @param {number} options.qrTimeoutMs - Timeout for qr code selector in puppeteer + * @param {string} options.restartOnAuthFail - Restart client with a new session (i.e. use null 'session' var) if authentication fails + * @param {object} options.session - Whatsapp session to restore. If not set, will start a new session + * @param {string} options.session.WABrowserId + * @param {string} options.session.WASecretBundle + * @param {string} options.session.WAToken1 + * @param {string} options.session.WAToken2 + * @param {number} options.takeoverOnConflict - If another whatsapp web session is detected (another browser), take over the session in the current browser + * @param {number} options.takeoverTimeoutMs - How much time to wait before taking over the session + * @param {string} options.userAgent - User agent to use in puppeteer + * * @fires Client#qr * @fires Client#authenticated * @fires Client#auth_failure @@ -53,7 +67,7 @@ class Client extends EventEmitter { this.pupBrowser = browser; this.pupPage = page; - + if (this.options.session) { await page.evaluateOnNewDocument( session => { @@ -69,7 +83,7 @@ class Client extends EventEmitter { waitUntil: 'load', timeout: 0, }); - + const KEEP_PHONE_CONNECTED_IMG_SELECTOR = '[data-asset-intro-image-light="true"]'; if (this.options.session) { @@ -146,6 +160,10 @@ class Client extends EventEmitter { * Emitted when authentication is successful * @event Client#authenticated * @param {object} session Object containing session information. Can be used to restore the session. + * @param {string} session.WABrowserId + * @param {string} session.WASecretBundle + * @param {string} session.WAToken1 + * @param {string} session.WAToken2 */ this.emit(Events.AUTHENTICATED, session); @@ -156,6 +174,10 @@ class Client extends EventEmitter { await page.evaluate(LoadUtils); // Expose client info + /** + * Current connection information + * @type {ClientInfo} + */ this.info = new ClientInfo(this, await page.evaluate(() => { return window.Store.Conn.serialize(); })); @@ -193,7 +215,7 @@ class Client extends EventEmitter { } return; } - + const message = new Message(this, msg); /** @@ -262,7 +284,7 @@ class Client extends EventEmitter { await page.exposeFunction('onMessageAckEvent', (msg, ack) => { const message = new Message(this, msg); - + /** * Emitted when an ack event occurrs on message type. * @event Client#message_ack @@ -276,7 +298,7 @@ class Client extends EventEmitter { await page.exposeFunction('onMessageMediaUploadedEvent', (msg) => { const message = new Message(this, msg); - + /** * Emitted when media has been uploaded for a message sent by the client. * @event Client#media_uploaded @@ -296,10 +318,10 @@ class Client extends EventEmitter { const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT]; - if(this.options.takeoverOnConflict) { + if (this.options.takeoverOnConflict) { ACCEPTED_STATES.push(WAState.CONFLICT); - if(state === WAState.CONFLICT) { + if (state === WAState.CONFLICT) { setTimeout(() => { this.pupPage.evaluate(() => window.Store.AppState.takeover()); }, this.options.takeoverTimeoutMs); @@ -320,7 +342,7 @@ class Client extends EventEmitter { await page.exposeFunction('onBatteryStateChangedEvent', (state) => { const { battery, plugged } = state; - if(battery === undefined) return; + if (battery === undefined) return; /** * Emitted when the battery percentage for the attached device changes @@ -333,12 +355,12 @@ class Client extends EventEmitter { }); await page.evaluate(() => { - window.Store.Msg.on('add', (msg) => { if(msg.isNewMsg) window.onAddMessageEvent(msg); }); + window.Store.Msg.on('add', (msg) => { if (msg.isNewMsg) window.onAddMessageEvent(msg); }); window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(msg); }); window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(msg); }); window.Store.Msg.on('change:ack', (msg, ack) => { window.onMessageAckEvent(msg, ack); }); - window.Store.Msg.on('change:isUnsentMedia', (msg, unsent) => { if(msg.id.fromMe && !unsent) window.onMessageMediaUploadedEvent(msg); }); - window.Store.Msg.on('remove', (msg) => { if(msg.isNewMsg) window.onRemoveMessageEvent(msg); }); + window.Store.Msg.on('change:isUnsentMedia', (msg, unsent) => { if (msg.id.fromMe && !unsent) window.onMessageMediaUploadedEvent(msg); }); + window.Store.Msg.on('remove', (msg) => { if (msg.isNewMsg) window.onRemoveMessageEvent(msg); }); window.Store.AppState.on('change:state', (_AppState, state) => { window.onAppStateChangedEvent(state); }); window.Store.Conn.on('change:battery', (state) => { window.onBatteryStateChangedEvent(state); }); }); @@ -393,11 +415,24 @@ class Client extends EventEmitter { return result; } + /** + * Message options. + * @typedef {Object} MessageSendOptions + * @property {boolean} [linkPreview=true] - Show links preview + * @property {boolean} [sendAudioAsVoice=false] - Send audio as voice message + * @property {string} [caption] - Image or video caption + * @property {string} [quotedMessageId] - Id of the message that is being quoted (or replied to) + * @property {Contact[]} [mentions] - Contacts that are being mentioned in the message + * @property {boolean} [sendSeen=true] - Mark the conversation as seen after sending the message + * @property {boolean} [media] - Media to be sent + */ + /** * Send a message to a specific chatId * @param {string} chatId * @param {string|MessageMedia|Location} content - * @param {object} options + * @param {MessageSendOptions} [options] - Options used when sending the message + * * @returns {Promise} Message that was just sent */ async sendMessage(chatId, content, options = {}) { @@ -408,7 +443,7 @@ class Client extends EventEmitter { quotedMessageId: options.quotedMessageId, mentionedJidList: Array.isArray(options.mentions) ? options.mentions.map(contact => contact.id._serialized) : [] }; - + const sendSeen = typeof options.sendSeen === 'undefined' ? true : options.sendSeen; if (content instanceof MessageMedia) { @@ -427,7 +462,7 @@ class Client extends EventEmitter { const chatWid = window.Store.WidFactory.createWid(chatId); const chat = await window.Store.Chat.find(chatWid); - if(sendSeen) { + if (sendSeen) { window.WWebJS.sendSeen(chatId); } @@ -575,7 +610,7 @@ class Client extends EventEmitter { await chat.mute.mute(timestamp, !0); }, chatId, unmuteDate.getTime() / 1000); } - + /** * Unmutes the Chat * @param {string} chatId ID of the chat that will be unmuted @@ -586,7 +621,7 @@ class Client extends EventEmitter { await window.Store.Cmd.muteChat(chat, false); }, chatId); } - + /** * Returns the contact ID's profile picture URL, if privacy settings allow it * @param {string} contactId the whatsapp user's ID @@ -603,7 +638,7 @@ class Client extends EventEmitter { /** * Force reset of connection state for the client */ - async resetState(){ + async resetState() { await this.pupPage.evaluate(() => { window.Store.AppState.phoneWatchdog.shiftTimer.forceRunNow(); }); @@ -630,18 +665,18 @@ class Client extends EventEmitter { * @returns {Object.} 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. */ async createGroup(name, participants) { - if(!Array.isArray(participants) || participants.length == 0) { + if (!Array.isArray(participants) || participants.length == 0) { throw 'You need to add at least one other participant to the group'; } - if(participants.every(c => c instanceof Contact)) { + if (participants.every(c => c instanceof Contact)) { participants = participants.map(c => c.id._serialized); } const createRes = await this.pupPage.evaluate(async (name, participantIds) => { const res = await window.Store.Wap.createGroup(name, participantIds); console.log(res); - if(!res.status === 200) { + if (!res.status === 200) { throw 'An error occurred while creating the group!'; } @@ -651,11 +686,11 @@ class Client extends EventEmitter { const missingParticipants = createRes.participants.reduce(((missing, c) => { const id = Object.keys(c)[0]; const statusCode = c[id].code; - if(statusCode != 200) return Object.assign(missing, {[id]: statusCode}); + if (statusCode != 200) return Object.assign(missing, { [id]: statusCode }); return missing; }), {}); - return { gid: createRes.gid, missingParticipants}; + return { gid: createRes.gid, missingParticipants }; } } diff --git a/src/structures/Chat.js b/src/structures/Chat.js index 506fbb2..6f99b0c 100644 --- a/src/structures/Chat.js +++ b/src/structures/Chat.js @@ -63,7 +63,7 @@ class Chat extends Base { /** * Send a message to this chat * @param {string|MessageMedia|Location} content - * @param {object} options + * @param {MessageSendOptions} [options] * @returns {Promise} Message that was just sent */ async sendMessage(content, options) { @@ -119,7 +119,7 @@ class Chat extends Base { async mute(unmuteDate) { return this.client.muteChat(this.id._serialized, unmuteDate); } - + /** * Unmutes this chat */ @@ -134,18 +134,18 @@ class Chat extends Base { * @returns {Promise>} */ async fetchMessages(searchOptions) { - if(!searchOptions || !searchOptions.limit) { - searchOptions = {limit: 50}; + if (!searchOptions || !searchOptions.limit) { + searchOptions = { limit: 50 }; } let messages = await this.client.pupPage.evaluate(async (chatId, limit) => { const msgFilter = m => !m.isNotification; // dont include notification messages - + const chat = window.Store.Chat.get(chatId); let msgs = chat.msgs.models.filter(msgFilter); - - while(msgs.length < limit) { + + while (msgs.length < limit) { const loadedMessages = await chat.loadEarlierMsgs(); - if(!loadedMessages) break; + if (!loadedMessages) break; msgs = [...loadedMessages.filter(msgFilter), ...msgs]; } @@ -156,7 +156,7 @@ class Chat extends Base { return messages.map(m => new Message(this.client, m)); } - + /** * Simulate typing in chat. This will last for 25 seconds. */ @@ -166,7 +166,7 @@ class Chat extends Base { return true; }, this.id._serialized); } - + /** * Simulate recording audio in chat. This will last for 25 seconds. */ @@ -176,7 +176,7 @@ class Chat extends Base { return true; }, this.id._serialized); } - + /** * Stops typing or recording in chat immediately. */ diff --git a/src/structures/Message.js b/src/structures/Message.js index 8702915..7175880 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -13,7 +13,7 @@ class Message extends Base { constructor(client, data) { super(client); - if(data) this._patch(data); + if (data) this._patch(data); } _patch(data) { @@ -98,7 +98,7 @@ class Message extends Base { * @type {boolean} */ this.fromMe = data.id.fromMe; - + /** * Indicates if the message was sent as a reply to another message. * @type {boolean} @@ -173,11 +173,11 @@ class Message extends Base { * in the same Chat as the original message was sent. * * @param {string|MessageMedia|Location} content - * @param {?string} chatId - * @param {object} options + * @param {string} [chatId] + * @param {MessageSendOptions} [options] * @returns {Promise} */ - async reply(content, chatId, options={}) { + async reply(content, chatId, options = {}) { if (!chatId) { chatId = this._getChatId(); } @@ -201,13 +201,13 @@ class Message extends Base { const result = await this.client.pupPage.evaluate(async (msgId) => { const msg = window.Store.Msg.get(msgId); - - if(msg.mediaData.mediaStage != 'RESOLVED') { + + if (msg.mediaData.mediaStage != 'RESOLVED') { // try to resolve media await msg.downloadMedia(true, 1); } - - if(msg.mediaData.mediaStage.includes('ERROR')) { + + if (msg.mediaData.mediaStage.includes('ERROR')) { // media could not be downloaded return undefined; } @@ -215,7 +215,7 @@ class Message extends Base { const buffer = await window.WWebJS.downloadBuffer(msg.clientUrl); const decrypted = await window.Store.CryptoLib.decryptE2EMedia(msg.type, buffer, msg.mediaKey, msg.mimetype); const data = await window.WWebJS.readBlobAsync(decrypted._blob); - + return { data: data.split(',')[1], mimetype: msg.mimetype, @@ -224,7 +224,7 @@ class Message extends Base { }, this.id._serialized); - if(!result) return undefined; + if (!result) return undefined; return new MessageMedia(result.mimetype, result.data, result.filename); } @@ -236,10 +236,10 @@ class Message extends Base { await this.client.pupPage.evaluate((msgId, everyone) => { let msg = window.Store.Msg.get(msgId); - if(everyone && msg.id.fromMe && msg.canRevoke()) { + if (everyone && msg.id.fromMe && msg.canRevoke()) { return window.Store.Cmd.sendRevokeMsgs(msg.chat, [msg], true); - } - + } + return window.Store.Cmd.sendDeleteMsgs(msg.chat, [msg], true); }, this.id._serialized, everyone); }