- Separated message-status-update & message-update
- Removed contact storage & waiting for contacts. General connect speed should improve by 10%-20%
- Added `contacts-received` event
This commit is contained in:
Adhiraj
2020-08-28 20:01:48 +05:30
parent d0c593731e
commit 890fb726f1
7 changed files with 94 additions and 101 deletions

View File

@@ -17,23 +17,29 @@ async function example() {
conn.logLevel = MessageLogLevel.info // set to unhandled to see what kind of stuff you can implement
// loads the auth file credentials if present
if (fs.existsSync('./auth_info.json')) conn.loadAuthInfo ('./auth_info.json')
conn.on ('qr', qr => console.log (qr))
// connect or timeout in 30 seconds
await conn.connect({ timeoutMs: 30 * 1000 })
fs.existsSync('./auth_info.json') && conn.loadAuthInfo ('./auth_info.json')
/* Called when contacts are received,
* do note, that this method may be called before the connection is done completely because WA is funny sometimes
* */
conn.on ('contacts-received', contacts => console.log(`received ${Object.keys(contacts).length} contacts`))
// connect or timeout in 60 seconds
await conn.connect({ timeoutMs: 60 * 1000, retryOnNetworkErrors: true })
const unread = await conn.loadAllUnreadMessages ()
console.log('oh hello ' + conn.user.name + ' (' + conn.user.id + ')')
console.log('you have ' + conn.chats.all().length + ' chats & ' + Object.keys(conn.contacts).length + ' contacts')
console.log('you have ' + conn.chats.all().length + ' chats')
console.log ('you have ' + unread.length + ' unread messages')
const authInfo = conn.base64EncodedAuthInfo() // get all the auth info we need to restore this session
fs.writeFileSync('./auth_info.json', JSON.stringify(authInfo, null, '\t')) // save this info to a file
/* Note: one can take this auth_info.json file and login again from any computer without having to scan the QR code,
and get full access to one's WhatsApp. Despite the convenience, be careful with this file */
conn.on ('user-presence-update', json => console.log(json.id + ' presence is ' + json.type))
conn.on ('message-update', json => {
conn.on ('message-status-update', json => {
const participant = json.participant ? ' (' + json.participant + ')' : '' // participant exists when the message is from a group
console.log(`${json.to}${participant} acknlowledged message(s) ${json.ids} as ${json.type}`)
})

View File

@@ -1,4 +1,4 @@
import { MessageType, Mimetype, delay, promiseTimeout, WAMessage, WA_MESSAGE_STATUS_TYPE, MessageStatusUpdate } from '../WAConnection/WAConnection'
import { MessageType, Mimetype, delay, promiseTimeout, WAMessage, WA_MESSAGE_STATUS_TYPE, WAMessageStatusUpdate } from '../WAConnection/WAConnection'
import {promises as fs} from 'fs'
import * as assert from 'assert'
import { WAConnectionTest, testJid, sendAndRetreiveMessage } from './Common'
@@ -79,7 +79,7 @@ WAConnectionTest('Message Events', (conn) => {
resolve(update)
}
})
}) as Promise<MessageStatusUpdate>
}) as Promise<WAMessageStatusUpdate>
const response = await conn.sendMessage(testJid, 'My Name Jeff', MessageType.text)
const m = await waitForUpdate
assert.ok (m.type >= WA_MESSAGE_STATUS_TYPE.DELIVERY_ACK)

View File

@@ -45,7 +45,6 @@ export class WAConnection extends EventEmitter {
maxCachedMessages = 25
contacts: {[k: string]: WAContact} = {}
chats: KeyedDB<WAChat> = new KeyedDB (Utils.waChatUniqueKey, value => value.jid)
/** Data structure of tokens & IDs used to establish one's identiy to WhatsApp Web */

View File

@@ -1,7 +1,7 @@
import * as Curve from 'curve25519-js'
import * as Utils from './Utils'
import {WAConnection as Base} from './0.Base'
import { MessageLogLevel, WAMetric, WAFlag, BaileysError, Presence } from './Constants'
import { MessageLogLevel, WAMetric, WAFlag, BaileysError, Presence, WAUser } from './Constants'
export class WAConnection extends Base {
@@ -53,14 +53,16 @@ export class WAConnection extends Base {
// otherwise just chain the promise further
return json
})
.then(async json => {
this.validateNewConnection(json[1]) // validate the connection
.then(json => {
this.user = this.validateNewConnection(json[1]) // validate the connection
this.log('validated connection successfully', MessageLogLevel.info)
this.sendPostConnectQueries ()
this.lastSeen = new Date() // set last seen to right now
})
// load profile picture
.then (() => this.query({ json: ['query', 'ProfilePicThumb', this.user.id], waitForOpen: false, expect200: false }))
.then (response => this.user.imgUrl = response?.eurl || '')
}
/**
* Send the same queries WA Web sends after connect
@@ -88,25 +90,24 @@ export class WAConnection extends Base {
* @param {object} json
*/
private validateNewConnection(json) {
const onValidationSuccess = () => {
// set metadata: one's WhatsApp ID [cc][number]@s.whatsapp.net, name on WhatsApp, info about the phone
this.user = {
id: Utils.whatsappID(json.wid),
name: json.pushname,
phone: json.phone,
imgUrl: null
}
return this.user
}
// set metadata: one's WhatsApp ID [cc][number]@s.whatsapp.net, name on WhatsApp, info about the phone
const onValidationSuccess = () => ({
id: Utils.whatsappID(json.wid),
name: json.pushname,
phone: json.phone,
imgUrl: null
}) as WAUser
if (!json.secret) {
// if we didn't get a secret, we don't need it, we're validated
return onValidationSuccess()
}
const secret = Buffer.from(json.secret, 'base64')
if (secret.length !== 144) {
throw new Error ('incorrect secret length received: ' + secret.length)
}
// generate shared key from our private key & the secret shared by the server
const sharedKey = Curve.sharedKey(this.curveKeys.private, secret.slice(0, 32))
// expand the key to 80 bytes using HKDF
@@ -118,28 +119,28 @@ export class WAConnection extends Base {
const hmac = Utils.hmacSign(hmacValidationMessage, hmacValidationKey)
if (hmac.equals(secret.slice(32, 64))) {
// computed HMAC should equal secret[32:64]
// expandedKey[64:] + secret[64:] are the keys, encrypted using AES, that are used to encrypt/decrypt the messages recieved from WhatsApp
// they are encrypted using key: expandedKey[0:32]
const encryptedAESKeys = Buffer.concat([
expandedKey.slice(64, expandedKey.length),
secret.slice(64, secret.length),
])
const decryptedKeys = Utils.aesDecrypt(encryptedAESKeys, expandedKey.slice(0, 32))
// set the credentials
this.authInfo = {
encKey: decryptedKeys.slice(0, 32), // first 32 bytes form the key to encrypt/decrypt messages
macKey: decryptedKeys.slice(32, 64), // last 32 bytes from the key to sign messages
clientToken: json.clientToken,
serverToken: json.serverToken,
clientID: this.authInfo.clientID,
}
return onValidationSuccess()
} else {
if (!hmac.equals(secret.slice(32, 64))) {
// if the checksums didn't match
throw new BaileysError ('HMAC validation failed', json)
}
// computed HMAC should equal secret[32:64]
// expandedKey[64:] + secret[64:] are the keys, encrypted using AES, that are used to encrypt/decrypt the messages recieved from WhatsApp
// they are encrypted using key: expandedKey[0:32]
const encryptedAESKeys = Buffer.concat([
expandedKey.slice(64, expandedKey.length),
secret.slice(64, secret.length),
])
const decryptedKeys = Utils.aesDecrypt(encryptedAESKeys, expandedKey.slice(0, 32))
// set the credentials
this.authInfo = {
encKey: decryptedKeys.slice(0, 32), // first 32 bytes form the key to encrypt/decrypt messages
macKey: decryptedKeys.slice(32, 64), // last 32 bytes from the key to sign messages
clientToken: json.clientToken,
serverToken: json.serverToken,
clientID: this.authInfo.clientID,
}
return onValidationSuccess()
}
/**
* When logging back in (restoring a previously closed session), WhatsApp may challenge one to check if one still has the encryption keys

View File

@@ -47,10 +47,9 @@ export class WAConnection extends Base {
this.phoneConnected = true
this.state = 'open'
this.user.imgUrl = await this.getProfilePicture (this.user.id).catch (err => '')
this.registerPhoneConnectionPoll ()
this.emit ('open')
this.registerPhoneConnectionPoll ()
this.releasePendingRequests ()
this.log ('opened connection to WhatsApp Web', MessageLogLevel.info)
@@ -64,21 +63,14 @@ export class WAConnection extends Base {
throw error
}
}
/** Get the URL to download the profile picture of a person/group */
async getProfilePicture(jid: string | null) {
const response = await this.query({ json: ['query', 'ProfilePicThumb', jid || this.user.id] })
return response.eurl as string
}
/**
* Sets up callbacks to receive chats, contacts & messages.
* Must be called immediately after connect
* @returns [chats, contacts]
*/
protected async receiveChatsAndContacts(timeoutMs: number = null, stopAfterMostRecentMessage: boolean=false) {
this.contacts = {}
this.chats.clear ()
let receivedContacts = false
let receivedMessages = false
let resolveTask: () => void
@@ -90,12 +82,7 @@ export class WAConnection extends Base {
this.deregisterCallback(['action', 'add:unread'])
}
this.deregisterCallback(['response', 'type:chat'])
this.deregisterCallback(['response', 'type:contacts'])
}
const checkForResolution = () => {
if (receivedContacts && receivedMessages) resolveTask ()
}
}
// wait for messages to load
const chatUpdate = json => {
receivedMessages = true
@@ -110,7 +97,7 @@ export class WAConnection extends Base {
})
}
// if received contacts before messages
if (isLast && receivedContacts) checkForResolution ()
if (isLast) resolveTask ()
}
// wait for actual messages to load, "last" is the most recent message, "before" contains prior messages
@@ -121,10 +108,9 @@ export class WAConnection extends Base {
}
// get chats
this.registerCallback(['response', 'type:chat'], json => {
if (json[1].duplicate || !json[2]) return
if (json[1].duplicate) return
json[2]
.forEach(([item, chat]: [any, WAChat]) => {
json[2]?.forEach(([item, chat]: [any, WAChat]) => {
if (!chat) {
this.log (`unexpectedly got null chat: ${item}, ${chat}`, MessageLogLevel.info)
return
@@ -140,23 +126,14 @@ export class WAConnection extends Base {
this.chats.insert (chat) // chats data (log json to see what it looks like)
})
this.log ('received chats list', MessageLogLevel.info)
this.log (`received ${this.chats.all().length} chats`, MessageLogLevel.info)
// if there are no chats
if (this.chats.all().length === 0) {
receivedMessages = true
resolveTask ()
}
})
// get contacts
this.registerCallback(['response', 'type:contacts'], json => {
if (json[1].duplicate) return
receivedContacts = true
json[2].forEach(([type, contact]: ['user', WAContact]) => {
if (!contact) return this.log (`unexpectedly got null contact: ${type}, ${contact}`, MessageLogLevel.info)
contact.jid = Utils.whatsappID (contact.jid)
this.contacts[contact.jid] = contact
})
this.log ('received contacts list', MessageLogLevel.info)
checkForResolution ()
})
// wait for the chats & contacts to load
await Utils.promiseTimeout (timeoutMs, (resolve, reject) => {
resolveTask = resolve
@@ -167,13 +144,6 @@ export class WAConnection extends Base {
this.on ('close', rejectTask)
})
.finally (deregisterCallbacks)
this.chats
.all ()
.forEach (chat => {
const respectiveContact = this.contacts[chat.jid]
chat.name = respectiveContact?.name || respectiveContact?.notify || chat.name
})
}
private releasePendingRequests () {
this.pendingRequests.forEach (({resolve}) => resolve()) // send off all pending request

View File

@@ -1,6 +1,6 @@
import * as QR from 'qrcode-terminal'
import { WAConnection as Base } from './3.Connect'
import { MessageStatusUpdate, WAMessage, WAContact, WAChat, WAMessageProto, WA_MESSAGE_STUB_TYPE, WA_MESSAGE_STATUS_TYPE, MessageLogLevel, PresenceUpdate, BaileysEvent, DisconnectReason, WANode } from './Constants'
import { WAMessageStatusUpdate, WAMessage, WAContact, WAChat, WAMessageProto, WA_MESSAGE_STUB_TYPE, WA_MESSAGE_STATUS_TYPE, MessageLogLevel, PresenceUpdate, BaileysEvent, DisconnectReason, WANode } from './Constants'
import { whatsappID, unixTimestampSeconds, isGroupID } from './Utils'
export class WAConnection extends Base {
@@ -30,8 +30,7 @@ export class WAConnection extends Base {
if (node) {
const user = node[1] as WAContact
user.jid = whatsappID(user.jid)
this.contacts[user.jid] = user
const chat = this.chats.get (user.jid)
if (chat) {
chat.name = user.name || user.notify
@@ -104,6 +103,19 @@ export class WAConnection extends Base {
this.emit ('chat-update', { jid: chat.jid, count: chat.count })
})
// get contacts
this.registerCallback(['response', 'type:contacts'], json => {
if (json[1].duplicate || !json[2]) return
const contacts: {[k: string]: WAContact} = {}
json[2].forEach(([type, contact]: ['user', WAContact]) => {
if (!contact) return this.log (`unexpectedly got null contact: ${type}, ${contact}`, MessageLogLevel.info)
contact.jid = whatsappID (contact.jid)
contacts[contact.jid] = contact
})
this.emit ('contacts-received', contacts)
})
/*// genetic chat action
this.registerCallback (['Chat', 'cmd:action'], json => {
const data = json[1].data as WANode
@@ -118,6 +130,11 @@ 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 */
async getProfilePicture(jid: string | null) {
const response = await this.query({ json: ['query', 'ProfilePicThumb', jid || this.user.id] })
return response.eurl as string
}
/** Set the callback for message status updates (when a message is delivered, read etc.) */
protected registerOnMessageStatusChange() {
const func = json => {
@@ -126,7 +143,7 @@ export class WAConnection extends Base {
if (json.cmd === 'ack') ids = [json.id]
const update: MessageStatusUpdate = {
const update: WAMessageStatusUpdate = {
from: json.from,
to: json.to,
participant: json.participant,
@@ -138,7 +155,7 @@ export class WAConnection extends Base {
const chat = this.chats.get( whatsappID(update.to) )
if (!chat) return
this.emit ('message-update', update)
this.emit ('message-status-update', update)
this.chatUpdatedMessage (update.ids, update.type as number, chat)
}
this.registerCallback('Msg', func)
@@ -199,14 +216,8 @@ export class WAConnection extends Base {
found.messageStubType = WA_MESSAGE_STUB_TYPE.REVOKE
found.message = null
const update: MessageStatusUpdate = {
from: this.user.id,
to: message.key.remoteJid,
ids: [message.key.id],
timestamp: new Date(),
type: 'delete'
}
this.emit ('message-update', update)
this.emit ('message-update', found)
}
break
default:
@@ -294,14 +305,18 @@ export class WAConnection extends Base {
on (event: 'user-presence-update', listener: (update: PresenceUpdate) => void): this
/** when a user's status is updated */
on (event: 'user-status-update', listener: (update: {jid: string, status?: string}) => void): this
/** when a user receives contacts */
on (event: 'contacts-received', listener: (contacts: {[k: string]: WAContact}) => void): this
/** when a new chat is added */
on (event: 'chat-new', listener: (chat: WAChat) => void): this
/** when a chat is updated (archived, deleted, pinned) */
on (event: 'chat-update', listener: (chat: Partial<WAChat> & { jid: string }) => void): this
/** when a new message is relayed */
on (event: 'message-new', listener: (message: WAMessage) => void): this
/** when a message is updated (deleted, delivered, read, sent etc.) */
on (event: 'message-update', listener: (message: MessageStatusUpdate) => void): this
/** when a message object itself is updated (receives its media info or is deleted) */
on (event: 'message-update', listener: (message: WAMessage) => void): this
/** when a message's status is updated (deleted, delivered, read, sent etc.) */
on (event: 'message-status-update', listener: (message: WAMessageStatusUpdate) => void): this
/** when participants are added to a group */
on (event: 'group-participants-add', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this
/** when participants are removed or leave from a group */

View File

@@ -287,7 +287,7 @@ export interface MessageInfo {
reads: {jid: string, t: string}[]
deliveries: {jid: string, t: string}[]
}
export interface MessageStatusUpdate {
export interface WAMessageStatusUpdate {
from: string
to: string
/** Which participant caused the update (only for groups) */
@@ -296,7 +296,7 @@ export interface MessageStatusUpdate {
/** Message IDs read/delivered */
ids: string[]
/** Status of the Message IDs */
type: WA_MESSAGE_STATUS_TYPE | 'delete'
type: WA_MESSAGE_STATUS_TYPE
}
export enum GroupSettingChange {
messageSend = 'announcement',
@@ -338,10 +338,12 @@ export type BaileysEvent =
'connection-phone-change' |
'user-presence-update' |
'user-status-update' |
'contacts-received' |
'chat-new' |
'chat-update' |
'message-new' |
'message-update' |
'message-status-update' |
'group-participants-add' |
'group-participants-remove' |
'group-participants-promote' |