From 8277590d11643c0f2e134a8d693d9c7f0a75ea08 Mon Sep 17 00:00:00 2001 From: Adhiraj Date: Wed, 30 Sep 2020 20:44:22 +0530 Subject: [PATCH] Added ability to log messages --- src/BrowserMessageDecoding.ts | 1 - src/Tests/Common.ts | 4 +- src/WAConnection/0.Base.ts | 64 +++++++++++++++++------------ src/WAConnection/3.Connect.ts | 7 +++- src/WAConnection/4.Events.ts | 2 + src/WAConnection/5.User.ts | 24 ++++++----- src/WAConnection/7.MessagesExtra.ts | 2 +- src/WAConnection/Constants.ts | 6 ++- 8 files changed, 66 insertions(+), 44 deletions(-) diff --git a/src/BrowserMessageDecoding.ts b/src/BrowserMessageDecoding.ts index 200b73e..c9634f9 100644 --- a/src/BrowserMessageDecoding.ts +++ b/src/BrowserMessageDecoding.ts @@ -33,7 +33,6 @@ const list = wsMessages.map ((item, i) => { try { const [tag, json, binaryTags] = decrypt (buffer, item.type === 'send') - if (json && json[1] && json[1].add) return return {tag, json: json && JSON.stringify(json), binaryTags} } catch (error) { return { error: error.message, data: buffer.toString('utf-8') } diff --git a/src/Tests/Common.ts b/src/Tests/Common.ts index 86e4659..f9099bf 100644 --- a/src/Tests/Common.ts +++ b/src/Tests/Common.ts @@ -20,10 +20,10 @@ export async function sendAndRetreiveMessage(conn: WAConnection, content, type: export const WAConnectionTest = (name: string, func: (conn: WAConnection) => void) => ( describe(name, () => { const conn = new WAConnection() - conn.logLevel = MessageLogLevel.info + conn.connectOptions.maxIdleTimeMs = 30_000 + conn.logLevel = MessageLogLevel.unhandled before(async () => { - //conn.logLevel = MessageLogLevel.unhandled const file = './auth_info.json' await conn.loadAuthInfo(file).connect() await fs.writeFile(file, JSON.stringify(conn.base64EncodedAuthInfo(), null, '\t')) diff --git a/src/WAConnection/0.Base.ts b/src/WAConnection/0.Base.ts index bc6bfe6..52774e4 100644 --- a/src/WAConnection/0.Base.ts +++ b/src/WAConnection/0.Base.ts @@ -30,7 +30,7 @@ import { STATUS_CODES, Agent } from 'http' export class WAConnection extends EventEmitter { /** The version of WhatsApp Web we're telling the servers we are */ - version: [number, number, number] = [2, 2039, 6] + version: [number, number, number] = [2, 2039, 9] /** The Browser we're telling the WhatsApp Web servers we are */ browserDescription: [string, string, string] = Utils.Browsers.baileys ('Chrome') /** Metadata like WhatsApp id, name set on WhatsApp etc. */ @@ -57,6 +57,10 @@ export class WAConnection extends EventEmitter { /** key to use to order chats */ chatOrderingKey = Utils.WA_CHAT_KEY + /** log messages */ + shouldLogMessages = false + messageLog: { tag: string, json: string, fromMe: boolean, binaryTags?: any[] }[] = [] + maxCachedMessages = 50 chats: KeyedDB = new KeyedDB (Utils.WA_CHAT_KEY, value => value.jid) @@ -74,6 +78,7 @@ export class WAConnection extends EventEmitter { protected encoder = new Encoder() protected decoder = new Decoder() protected pendingRequests: {resolve: () => void, reject: (error) => void}[] = [] + protected referenceDate = new Date () // used for generating tags protected lastSeen: Date = null // last keep alive received protected qrTimeout: NodeJS.Timeout @@ -208,15 +213,15 @@ export class WAConnection extends EventEmitter { * @param json query that was sent * @param timeoutMs timeout after which the promise will reject */ - async waitForMessage(tag: string, json: Object = null, timeoutMs: number = null) { - let promise = Utils.promiseTimeout(timeoutMs, - (resolve, reject) => (this.callbacks[tag] = { queryJSON: json, callback: resolve, errCallback: reject }), - ) - .catch((err) => { + async waitForMessage(tag: string, json?: Object, timeoutMs?: number) { + try { + const result = await Utils.promiseTimeout(timeoutMs, + (resolve, reject) => (this.callbacks[tag] = { queryJSON: json, callback: resolve, errCallback: reject }), + ) + return result as any + } finally { delete this.callbacks[tag] - throw err - }) - return promise as Promise + } } /** Generic function for action, set queries */ async setQuery (nodes: WANode[], binaryTags: WATag = [WAMetric.group, WAFlag.ignore], tag?: string) { @@ -230,16 +235,18 @@ export class WAConnection extends EventEmitter { * @param binaryTags the tags to attach if the query is supposed to be sent encoded in binary * @param timeoutMs timeout after which the query will be failed (set to null to disable a timeout) * @param tag the tag to attach to the message - * recieved JSON */ async query({json, binaryTags, tag, timeoutMs, expect200, waitForOpen, longTag}: WAQuery) { - waitForOpen = typeof waitForOpen === 'undefined' ? true : waitForOpen - if (waitForOpen) await this.waitForConnection () - + waitForOpen = waitForOpen !== false + if (waitForOpen) await this.waitForConnection() + + tag = tag || this.generateMessageTag (longTag) + const promise = this.waitForMessage(tag, json, timeoutMs) + if (binaryTags) tag = await this.sendBinary(json as WANode, binaryTags, tag, longTag) else tag = await this.sendJSON(json, tag, longTag) - - const response = await this.waitForMessage(tag, json, timeoutMs) + + const response = await promise if (expect200 && response.status && Math.floor(+response.status / 100) !== 2) { // read here: http://getstatuscode.com/599 if (response.status === 599) { @@ -262,43 +269,48 @@ export class WAConnection extends EventEmitter { * @param tag the tag to attach to the message * @return the message tag */ - protected sendBinary(json: WANode, tags: WATag, tag: string = null, longTag: boolean = false) { + protected async sendBinary(json: WANode, tags: WATag, tag: string = null, longTag: boolean = false) { const binary = this.encoder.write(json) // encode the JSON to the WhatsApp binary format let buff = Utils.aesEncrypt(binary, this.authInfo.encKey) // encrypt it using AES and our encKey const sign = Utils.hmacSign(buff, this.authInfo.macKey) // sign the message using HMAC and our macKey tag = tag || this.generateMessageTag(longTag) + + if (this.shouldLogMessages) this.messageLog.push ({ tag, json: JSON.stringify(json), fromMe: true, binaryTags: tags }) + buff = Buffer.concat([ Buffer.from(tag + ','), // generate & prefix the message tag Buffer.from(tags), // prefix some bytes that tell whatsapp what the message is about sign, // the HMAC sign of the message buff, // the actual encrypted buffer ]) - this.send(buff) // send it off + await this.send(buff) // send it off return tag } /** * Send a plain JSON message to the WhatsApp servers * @param json the message to send * @param tag the tag to attach to the message - * @return the message tag + * @returns the message tag */ - protected sendJSON(json: any[] | WANode, tag: string = null, longTag: boolean = false) { + protected async sendJSON(json: any[] | WANode, tag: string = null, longTag: boolean = false) { tag = tag || this.generateMessageTag(longTag) - this.send(`${tag},${JSON.stringify(json)}`) + if (this.shouldLogMessages) this.messageLog.push ({ tag, json: JSON.stringify(json), fromMe: true }) + await this.send(`${tag},${JSON.stringify(json)}`) return tag } /** Send some message to the WhatsApp servers */ - protected send(m) { + protected async send(m) { this.msgCount += 1 // increment message count, it makes the 'epoch' field when sending binary messages - return this.conn.send(m) + this.conn.send(m) } protected async waitForConnection () { if (this.state === 'open') return await Utils.promiseTimeout ( this.pendingRequestTimeoutMs, - (resolve, reject) => this.pendingRequests.push({resolve, reject})) + (resolve, reject) => this.pendingRequests.push({resolve, reject}) + ) } /** * Disconnect from the phone. Your auth credentials become invalid after sending a disconnect request. @@ -325,7 +337,6 @@ export class WAConnection extends EventEmitter { this.debounceTimeout && clearTimeout (this.debounceTimeout) this.state = 'close' - this.msgCount = 0 this.phoneConnected = false this.lastDisconnectReason = reason this.lastDisconnectTime = new Date () @@ -344,13 +355,14 @@ export class WAConnection extends EventEmitter { this.conn?.removeAllListeners ('error') this.conn?.removeAllListeners ('open') this.conn?.removeAllListeners ('message') - //this.conn?.close () + this.conn?.terminate() this.conn = null this.lastSeen = null + this.msgCount = 0 Object.keys(this.callbacks).forEach(key => { - if (!key.includes('function:')) { + if (!key.startsWith('function:')) { this.log (`cancelling message wait: ${key}`, MessageLogLevel.info) this.callbacks[key].errCallback(new Error('close')) delete this.callbacks[key] diff --git a/src/WAConnection/3.Connect.ts b/src/WAConnection/3.Connect.ts index 248232b..d68fc01 100644 --- a/src/WAConnection/3.Connect.ts +++ b/src/WAConnection/3.Connect.ts @@ -267,6 +267,7 @@ export class WAConnection extends Base { resolveTask = resolve cancelChats = () => reject (CancelledError()) }) + console.log ('resolved task') const oldChats = this.chats const updatedChats: { [k: string]: Partial } = {} @@ -307,12 +308,14 @@ export class WAConnection extends Base { this.emit ('received-pong') } else { const [messageTag, json] = Utils.decryptWA (message, this.authInfo?.macKey, this.authInfo?.encKey, new Decoder()) - if (!json) return + if (this.shouldLogMessages) this.messageLog.push ({ tag: messageTag, json: JSON.stringify(json), fromMe: false }) + + if (!json) {return} if (this.logLevel === MessageLogLevel.all) { this.log(messageTag + ', ' + JSON.stringify(json), MessageLogLevel.all) } - if (!this.phoneConnected) { + if (!this.phoneConnected && this.state === 'open') { this.phoneConnected = true this.emit ('connection-phone-change', { connected: true }) } diff --git a/src/WAConnection/4.Events.ts b/src/WAConnection/4.Events.ts index 7612173..c49c4f4 100644 --- a/src/WAConnection/4.Events.ts +++ b/src/WAConnection/4.Events.ts @@ -3,6 +3,7 @@ import { WAConnection as Base } from './3.Connect' import { WAMessageStatusUpdate, WAMessage, WAContact, WAChat, WAMessageProto, WA_MESSAGE_STUB_TYPE, WA_MESSAGE_STATUS_TYPE, MessageLogLevel, PresenceUpdate, BaileysEvent, DisconnectReason, WANode, WAOpenResult, Presence } from './Constants' import { whatsappID, unixTimestampSeconds, isGroupID, toNumber, GET_MESSAGE_ID, WA_MESSAGE_ID, WA_MESSAGE_KEY } from './Utils' import KeyedDB from '@adiwajshing/keyed-db' +import { Mutex } from './Mutex' export class WAConnection extends Base { @@ -165,6 +166,7 @@ export class WAConnection extends Base { this.on ('qr', qr => QR.generate(qr, { small: true })) } /** Get the URL to download the profile picture of a person/group */ + @Mutex (jid => jid) async getProfilePicture(jid: string | null) { const response = await this.query({ json: ['query', 'ProfilePicThumb', jid || this.user.jid], expect200: true }) return response.eurl as string diff --git a/src/WAConnection/5.User.ts b/src/WAConnection/5.User.ts index 33d52be..1b81584 100644 --- a/src/WAConnection/5.User.ts +++ b/src/WAConnection/5.User.ts @@ -1,5 +1,5 @@ import {WAConnection as Base} from './4.Events' -import { Presence, WABroadcastListInfo, WAProfilePictureChange, WAChat, ChatModification } from './Constants' +import { Presence, WABroadcastListInfo, WAProfilePictureChange, WAChat, ChatModification, WALoadChatOptions } from './Constants' import { WAMessage, WANode, @@ -96,17 +96,19 @@ export class WAConnection extends Base { * @param searchString optionally search for users * @returns the chats & the cursor to fetch the next page */ - async loadChats (count: number, before: number | null, filters?: {searchString?: string, custom?: (c: WAChat) => boolean}) { - const chats = this.chats.paginated (before, count, filters && (chat => ( - (typeof filters?.custom !== 'function' || filters?.custom(chat)) && - (typeof filters?.searchString === 'undefined' || chat.name?.includes (filters.searchString) || chat.jid?.startsWith(filters.searchString)) + async loadChats (count: number, before: number | null, options: WALoadChatOptions = {}) { + const chats = this.chats.paginated (before, count, options && (chat => ( + (typeof options?.custom !== 'function' || options?.custom(chat)) && + (typeof options?.searchString === 'undefined' || chat.name?.includes (options.searchString) || chat.jid?.startsWith(options.searchString)) ))) - await Promise.all ( - chats.map (async chat => ( - chat.imgUrl === undefined && await this.setProfilePicture (chat) - )) - ) - const cursor = (chats[chats.length-1] && chats.length >= count) ? WA_CHAT_KEY (chats[chats.length-1]) : null + if (options.loadProfilePicture !== false) { + await Promise.all ( + chats.map (async chat => ( + typeof chat.imgUrl === 'undefined' && await this.setProfilePicture (chat) + )) + ) + } + const cursor = (chats[chats.length-1] && chats.length >= count) && this.chatOrderingKey (chats[chats.length-1]) return { chats, cursor } } /** diff --git a/src/WAConnection/7.MessagesExtra.ts b/src/WAConnection/7.MessagesExtra.ts index 12c0c5e..fb21786 100644 --- a/src/WAConnection/7.MessagesExtra.ts +++ b/src/WAConnection/7.MessagesExtra.ts @@ -93,7 +93,7 @@ export class WAConnection extends Base { * @param cursor the data for which message to offset the query by * @param mostRecentFirst retreive the most recent message first or retreive from the converation start */ - @Mutex ((jid, _, before, mostRecentFirst) => jid + (before?.id || '') + mostRecentFirst) + @Mutex () async loadMessages ( jid: string, count: number, diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index 8a259be..5c88a36 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -65,6 +65,11 @@ export enum ReconnectMode { /** reconnects on all disconnects, including take overs */ onAllErrors = 2 } +export type WALoadChatOptions = { + searchString?: string + custom?: (c: WAChat) => boolean + loadProfilePicture?: boolean +} export type WAConnectOptions = { /** New QR generation interval, set to null if you don't want to regenerate */ regenerateQRIntervalMs?: number @@ -78,7 +83,6 @@ export type WAConnectOptions = { waitOnlyForLastMessage?: boolean /** max time for the phone to respond to a connectivity test */ phoneResponseTime?: number - connectCooldownMs?: number /** agent which can be used for proxying connections */ agent?: Agent