diff --git a/src/WAClient/Base.ts b/src/WAClient/Base.ts index ab47e47..0634ca1 100644 --- a/src/WAClient/Base.ts +++ b/src/WAClient/Base.ts @@ -9,7 +9,6 @@ import { WATag, } from '../WAConnection/Constants' import { generateProfilePicture } from '../WAClient/Utils' -import { generateMessageTag } from '../WAConnection/Utils' export default class WhatsAppWebBase extends WAConnection { @@ -190,7 +189,7 @@ export default class WhatsAppWebBase extends WAConnection { } async updateProfilePicture (jid: string, img: Buffer) { const data = await generateProfilePicture (img) - const tag = generateMessageTag (this.msgCount) + const tag = this.generateMessageTag () const query: WANode = [ 'picture', { jid: jid, id: tag, type: 'set' }, @@ -199,7 +198,7 @@ export default class WhatsAppWebBase extends WAConnection { ['preview', null, data.preview] ] ] - return this.setQuery ([query], [14, 136], tag) as Promise + return this.setQuery ([query], [WAMetric.picture, 136], tag) as Promise } /** Generic function for action, set queries */ async setQuery (nodes: WANode[], binaryTags: WATag = [WAMetric.group, WAFlag.ignore], tag?: string) { diff --git a/src/WAClient/Groups.ts b/src/WAClient/Groups.ts index 5e274cc..bbc8c05 100644 --- a/src/WAClient/Groups.ts +++ b/src/WAClient/Groups.ts @@ -1,12 +1,11 @@ import WhatsAppWebBase from './Base' import { WAMessage, WAMetric, WAFlag, WANode, WAGroupMetadata, WAGroupCreateResponse, WAGroupModification } from '../WAConnection/Constants' import { GroupSettingChange } from './Constants' -import { generateMessageTag } from '../WAConnection/Utils' export default class WhatsAppWebGroups extends WhatsAppWebBase { /** Generic function for group queries */ async groupQuery(type: string, jid?: string, subject?: string, participants?: string[], additionalNodes?: WANode[]) { - const tag = generateMessageTag(this.msgCount) + const tag = this.generateMessageTag() const json: WANode = [ 'group', { diff --git a/src/WAConnection/Base.ts b/src/WAConnection/Base.ts index b09d6d3..1bb61f7 100644 --- a/src/WAConnection/Base.ts +++ b/src/WAConnection/Base.ts @@ -57,6 +57,7 @@ export default class WAConnectionBase { protected decoder = new Decoder() protected pendingRequests: (() => void)[] = [] protected reconnectLoop: () => Promise + protected referenceDate = new Date () // used for generating tags constructor () { this.registerCallback (['Cmd', 'type:disconnect'], json => this.unexpectedDisconnect(json[1].kind)) @@ -235,12 +236,12 @@ export default class WAConnectionBase { * @param tag the tag to attach to the message * @return the message tag */ - private async sendBinary(json: WANode, tags: WATag, tag: string) { + protected async sendBinary(json: WANode, tags: WATag, tag?: string) { 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 || Utils.generateMessageTag(this.msgCount) + tag = tag || this.generateMessageTag() 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 @@ -256,8 +257,8 @@ export default class WAConnectionBase { * @param tag the tag to attach to the message * @return the message tag */ - private async sendJSON(json: any[] | WANode, tag: string = null) { - tag = tag || Utils.generateMessageTag(this.msgCount) + protected async sendJSON(json: any[] | WANode, tag: string = null) { + tag = tag || this.generateMessageTag() await this.send(tag + ',' + JSON.stringify(json)) return tag } @@ -282,12 +283,8 @@ export default class WAConnectionBase { async logout() { if (!this.conn) throw new Error("You're not even connected, you can't log out") - await new Promise(resolve => { - this.conn.send('goodbye,["admin","Conn","disconnect"]', null, () => { - this.authInfo = null - resolve() - }) - }) + await new Promise(resolve => this.conn.send('goodbye,["admin","Conn","disconnect"]', null, resolve)) + this.authInfo = null this.close() } @@ -300,7 +297,7 @@ export default class WAConnectionBase { this.conn = null } const keys = Object.keys(this.callbacks) - keys.forEach((key) => { + keys.forEach(key => { if (!key.includes('function:')) { this.callbacks[key].errCallback('connection closed') delete this.callbacks[key] @@ -310,6 +307,9 @@ export default class WAConnectionBase { clearInterval(this.keepAliveReq) } } + generateMessageTag () { + return `${this.referenceDate.getTime()/1000}.--${this.msgCount}` + } protected log(text, level: MessageLogLevel) { if (this.logLevel >= level) console.log(`[Baileys][${new Date().toLocaleString()}] ${text}`) diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index f81e3dc..3a66740 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -75,12 +75,33 @@ export interface WAChat { messages: WAMessage[] } export enum WAMetric { + debugLog = 1, + queryResume = 2, liveLocation = 3, queryMedia = 4, + queryChat = 5, + queryContact = 6, queryMessages = 7, + presence = 8, + presenceSubscribe = 9, group = 10, + read = 11, + chat = 12, + received = 13, + picture = 14, + status = 15, message = 16, + queryActions = 17, + block = 18, + queryGroup = 19, + queryPreview = 20, + queryEmoji = 21, + queryVCard = 29, + queryStatus = 30, + queryStatusUpdate = 31, queryLiveLocation = 33, + queryLabel = 36, + queryQuickReply = 39 } export enum WAFlag { ignore = 1 << 7, diff --git a/src/WAConnection/Validation.ts b/src/WAConnection/Validation.ts index 4bc92fd..c48674a 100644 --- a/src/WAConnection/Validation.ts +++ b/src/WAConnection/Validation.ts @@ -1,7 +1,8 @@ import * as Curve from 'curve25519-js' import * as Utils from './Utils' import WAConnectionBase from './Base' -import { MessageLogLevel } from './Constants' +import { MessageLogLevel, WAMetric, WAFlag } from './Constants' +import { Presence } from '../WAClient/WAClient' const StatusError = (message: any, description: string='unknown error') => new Error (`unexpected status: ${message.status} on JSON: ${JSON.stringify(message)}`) @@ -19,32 +20,28 @@ export default class WAConnectionValidator extends WAConnectionBase { macKey: null, } } + + this.referenceDate = new Date () // refresh reference date const data = ['admin', 'init', this.version, this.browserDescription, this.authInfo.clientID, true] - return this.query(data) - .then((json) => { + return this.queryExpecting200(data) + .then(json => { // we're trying to establish a new connection or are trying to log in - switch (json.status) { - case 200: // all good and we can procede to generate a QR code for new connection, or can now login given present auth info - if (this.authInfo.encKey && this.authInfo.macKey) { - // if we have the info to restore a closed session - const data = [ - 'admin', - 'login', - this.authInfo.clientToken, - this.authInfo.serverToken, - this.authInfo.clientID, - 'takeover', - ] - return this.query(data, null, null, 's1') // wait for response with tag "s1" - } else { - return this.generateKeysForAuth(json.ref) - } - default: - throw StatusError (json) + if (this.authInfo.encKey && this.authInfo.macKey) { + // if we have the info to restore a closed session + const data = [ + 'admin', + 'login', + this.authInfo.clientToken, + this.authInfo.serverToken, + this.authInfo.clientID, + 'takeover', + ] + return this.query(data, null, null, 's1') // wait for response with tag "s1" } + return this.generateKeysForAuth(json.ref) // generate keys which will in turn be the QR }) - .then((json) => { - if ('status' in json) { + .then(json => { + if ('status' in json) { switch (json.status) { case 401: // if the phone was unpaired throw StatusError (json, 'unpaired from phone') @@ -54,28 +51,36 @@ export default class WAConnectionValidator extends WAConnectionBase { throw StatusError (json) } } - if (json[1] && json[1].challenge) { - // if its a challenge request (we get it when logging in) + // if its a challenge request (we get it when logging in) + if (json[1]?.challenge) { return this.respondToChallenge(json[1].challenge) - .then((json) => { - if (json.status !== 200) { - // throw an error if the challenge failed - throw StatusError (json) - } - return this.waitForMessage('s2', []) // otherwise wait for the validation message - }) - } else { - // otherwise just chain the promise further - return json + .then (() => this.waitForMessage('s2', [])) } + // otherwise just chain the promise further + return json }) - .then((json) => { + .then(async json => { this.validateNewConnection(json[1]) // validate the connection this.log('validated connection successfully', MessageLogLevel.info) + + await this.sendPostConnectQueries () + this.lastSeen = new Date() // set last seen to right now return this.userMetaData }) } + /** + * Send the same queries WA Web sends after connect + */ + async sendPostConnectQueries () { + await this.sendBinary (['query', {type: 'contacts', epoch: '1'}, null], [ WAMetric.queryContact, WAFlag.ignore ]) + await this.sendBinary (['query', {type: 'chat', epoch: '1'}, null], [ WAMetric.queryChat, WAFlag.ignore ]) + await this.sendBinary (['query', {type: 'status', epoch: '1'}, null], [ WAMetric.queryStatus, WAFlag.ignore ]) + await this.sendBinary (['query', {type: 'quick_reply', epoch: '1'}, null], [ WAMetric.queryQuickReply, WAFlag.ignore ]) + await this.sendBinary (['query', {type: 'label', epoch: '1'}, null], [ WAMetric.queryLabel, WAFlag.ignore ]) + await this.sendBinary (['query', {type: 'emoji', epoch: '1'}, null], [ WAMetric.queryEmoji, WAFlag.ignore ]) + await this.sendBinary (['action', {type: 'set', epoch: '1'}, [['presence', {type: Presence.available}, null]] ], [ WAMetric.presence, 160 ]) + } /** * Refresh QR Code * @returns the new ref @@ -158,7 +163,7 @@ export default class WAConnectionValidator extends WAConnectionBase { const signed = Utils.hmacSign(bytes, this.authInfo.macKey).toString('base64') // sign the challenge string with our macKey const data = ['admin', 'challenge', signed, this.authInfo.serverToken, this.authInfo.clientID] // prepare to send this signed string with the serverToken & clientID this.log('resolving login challenge', MessageLogLevel.info) - return this.query(data) + return this.queryExpecting200(data) } /** When starting a new session, generate a QR code by generating a private/public key pair & the keys the server sends */ protected async generateKeysForAuth(ref: string) {