mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
Unread fix + regen QR code changes + Browser message decoding
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,3 +8,6 @@ package-lock.json
|
|||||||
lib
|
lib
|
||||||
auth_info_browser.json
|
auth_info_browser.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
browser-messages.json
|
||||||
|
package-lock.json
|
||||||
|
package-lock.json
|
||||||
|
|||||||
3158
package-lock.json
generated
3158
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,8 @@
|
|||||||
"test": "mocha --timeout 30000 -r ts-node/register src/*/Tests.ts",
|
"test": "mocha --timeout 30000 -r ts-node/register src/*/Tests.ts",
|
||||||
"lint": "eslint '*/*.ts' --quiet --fix",
|
"lint": "eslint '*/*.ts' --quiet --fix",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"example": "npx ts-node Example/example.ts"
|
"example": "npx ts-node Example/example.ts",
|
||||||
|
"browser-decode": "npx ts-node src/WAConnection/BrowserMessageDecoding.ts"
|
||||||
},
|
},
|
||||||
"author": "Adhiraj Singh",
|
"author": "Adhiraj Singh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ import { proto } from '../../WAMessage/WAMessage'
|
|||||||
export default class WhatsAppWebMessages extends WhatsAppWebBase {
|
export default class WhatsAppWebMessages extends WhatsAppWebBase {
|
||||||
/**
|
/**
|
||||||
* Send a read receipt to the given ID for a certain message
|
* Send a read receipt to the given ID for a certain message
|
||||||
* @param {string} jid the ID of the person/group whose message you want to mark read
|
* @param jid the ID of the person/group whose message you want to mark read
|
||||||
* @param {string} [messageID] optionally, the message ID
|
* @param messageID optionally, the message ID
|
||||||
|
* @param type whether to read or unread the message
|
||||||
*/
|
*/
|
||||||
async sendReadReceipt(jid: string, messageID?: string, type: 'read' | 'unread' = 'read') {
|
async sendReadReceipt(jid: string, messageID?: string, type: 'read' | 'unread' = 'read') {
|
||||||
const attributes = {
|
const attributes = {
|
||||||
jid: jid,
|
jid: jid,
|
||||||
count: messageID ? '1' : null,
|
count: type === 'read' ? '1' : '-2',
|
||||||
index: messageID,
|
index: messageID,
|
||||||
owner: 'false',
|
owner: messageID ? 'false' : null
|
||||||
type: type==='unread' ? 'false' : null
|
|
||||||
}
|
}
|
||||||
return this.setQuery ([['read', attributes, null]])
|
return this.setQuery ([['read', attributes, null]])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import * as fs from 'fs'
|
|||||||
import * as assert from 'assert'
|
import * as assert from 'assert'
|
||||||
|
|
||||||
import { decodeMediaMessage, validateJIDForSending } from './Utils'
|
import { decodeMediaMessage, validateJIDForSending } from './Utils'
|
||||||
import { promiseTimeout } from '../WAConnection/Utils'
|
import { promiseTimeout, createTimeout } from '../WAConnection/Utils'
|
||||||
|
|
||||||
require ('dotenv').config () // dotenv to load test jid
|
require ('dotenv').config () // dotenv to load test jid
|
||||||
const testJid = process.env.TEST_JID || '1234@s.whatsapp.net' // set TEST_JID=xyz@s.whatsapp.net in a .env file in the root directory
|
const testJid = process.env.TEST_JID || '1234@s.whatsapp.net' // set TEST_JID=xyz@s.whatsapp.net in a .env file in the root directory
|
||||||
|
|
||||||
const createTimeout = (timeout) => new Promise(resolve => setTimeout(resolve, timeout))
|
|
||||||
|
|
||||||
async function sendAndRetreiveMessage(client: WAClient, content, type: MessageType, options: MessageOptions = {}) {
|
async function sendAndRetreiveMessage(client: WAClient, content, type: MessageType, options: MessageOptions = {}) {
|
||||||
const response = await client.sendMessage(testJid, content, type, options)
|
const response = await client.sendMessage(testJid, content, type, options)
|
||||||
assert.strictEqual(response.status, 200)
|
assert.strictEqual(response.status, 200)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
WATag,
|
WATag,
|
||||||
MessageLogLevel,
|
MessageLogLevel,
|
||||||
AuthenticationCredentialsBrowser,
|
AuthenticationCredentialsBrowser,
|
||||||
|
Browsers,
|
||||||
} from './Constants'
|
} from './Constants'
|
||||||
|
|
||||||
/** Generate a QR code from the ref & the curve public key. This is scanned by the phone */
|
/** Generate a QR code from the ref & the curve public key. This is scanned by the phone */
|
||||||
@@ -22,9 +23,9 @@ const generateQRCode = function ([ref, publicKey, clientID]) {
|
|||||||
|
|
||||||
export default class WAConnectionBase {
|
export default class WAConnectionBase {
|
||||||
/** The version of WhatsApp Web we're telling the servers we are */
|
/** The version of WhatsApp Web we're telling the servers we are */
|
||||||
version: [number, number, number] = [2, 2025, 6]
|
version: [number, number, number] = [2, 2027, 10]
|
||||||
/** The Browser we're telling the WhatsApp Web servers we are */
|
/** The Browser we're telling the WhatsApp Web servers we are */
|
||||||
browserDescription: [string, string] = ['Baileys', 'Baileys']
|
browserDescription: [string, string, string] = Browsers.baileys ('Baileys')
|
||||||
/** Metadata like WhatsApp id, name set on WhatsApp etc. */
|
/** Metadata like WhatsApp id, name set on WhatsApp etc. */
|
||||||
userMetaData: UserMetaData = { id: null, name: null, phone: null }
|
userMetaData: UserMetaData = { id: null, name: null, phone: null }
|
||||||
/** Should reconnect automatically after an unexpected disconnect */
|
/** Should reconnect automatically after an unexpected disconnect */
|
||||||
|
|||||||
39
src/WAConnection/BrowserMessageDecoding.ts
Normal file
39
src/WAConnection/BrowserMessageDecoding.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import { decryptWA } from './Utils'
|
||||||
|
import Decoder from '../Binary/Decoder'
|
||||||
|
|
||||||
|
interface BrowserMessagesInfo {
|
||||||
|
encKey: string,
|
||||||
|
macKey: string,
|
||||||
|
messages: string[]
|
||||||
|
}
|
||||||
|
const file = fs.readFileSync ('./browser-messages.json', {encoding: 'utf-8'})
|
||||||
|
const json: BrowserMessagesInfo = JSON.parse (file)
|
||||||
|
|
||||||
|
const encKey = Buffer.from (json.encKey, 'base64')
|
||||||
|
const macKey = Buffer.from (json.macKey, 'base64')
|
||||||
|
|
||||||
|
const decrypt = buffer => {
|
||||||
|
try {
|
||||||
|
return decryptWA (buffer, macKey, encKey, new Decoder())
|
||||||
|
} catch {
|
||||||
|
return decryptWA (buffer, macKey, encKey, new Decoder(), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json.messages.forEach ((str, i) => {
|
||||||
|
const buffer = Buffer.from (str, 'hex')
|
||||||
|
try {
|
||||||
|
const [tag, json, binaryTags] = decrypt (buffer)
|
||||||
|
console.log (
|
||||||
|
`
|
||||||
|
${i}.
|
||||||
|
messageTag: ${tag}
|
||||||
|
output: ${JSON.stringify(json)}
|
||||||
|
binaryTags: ${binaryTags}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error (`received error in decoding ${i}: ${error}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -2,6 +2,7 @@ import WS from 'ws'
|
|||||||
import * as Utils from './Utils'
|
import * as Utils from './Utils'
|
||||||
import { AuthenticationCredentialsBase64, UserMetaData, WAMessage, WAChat, WAContact, MessageLogLevel } from './Constants'
|
import { AuthenticationCredentialsBase64, UserMetaData, WAMessage, WAChat, WAContact, MessageLogLevel } from './Constants'
|
||||||
import WAConnectionValidator from './Validation'
|
import WAConnectionValidator from './Validation'
|
||||||
|
import Decoder from '../Binary/Decoder'
|
||||||
|
|
||||||
export default class WAConnectionConnector extends WAConnectionValidator {
|
export default class WAConnectionConnector extends WAConnectionValidator {
|
||||||
/**
|
/**
|
||||||
@@ -140,48 +141,12 @@ export default class WAConnectionConnector extends WAConnectionValidator {
|
|||||||
const timestamp = message.slice(1, message.length)
|
const timestamp = message.slice(1, message.length)
|
||||||
this.lastSeen = new Date(parseInt(timestamp))
|
this.lastSeen = new Date(parseInt(timestamp))
|
||||||
} else {
|
} else {
|
||||||
const commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message
|
const decrypted = Utils.decryptWA (message, this.authInfo.macKey, this.authInfo.encKey, new Decoder())
|
||||||
|
if (!decrypted) {
|
||||||
if (commaIndex < 0) {
|
|
||||||
// if there was no comma, then this message must be not be valid
|
|
||||||
throw [2, 'invalid message', message]
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = message.slice(commaIndex + 1, message.length)
|
|
||||||
// get the message tag.
|
|
||||||
// If a query was done, the server will respond with the same message tag we sent the query with
|
|
||||||
const messageTag = message.slice(0, commaIndex).toString()
|
|
||||||
if (data.length === 0) {
|
|
||||||
// got an empty message, usually get one after sending a query with the 128 tag
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const [messageTag, json] = decrypted
|
||||||
|
|
||||||
let json
|
|
||||||
if (data[0] === '[' || data[0] === '{') {
|
|
||||||
// if the first character is a "[", then the data must just be plain JSON array or object
|
|
||||||
json = JSON.parse(data) // parse the JSON
|
|
||||||
} else if (this.authInfo.macKey && this.authInfo.encKey) {
|
|
||||||
/*
|
|
||||||
If the data recieved was not a JSON, then it must be an encrypted message.
|
|
||||||
Such a message can only be decrypted if we're connected successfully to the servers & have encryption keys
|
|
||||||
*/
|
|
||||||
|
|
||||||
const checksum = data.slice(0, 32) // the first 32 bytes of the buffer are the HMAC sign of the message
|
|
||||||
data = data.slice(32, data.length) // the actual message
|
|
||||||
|
|
||||||
const computedChecksum = Utils.hmacSign(data, this.authInfo.macKey) // compute the sign of the message we recieved using our macKey
|
|
||||||
|
|
||||||
if (checksum.equals(computedChecksum)) {
|
|
||||||
// the checksum the server sent, must match the one we computed for the message to be valid
|
|
||||||
const decrypted = Utils.aesDecrypt(data, this.authInfo.encKey) // decrypt using AES
|
|
||||||
json = this.decoder.read(decrypted) // decode the binary message into a JSON array
|
|
||||||
} else {
|
|
||||||
throw [7, "checksums don't match"]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if we recieved a message that was encrypted but we don't have the keys, then there must be an error
|
|
||||||
throw [3, 'recieved encrypted message when auth creds not available', message]
|
|
||||||
}
|
|
||||||
if (this.logLevel === MessageLogLevel.all) {
|
if (this.logLevel === MessageLogLevel.all) {
|
||||||
this.log(messageTag + ', ' + JSON.stringify(json))
|
this.log(messageTag + ', ' + JSON.stringify(json))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { WA } from '../Binary/Constants'
|
import { WA } from '../Binary/Constants'
|
||||||
import { proto } from '../../WAMessage/WAMessage'
|
import { proto } from '../../WAMessage/WAMessage'
|
||||||
|
|
||||||
|
export const Browsers: Record<string, (string) => [string, string, string]> = {
|
||||||
|
ubuntu: browser => ['Ubuntu', browser, '18.04'],
|
||||||
|
macOS: browser => ['Mac OS', browser, '10.15.3'],
|
||||||
|
baileys: browser => ['Baileys', browser, '2.0']
|
||||||
|
}
|
||||||
export enum MessageLogLevel {
|
export enum MessageLogLevel {
|
||||||
none=0,
|
none=0,
|
||||||
unhandled=1,
|
unhandled=1,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as assert from 'assert'
|
import * as assert from 'assert'
|
||||||
|
import * as QR from 'qrcode-terminal'
|
||||||
import WAConnection from './WAConnection'
|
import WAConnection from './WAConnection'
|
||||||
import { AuthenticationCredentialsBase64 } from './Constants'
|
import { AuthenticationCredentialsBase64 } from './Constants'
|
||||||
|
import { createTimeout } from './Utils'
|
||||||
|
|
||||||
describe('QR generation', () => {
|
describe('QR generation', () => {
|
||||||
it('should generate QR', async () => {
|
it('should generate QR', async () => {
|
||||||
@@ -29,6 +31,23 @@ describe('Test Connect', () => {
|
|||||||
conn.close()
|
conn.close()
|
||||||
auth = conn.base64EncodedAuthInfo()
|
auth = conn.base64EncodedAuthInfo()
|
||||||
})
|
})
|
||||||
|
it('should re-generate QR & connect', async () => {
|
||||||
|
const conn = new WAConnection()
|
||||||
|
conn.onReadyForPhoneAuthentication = async ([ref, publicKey, clientID]) => {
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
console.log ('called QR ' + i + ' times')
|
||||||
|
await createTimeout (3000)
|
||||||
|
ref = await conn.generateNewQRCode ()
|
||||||
|
}
|
||||||
|
const str = ref + ',' + publicKey + ',' + clientID
|
||||||
|
QR.generate(str, { small: true })
|
||||||
|
}
|
||||||
|
const user = await conn.connectSlim(null)
|
||||||
|
assert.ok(user)
|
||||||
|
assert.ok(user.id)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
})
|
||||||
it('should reconnect', async () => {
|
it('should reconnect', async () => {
|
||||||
const conn = new WAConnection()
|
const conn = new WAConnection()
|
||||||
const [user, chats, contacts, unread] = await conn.connect(auth, 20*1000)
|
const [user, chats, contacts, unread] = await conn.connect(auth, 20*1000)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import * as Crypto from 'crypto'
|
import * as Crypto from 'crypto'
|
||||||
import HKDF from 'futoin-hkdf'
|
import HKDF from 'futoin-hkdf'
|
||||||
|
import Decoder from '../Binary/Decoder'
|
||||||
|
import { off } from 'process'
|
||||||
|
|
||||||
/** decrypt AES 256 CBC; where the IV is prefixed to the buffer */
|
/** decrypt AES 256 CBC; where the IV is prefixed to the buffer */
|
||||||
|
|
||||||
export function aesDecrypt(buffer: Buffer, key: Buffer) {
|
export function aesDecrypt(buffer: Buffer, key: Buffer) {
|
||||||
return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16))
|
return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16))
|
||||||
}
|
}
|
||||||
@@ -37,6 +38,7 @@ export function hkdf(buffer: Buffer, expandedLength: number, info = null) {
|
|||||||
export function randomBytes(length) {
|
export function randomBytes(length) {
|
||||||
return Crypto.randomBytes(length)
|
return Crypto.randomBytes(length)
|
||||||
}
|
}
|
||||||
|
export const createTimeout = (timeout) => new Promise(resolve => setTimeout(resolve, timeout))
|
||||||
export function promiseTimeout<T>(ms: number, promise: Promise<T>) {
|
export function promiseTimeout<T>(ms: number, promise: Promise<T>) {
|
||||||
if (!ms) {
|
if (!ms) {
|
||||||
return promise
|
return promise
|
||||||
@@ -66,10 +68,59 @@ export function generateMessageID() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function errorOnNon200Status(p: Promise<any>) {
|
export function errorOnNon200Status(p: Promise<any>) {
|
||||||
return p.then((json) => {
|
return p.then(json => {
|
||||||
if (json.status && typeof json.status === 'number' && Math.floor(json.status / 100) !== 2) {
|
if (json.status && typeof json.status === 'number' && Math.floor(json.status / 100) !== 2) {
|
||||||
throw new Error(`Unexpected status code: ${json.status}`)
|
throw new Error(`Unexpected status code: ${json.status}`)
|
||||||
}
|
}
|
||||||
return json
|
return json
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function decryptWA (message: any, macKey: Buffer, encKey: Buffer, decoder: Decoder, fromMe: boolean=false): [string, Object, [number, number]?] {
|
||||||
|
const commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message
|
||||||
|
if (commaIndex < 0) {
|
||||||
|
// if there was no comma, then this message must be not be valid
|
||||||
|
throw [2, 'invalid message', message]
|
||||||
|
}
|
||||||
|
let data = message.slice(commaIndex+1, message.length)
|
||||||
|
// get the message tag.
|
||||||
|
// If a query was done, the server will respond with the same message tag we sent the query with
|
||||||
|
const messageTag: string = message.slice(0, commaIndex).toString()
|
||||||
|
if (data.length === 0) {
|
||||||
|
// got an empty message, usually get one after sending a query with the 128 tag
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let json
|
||||||
|
let tags = null
|
||||||
|
if (data[0] === '[' || data[0] === '{') {
|
||||||
|
// if the first character is a "[", then the data must just be plain JSON array or object
|
||||||
|
json = JSON.parse(data) // parse the JSON
|
||||||
|
} else {
|
||||||
|
if (!macKey || !encKey) {
|
||||||
|
// if we recieved a message that was encrypted but we don't have the keys, then there must be an error
|
||||||
|
throw [3, 'recieved encrypted message when auth creds not available', data]
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
If the data recieved was not a JSON, then it must be an encrypted message.
|
||||||
|
Such a message can only be decrypted if we're connected successfully to the servers & have encryption keys
|
||||||
|
*/
|
||||||
|
if (fromMe) {
|
||||||
|
tags = [data[0], data[1]]
|
||||||
|
data = data.slice(2, data.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
const checksum = data.slice(0, 32) // the first 32 bytes of the buffer are the HMAC sign of the message
|
||||||
|
data = data.slice(32, data.length) // the actual message
|
||||||
|
const computedChecksum = hmacSign(data, macKey) // compute the sign of the message we recieved using our macKey
|
||||||
|
|
||||||
|
if (!checksum.equals(computedChecksum)) {
|
||||||
|
throw [7, "checksums don't match"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// the checksum the server sent, must match the one we computed for the message to be valid
|
||||||
|
const decrypted = aesDecrypt(data, encKey) // decrypt using AES
|
||||||
|
json = decoder.read(decrypted) // decode the binary message into a JSON array
|
||||||
|
}
|
||||||
|
return [messageTag, json, tags]
|
||||||
|
}
|
||||||
@@ -74,10 +74,14 @@ export default class WAConnectionValidator extends WAConnectionBase {
|
|||||||
return this.userMetaData
|
return this.userMetaData
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/** Refresh QR Code */
|
/**
|
||||||
protected refreshQRCode() {
|
* Refresh QR Code
|
||||||
|
* @returns the new ref
|
||||||
|
*/
|
||||||
|
async generateNewQRCode() {
|
||||||
const data = ['admin', 'Conn', 'reref']
|
const data = ['admin', 'Conn', 'reref']
|
||||||
return this.query(data)
|
const response = await this.query(data)
|
||||||
|
return response.ref as string
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Once the QR code is scanned and we can validate our connection, or we resolved the challenge when logging back in
|
* Once the QR code is scanned and we can validate our connection, or we resolved the challenge when logging back in
|
||||||
@@ -154,31 +158,14 @@ export default class WAConnectionValidator extends WAConnectionBase {
|
|||||||
this.log('resolving login challenge')
|
this.log('resolving login challenge')
|
||||||
return this.query(data)
|
return this.query(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
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
protected async generateKeysForAuth(ref: string) {
|
protected async generateKeysForAuth(ref: string) {
|
||||||
this.curveKeys = Curve.generateKeyPair(Utils.randomBytes(32))
|
this.curveKeys = Curve.generateKeyPair(Utils.randomBytes(32))
|
||||||
|
this.onReadyForPhoneAuthentication([
|
||||||
let retries = 0
|
ref,
|
||||||
let _ref = ref
|
Buffer.from(this.curveKeys.public).toString('base64'),
|
||||||
|
this.authInfo.clientID,
|
||||||
while (retries < 5) {
|
])
|
||||||
retries++
|
return this.waitForMessage('s1', [])
|
||||||
|
|
||||||
this.onReadyForPhoneAuthentication([
|
|
||||||
_ref,
|
|
||||||
Buffer.from(this.curveKeys.public).toString('base64'),
|
|
||||||
this.authInfo.clientID,
|
|
||||||
])
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await this.waitForMessage('s1', [], 20 * 1000)
|
|
||||||
} catch (err) {
|
|
||||||
const json = await this.refreshQRCode()
|
|
||||||
_ref = json.ref
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user