diff --git a/.gitignore b/.gitignore index 30da7c6..e2aa22d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ package-lock.json */.DS_Store .DS_Store .env -lib \ No newline at end of file +lib +auth_info_browser.json diff --git a/README.md b/README.md index 6e35b84..a3f375a 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ To run the example script, download or clone the repo and then type the followin ## Install Create and cd to your NPM project directory and then in terminal, write: 1. stable: `npm install @adiwajshing/baileys` -2. stabl-ish (but quicker fixes): `npm install github:adiwajshing/baileys` +2. stabl-ish (but quicker fixes & latest features): `npm install github:adiwajshing/baileys` Then import in your code using: ``` ts @@ -86,9 +86,22 @@ client.connectSlim('./auth_info.json') // will load JSON credentials from file .then (user => { // yay connected without scanning QR }) + +/* + Optionally, you can load the credentials yourself from somewhere + & pass in the JSON object to connectSlim () as well. +*/ ``` -Optionally, you can load the credentials yourself from somewhere & pass in the JSON object as well. +If you're considering switching from a Chromium/Puppeteer based library, you can use WhatsApp Web's Browser credentials to restore sessions too: +``` ts +client.loadAuthInfoFromBrowser ('./auth_info_browser.json') +client.connectSlim(null, 20*1000) // use loaded credentials & timeout in 20s +.then (user => { + // yay! connected using browser keys & without scanning QR +}) +``` +See the browser credentials type [here](/src/WAConnection/Constants.ts). ## Handling Events Implement the following callbacks in your code: @@ -217,11 +230,26 @@ client.setOnUnreadMessage (false, async m => { const jid = '1234@s.whatsapp.net' // can also be a group const response = await client.sendMessage (jid, 'hello!', MessageType.text) // send a message await client.deleteMessage (jid, {id: response.messageID, remoteJid: jid, fromMe: true}) // will delete the sent message! - -// You can also archive a chat using: -await client.archiveChat(jid) ``` +## Modifying Chats + +``` ts +const jid = '1234@s.whatsapp.net' // can also be a group +await client.modifyChat (jid, ChatModification.archive) // archive chat +await client.modifyChat (jid, ChatModification.unarchive) // unarchive chat + +const response = await client.modifyChat (jid, ChatModification.pin) // pin the chat +await client.modifyChat (jid, ChatModification.unarchive, {stamp: response.stamp}) + +const mutedate = new Date (new Date().getTime() + 8*60*60*1000) // mute for 8 hours in the future +await client.modifyChat (jid, ChatModification.mute, {stamp: mutedate}) // mute +setTimeout (() => { + client.modifyChat (jid, ChatModification.unmute, {stamp: mutedate}) +}, 5000) // unmute after 5 seconds +``` + +**Note:** to unmute or unpin a chat, one must pass the timestamp of the pinning or muting. This is returned by the pin & mute functions. This is also available in the `WAChat` objects of the respective chats, as a `mute` or `pin` property. ## Querying - To check if a given ID is on WhatsApp ``` ts diff --git a/src/WAClient/Messages.ts b/src/WAClient/Messages.ts index c90d56e..0d50282 100644 --- a/src/WAClient/Messages.ts +++ b/src/WAClient/Messages.ts @@ -39,25 +39,34 @@ export default class WhatsAppWebMessages extends WhatsAppWebBase { /** * Modify a given chat (archive, pin etc.) * @param jid the ID of the person/group you are modifiying + * @param options.stamp the timestamp of pinning/muting the chat. Is required when unpinning/unmuting */ - async modifyChat (jid: string, type: ChatModification, options: {stamp: Date} = {stamp: new Date()}) { + async modifyChat (jid: string, type: ChatModification, options: {stamp: Date | string} = {stamp: new Date()}) { let chatAttrs: Record = {jid: jid} + if ((type === ChatModification.unpin || type === ChatModification.unmute) && !options?.stamp) { + throw 'options.stamp must be set to the timestamp of the time of pinning/unpinning of the chat' + } + const strStamp = options.stamp && + (typeof options.stamp === 'string' ? options.stamp : Math.round(options.stamp.getTime ()/1000).toString ()) switch (type) { case ChatModification.pin: case ChatModification.mute: chatAttrs.type = type - chatAttrs[type] = Math.round(options.stamp.getTime ()/1000).toString () + chatAttrs[type] = strStamp break case ChatModification.unpin: case ChatModification.unmute: chatAttrs.type = type.replace ('un', '') // replace 'unpin' with 'pin' - chatAttrs.previous = Math.round(options.stamp.getTime ()/1000).toString () + chatAttrs.previous = strStamp break default: chatAttrs.type = type break } - return this.setQuery ([['chat', chatAttrs, null]]) + console.log (chatAttrs) + let response = await this.setQuery ([['chat', chatAttrs, null]]) as any + response.stamp = strStamp + return response as {status: number, stamp: string} } /** * Search WhatsApp messages with a given text string @@ -82,7 +91,22 @@ export default class WhatsAppWebMessages extends WhatsAppWebBase { return { last: response[1]['last'] === 'true', messages: messages as WAMessage[] } } /** - * Delete a message in a chat + * Delete a message in a chat for yourself + * @param messageKey key of the message you want to delete + */ + async clearMessage (messageKey: WAMessageKey) { + const tag = Math.round(Math.random ()*1000000) + const attrs: WANode = [ + 'chat', + { jid: messageKey.remoteJid, modify_tag: tag.toString(), type: 'clear' }, + [ + ['item', {owner: `${messageKey.fromMe}`, index: messageKey.id}, null] + ] + ] + return this.setQuery ([attrs]) + } + /** + * Delete a message in a chat for everyone * @param id the person or group where you're trying to delete the message * @param messageKey key of the message you want to delete */ diff --git a/src/WAClient/Tests.ts b/src/WAClient/Tests.ts index 98e8714..63b9c7c 100644 --- a/src/WAClient/Tests.ts +++ b/src/WAClient/Tests.ts @@ -62,10 +62,14 @@ WAClientTest('Messages', (client) => { }) it('should send a text message & delete it', async () => { const message = await sendAndRetreiveMessage(client, 'hello fren', MessageType.text) - assert.strictEqual(message.message.conversation, 'hello fren') await createTimeout (2000) await client.deleteMessage (testJid, message.key) }) + it('should clear the most recent message', async () => { + const messages = await client.loadConversation (testJid, 1) + await createTimeout (2000) + await client.clearMessage (messages[0].key) + }) }) describe('Validate WhatsApp IDs', () => { @@ -118,10 +122,9 @@ WAClientTest('Misc', (client) => { await client.modifyChat (testJid, ChatModification.unarchive) }) it('should pin & unpin a chat', async () => { - const pindate = new Date() - await client.modifyChat (testJid, ChatModification.pin, {stamp: pindate}) + const response = await client.modifyChat (testJid, ChatModification.pin) await createTimeout (2000) - await client.modifyChat (testJid, ChatModification.unpin, {stamp: pindate}) + await client.modifyChat (testJid, ChatModification.unpin, {stamp: response.stamp}) }) it('should mute & unmute a chat', async () => { const mutedate = new Date (new Date().getTime() + 8*60*60*1000) // 8 hours in the future @@ -129,9 +132,6 @@ WAClientTest('Misc', (client) => { await createTimeout (2000) await client.modifyChat (testJid, ChatModification.unmute, {stamp: mutedate}) }) - it('should unpin a chat', async () => { - await client.modifyChat (testJid, ChatModification.unpin) - }) it('should return search results', async () => { const response = await client.searchMessages('Adh', 25, 0) assert.ok (response.messages) diff --git a/src/WAConnection/Base.ts b/src/WAConnection/Base.ts index 9b9b2d8..d29e637 100644 --- a/src/WAConnection/Base.ts +++ b/src/WAConnection/Base.ts @@ -4,7 +4,7 @@ import WS from 'ws' import * as Utils from './Utils' import Encoder from '../Binary/Encoder' import Decoder from '../Binary/Decoder' -import { AuthenticationCredentials, UserMetaData, WANode, AuthenticationCredentialsBase64, WATag, MessageLogLevel } from './Constants' +import { AuthenticationCredentials, UserMetaData, WANode, AuthenticationCredentialsBase64, WATag, MessageLogLevel, AuthenticationCredentialsBrowser } from './Constants' /** Generate a QR code from the ref & the curve public key. This is scanned by the phone */ @@ -82,6 +82,27 @@ export default class WAConnectionBase { macKey: Buffer.from(authInfo.macKey, 'base64'), // decode from base64 } } + /** + * Load in the authentication credentials + * @param authInfo the authentication credentials or path to browser credentials JSON + */ + loadAuthInfoFromBrowser(authInfo: AuthenticationCredentialsBrowser | string) { + if (!authInfo) { + throw 'given authInfo is null' + } + if (typeof authInfo === 'string') { + this.log(`loading authentication credentials from ${authInfo}`) + const file = fs.readFileSync(authInfo, { encoding: 'utf-8' }) // load a closed session back if it exists + authInfo = JSON.parse(file) as AuthenticationCredentialsBrowser + } + this.authInfo = { + clientID: authInfo.WABrowserId.replace (/\"/g, ''), + serverToken: authInfo.WAToken2.replace (/\"/g, ''), + clientToken: authInfo.WAToken1.replace (/\"/g, ''), + encKey: Buffer.from(authInfo.WASecretBundle.encKey, 'base64'), // decode from base64 + macKey: Buffer.from(authInfo.WASecretBundle.macKey, 'base64'), // decode from base64 + } + } /** * Register for a callback for a certain function, will cancel automatically after one execution * @param {[string, object, string] | string} parameters name of the function along with some optional specific parameters diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index b7682c4..7e0c01d 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -20,6 +20,12 @@ export interface AuthenticationCredentialsBase64 { encKey: string macKey: string } +export interface AuthenticationCredentialsBrowser { + WABrowserId: string + WASecretBundle: {encKey: string, macKey: string} + WAToken1: string + WAToken2: string +} export interface UserMetaData { id: string name: string @@ -58,6 +64,8 @@ export interface WAChat { count: number archive?: 'true' | 'false' read_only?: 'true' | 'false' + mute?: string + pin?: string spam: 'false' | 'true' jid: string modify_tag: string diff --git a/src/WAConnection/Validation.ts b/src/WAConnection/Validation.ts index 1e1d85b..48935fb 100644 --- a/src/WAConnection/Validation.ts +++ b/src/WAConnection/Validation.ts @@ -16,7 +16,6 @@ export default class WAConnectionValidator extends WAConnectionBase { macKey: null, } } - const data = ['admin', 'init', this.version, this.browserDescription, this.authInfo.clientID, true] return this.query(data) .then((json) => { @@ -42,15 +41,17 @@ export default class WAConnectionValidator extends WAConnectionBase { } }) .then((json) => { - switch (json.status) { - case 401: // if the phone was unpaired - throw [json.status, 'unpaired from phone', json] - case 429: // request to login was denied, don't know why it happens - throw [json.status, 'request denied, try reconnecting', json] - case 304: // request to generate a new key for a QR code was denied - throw [json.status, 'request for new key denied', json] - default: - break + if ('status' in json) { + switch (json.status) { + case 401: // if the phone was unpaired + throw [json.status, 'unpaired from phone', json] + case 429: // request to login was denied, don't know why it happens + throw [json.status, 'request denied, try reconnecting', json] + case 304: // request to generate a new key for a QR code was denied + throw [json.status, 'request for new key denied', json] + default: + throw [json.status, 'unknown error status', json] + } } if (json[1] && json[1].challenge) { // if its a challenge request (we get it when logging in)