Better tag generation + better traffic emulation

This commit is contained in:
Adhiraj
2020-07-20 12:43:02 +05:30
parent d096c8c012
commit 13ca965cdf
5 changed files with 77 additions and 53 deletions

View File

@@ -9,7 +9,6 @@ import {
WATag, WATag,
} from '../WAConnection/Constants' } from '../WAConnection/Constants'
import { generateProfilePicture } from '../WAClient/Utils' import { generateProfilePicture } from '../WAClient/Utils'
import { generateMessageTag } from '../WAConnection/Utils'
export default class WhatsAppWebBase extends WAConnection { export default class WhatsAppWebBase extends WAConnection {
@@ -190,7 +189,7 @@ export default class WhatsAppWebBase extends WAConnection {
} }
async updateProfilePicture (jid: string, img: Buffer) { async updateProfilePicture (jid: string, img: Buffer) {
const data = await generateProfilePicture (img) const data = await generateProfilePicture (img)
const tag = generateMessageTag (this.msgCount) const tag = this.generateMessageTag ()
const query: WANode = [ const query: WANode = [
'picture', 'picture',
{ jid: jid, id: tag, type: 'set' }, { jid: jid, id: tag, type: 'set' },
@@ -199,7 +198,7 @@ export default class WhatsAppWebBase extends WAConnection {
['preview', null, data.preview] ['preview', null, data.preview]
] ]
] ]
return this.setQuery ([query], [14, 136], tag) as Promise<WAProfilePictureChange> return this.setQuery ([query], [WAMetric.picture, 136], tag) as Promise<WAProfilePictureChange>
} }
/** Generic function for action, set queries */ /** Generic function for action, set queries */
async setQuery (nodes: WANode[], binaryTags: WATag = [WAMetric.group, WAFlag.ignore], tag?: string) { async setQuery (nodes: WANode[], binaryTags: WATag = [WAMetric.group, WAFlag.ignore], tag?: string) {

View File

@@ -1,12 +1,11 @@
import WhatsAppWebBase from './Base' import WhatsAppWebBase from './Base'
import { WAMessage, WAMetric, WAFlag, WANode, WAGroupMetadata, WAGroupCreateResponse, WAGroupModification } from '../WAConnection/Constants' import { WAMessage, WAMetric, WAFlag, WANode, WAGroupMetadata, WAGroupCreateResponse, WAGroupModification } from '../WAConnection/Constants'
import { GroupSettingChange } from './Constants' import { GroupSettingChange } from './Constants'
import { generateMessageTag } from '../WAConnection/Utils'
export default class WhatsAppWebGroups extends WhatsAppWebBase { export default class WhatsAppWebGroups extends WhatsAppWebBase {
/** Generic function for group queries */ /** Generic function for group queries */
async groupQuery(type: string, jid?: string, subject?: string, participants?: string[], additionalNodes?: WANode[]) { async groupQuery(type: string, jid?: string, subject?: string, participants?: string[], additionalNodes?: WANode[]) {
const tag = generateMessageTag(this.msgCount) const tag = this.generateMessageTag()
const json: WANode = [ const json: WANode = [
'group', 'group',
{ {

View File

@@ -57,6 +57,7 @@ export default class WAConnectionBase {
protected decoder = new Decoder() protected decoder = new Decoder()
protected pendingRequests: (() => void)[] = [] protected pendingRequests: (() => void)[] = []
protected reconnectLoop: () => Promise<void> protected reconnectLoop: () => Promise<void>
protected referenceDate = new Date () // used for generating tags
constructor () { constructor () {
this.registerCallback (['Cmd', 'type:disconnect'], json => this.unexpectedDisconnect(json[1].kind)) 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 * @param tag the tag to attach to the message
* @return the message tag * @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 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 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 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([ buff = Buffer.concat([
Buffer.from(tag + ','), // generate & prefix the message tag Buffer.from(tag + ','), // generate & prefix the message tag
Buffer.from(tags), // prefix some bytes that tell whatsapp what the message is about 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 * @param tag the tag to attach to the message
* @return the message tag * @return the message tag
*/ */
private async sendJSON(json: any[] | WANode, tag: string = null) { protected async sendJSON(json: any[] | WANode, tag: string = null) {
tag = tag || Utils.generateMessageTag(this.msgCount) tag = tag || this.generateMessageTag()
await this.send(tag + ',' + JSON.stringify(json)) await this.send(tag + ',' + JSON.stringify(json))
return tag return tag
} }
@@ -282,12 +283,8 @@ export default class WAConnectionBase {
async logout() { async logout() {
if (!this.conn) throw new Error("You're not even connected, you can't log out") if (!this.conn) throw new Error("You're not even connected, you can't log out")
await new Promise(resolve => { await new Promise(resolve => this.conn.send('goodbye,["admin","Conn","disconnect"]', null, resolve))
this.conn.send('goodbye,["admin","Conn","disconnect"]', null, () => { this.authInfo = null
this.authInfo = null
resolve()
})
})
this.close() this.close()
} }
@@ -300,7 +297,7 @@ export default class WAConnectionBase {
this.conn = null this.conn = null
} }
const keys = Object.keys(this.callbacks) const keys = Object.keys(this.callbacks)
keys.forEach((key) => { keys.forEach(key => {
if (!key.includes('function:')) { if (!key.includes('function:')) {
this.callbacks[key].errCallback('connection closed') this.callbacks[key].errCallback('connection closed')
delete this.callbacks[key] delete this.callbacks[key]
@@ -310,6 +307,9 @@ export default class WAConnectionBase {
clearInterval(this.keepAliveReq) clearInterval(this.keepAliveReq)
} }
} }
generateMessageTag () {
return `${this.referenceDate.getTime()/1000}.--${this.msgCount}`
}
protected log(text, level: MessageLogLevel) { protected log(text, level: MessageLogLevel) {
if (this.logLevel >= level) if (this.logLevel >= level)
console.log(`[Baileys][${new Date().toLocaleString()}] ${text}`) console.log(`[Baileys][${new Date().toLocaleString()}] ${text}`)

View File

@@ -75,12 +75,33 @@ export interface WAChat {
messages: WAMessage[] messages: WAMessage[]
} }
export enum WAMetric { export enum WAMetric {
debugLog = 1,
queryResume = 2,
liveLocation = 3, liveLocation = 3,
queryMedia = 4, queryMedia = 4,
queryChat = 5,
queryContact = 6,
queryMessages = 7, queryMessages = 7,
presence = 8,
presenceSubscribe = 9,
group = 10, group = 10,
read = 11,
chat = 12,
received = 13,
picture = 14,
status = 15,
message = 16, message = 16,
queryActions = 17,
block = 18,
queryGroup = 19,
queryPreview = 20,
queryEmoji = 21,
queryVCard = 29,
queryStatus = 30,
queryStatusUpdate = 31,
queryLiveLocation = 33, queryLiveLocation = 33,
queryLabel = 36,
queryQuickReply = 39
} }
export enum WAFlag { export enum WAFlag {
ignore = 1 << 7, ignore = 1 << 7,

View File

@@ -1,7 +1,8 @@
import * as Curve from 'curve25519-js' import * as Curve from 'curve25519-js'
import * as Utils from './Utils' import * as Utils from './Utils'
import WAConnectionBase from './Base' 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)}`) 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, macKey: null,
} }
} }
this.referenceDate = new Date () // refresh reference date
const data = ['admin', 'init', this.version, this.browserDescription, this.authInfo.clientID, true] const data = ['admin', 'init', this.version, this.browserDescription, this.authInfo.clientID, true]
return this.query(data) return this.queryExpecting200(data)
.then((json) => { .then(json => {
// we're trying to establish a new connection or are trying to log in // we're trying to establish a new connection or are trying to log in
switch (json.status) { if (this.authInfo.encKey && this.authInfo.macKey) {
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 we have the info to restore a closed session
if (this.authInfo.encKey && this.authInfo.macKey) { const data = [
// if we have the info to restore a closed session 'admin',
const data = [ 'login',
'admin', this.authInfo.clientToken,
'login', this.authInfo.serverToken,
this.authInfo.clientToken, this.authInfo.clientID,
this.authInfo.serverToken, 'takeover',
this.authInfo.clientID, ]
'takeover', return this.query(data, null, null, 's1') // wait for response with tag "s1"
]
return this.query(data, null, null, 's1') // wait for response with tag "s1"
} else {
return this.generateKeysForAuth(json.ref)
}
default:
throw StatusError (json)
} }
return this.generateKeysForAuth(json.ref) // generate keys which will in turn be the QR
}) })
.then((json) => { .then(json => {
if ('status' in json) { if ('status' in json) {
switch (json.status) { switch (json.status) {
case 401: // if the phone was unpaired case 401: // if the phone was unpaired
throw StatusError (json, 'unpaired from phone') throw StatusError (json, 'unpaired from phone')
@@ -54,28 +51,36 @@ export default class WAConnectionValidator extends WAConnectionBase {
throw StatusError (json) 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) return this.respondToChallenge(json[1].challenge)
.then((json) => { .then (() => this.waitForMessage('s2', []))
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
} }
// otherwise just chain the promise further
return json
}) })
.then((json) => { .then(async json => {
this.validateNewConnection(json[1]) // validate the connection this.validateNewConnection(json[1]) // validate the connection
this.log('validated connection successfully', MessageLogLevel.info) this.log('validated connection successfully', MessageLogLevel.info)
await this.sendPostConnectQueries ()
this.lastSeen = new Date() // set last seen to right now this.lastSeen = new Date() // set last seen to right now
return this.userMetaData 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 * Refresh QR Code
* @returns the new ref * @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 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 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) 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 */ /** 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) { protected async generateKeysForAuth(ref: string) {