- 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 conn.logLevel = MessageLogLevel.info // set to unhandled to see what kind of stuff you can implement
// loads the auth file credentials if present // loads the auth file credentials if present
if (fs.existsSync('./auth_info.json')) conn.loadAuthInfo ('./auth_info.json') fs.existsSync('./auth_info.json') && conn.loadAuthInfo ('./auth_info.json')
conn.on ('qr', qr => console.log (qr))
// connect or timeout in 30 seconds /* Called when contacts are received,
await conn.connect({ timeoutMs: 30 * 1000 }) * 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 () const unread = await conn.loadAllUnreadMessages ()
console.log('oh hello ' + conn.user.name + ' (' + conn.user.id + ')') 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') console.log ('you have ' + unread.length + ' unread messages')
const authInfo = conn.base64EncodedAuthInfo() // get all the auth info we need to restore this session 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 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, /* 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 */ 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 ('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 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}`) 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 {promises as fs} from 'fs'
import * as assert from 'assert' import * as assert from 'assert'
import { WAConnectionTest, testJid, sendAndRetreiveMessage } from './Common' import { WAConnectionTest, testJid, sendAndRetreiveMessage } from './Common'
@@ -79,7 +79,7 @@ WAConnectionTest('Message Events', (conn) => {
resolve(update) resolve(update)
} }
}) })
}) as Promise<MessageStatusUpdate> }) as Promise<WAMessageStatusUpdate>
const response = await conn.sendMessage(testJid, 'My Name Jeff', MessageType.text) const response = await conn.sendMessage(testJid, 'My Name Jeff', MessageType.text)
const m = await waitForUpdate const m = await waitForUpdate
assert.ok (m.type >= WA_MESSAGE_STATUS_TYPE.DELIVERY_ACK) assert.ok (m.type >= WA_MESSAGE_STATUS_TYPE.DELIVERY_ACK)

View File

@@ -45,7 +45,6 @@ export class WAConnection extends EventEmitter {
maxCachedMessages = 25 maxCachedMessages = 25
contacts: {[k: string]: WAContact} = {}
chats: KeyedDB<WAChat> = new KeyedDB (Utils.waChatUniqueKey, value => value.jid) chats: KeyedDB<WAChat> = new KeyedDB (Utils.waChatUniqueKey, value => value.jid)
/** Data structure of tokens & IDs used to establish one's identiy to WhatsApp Web */ /** 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 Curve from 'curve25519-js'
import * as Utils from './Utils' import * as Utils from './Utils'
import {WAConnection as Base} from './0.Base' 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 { export class WAConnection extends Base {
@@ -53,14 +53,16 @@ export class WAConnection extends Base {
// otherwise just chain the promise further // otherwise just chain the promise further
return json return json
}) })
.then(async json => { .then(json => {
this.validateNewConnection(json[1]) // validate the connection this.user = this.validateNewConnection(json[1]) // validate the connection
this.log('validated connection successfully', MessageLogLevel.info) this.log('validated connection successfully', MessageLogLevel.info)
this.sendPostConnectQueries () this.sendPostConnectQueries ()
this.lastSeen = new Date() // set last seen to right now 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 * Send the same queries WA Web sends after connect
@@ -88,25 +90,24 @@ export class WAConnection extends Base {
* @param {object} json * @param {object} json
*/ */
private validateNewConnection(json) { private validateNewConnection(json) {
const onValidationSuccess = () => { // set metadata: one's WhatsApp ID [cc][number]@s.whatsapp.net, name on WhatsApp, info about the phone
// set metadata: one's WhatsApp ID [cc][number]@s.whatsapp.net, name on WhatsApp, info about the phone const onValidationSuccess = () => ({
this.user = { id: Utils.whatsappID(json.wid),
id: Utils.whatsappID(json.wid), name: json.pushname,
name: json.pushname, phone: json.phone,
phone: json.phone, imgUrl: null
imgUrl: null }) as WAUser
}
return this.user
}
if (!json.secret) { if (!json.secret) {
// if we didn't get a secret, we don't need it, we're validated // if we didn't get a secret, we don't need it, we're validated
return onValidationSuccess() return onValidationSuccess()
} }
const secret = Buffer.from(json.secret, 'base64') const secret = Buffer.from(json.secret, 'base64')
if (secret.length !== 144) { if (secret.length !== 144) {
throw new Error ('incorrect secret length received: ' + secret.length) throw new Error ('incorrect secret length received: ' + secret.length)
} }
// generate shared key from our private key & the secret shared by the server // generate shared key from our private key & the secret shared by the server
const sharedKey = Curve.sharedKey(this.curveKeys.private, secret.slice(0, 32)) const sharedKey = Curve.sharedKey(this.curveKeys.private, secret.slice(0, 32))
// expand the key to 80 bytes using HKDF // expand the key to 80 bytes using HKDF
@@ -118,28 +119,28 @@ export class WAConnection extends Base {
const hmac = Utils.hmacSign(hmacValidationMessage, hmacValidationKey) const hmac = Utils.hmacSign(hmacValidationMessage, hmacValidationKey)
if (hmac.equals(secret.slice(32, 64))) { 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 the checksums didn't match // if the checksums didn't match
throw new BaileysError ('HMAC validation failed', json) 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 * 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.phoneConnected = true
this.state = 'open' this.state = 'open'
this.user.imgUrl = await this.getProfilePicture (this.user.id).catch (err => '')
this.registerPhoneConnectionPoll ()
this.emit ('open') this.emit ('open')
this.registerPhoneConnectionPoll ()
this.releasePendingRequests () this.releasePendingRequests ()
this.log ('opened connection to WhatsApp Web', MessageLogLevel.info) this.log ('opened connection to WhatsApp Web', MessageLogLevel.info)
@@ -64,21 +63,14 @@ export class WAConnection extends Base {
throw error 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. * Sets up callbacks to receive chats, contacts & messages.
* Must be called immediately after connect * Must be called immediately after connect
* @returns [chats, contacts] * @returns [chats, contacts]
*/ */
protected async receiveChatsAndContacts(timeoutMs: number = null, stopAfterMostRecentMessage: boolean=false) { protected async receiveChatsAndContacts(timeoutMs: number = null, stopAfterMostRecentMessage: boolean=false) {
this.contacts = {}
this.chats.clear () this.chats.clear ()
let receivedContacts = false
let receivedMessages = false let receivedMessages = false
let resolveTask: () => void let resolveTask: () => void
@@ -90,12 +82,7 @@ export class WAConnection extends Base {
this.deregisterCallback(['action', 'add:unread']) this.deregisterCallback(['action', 'add:unread'])
} }
this.deregisterCallback(['response', 'type:chat']) this.deregisterCallback(['response', 'type:chat'])
this.deregisterCallback(['response', 'type:contacts'])
} }
const checkForResolution = () => {
if (receivedContacts && receivedMessages) resolveTask ()
}
// wait for messages to load // wait for messages to load
const chatUpdate = json => { const chatUpdate = json => {
receivedMessages = true receivedMessages = true
@@ -110,7 +97,7 @@ export class WAConnection extends Base {
}) })
} }
// if received contacts before messages // 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 // 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 // get chats
this.registerCallback(['response', 'type:chat'], json => { this.registerCallback(['response', 'type:chat'], json => {
if (json[1].duplicate || !json[2]) return if (json[1].duplicate) return
json[2] json[2]?.forEach(([item, chat]: [any, WAChat]) => {
.forEach(([item, chat]: [any, WAChat]) => {
if (!chat) { if (!chat) {
this.log (`unexpectedly got null chat: ${item}, ${chat}`, MessageLogLevel.info) this.log (`unexpectedly got null chat: ${item}, ${chat}`, MessageLogLevel.info)
return 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.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 // wait for the chats & contacts to load
await Utils.promiseTimeout (timeoutMs, (resolve, reject) => { await Utils.promiseTimeout (timeoutMs, (resolve, reject) => {
resolveTask = resolve resolveTask = resolve
@@ -167,13 +144,6 @@ export class WAConnection extends Base {
this.on ('close', rejectTask) this.on ('close', rejectTask)
}) })
.finally (deregisterCallbacks) .finally (deregisterCallbacks)
this.chats
.all ()
.forEach (chat => {
const respectiveContact = this.contacts[chat.jid]
chat.name = respectiveContact?.name || respectiveContact?.notify || chat.name
})
} }
private releasePendingRequests () { private releasePendingRequests () {
this.pendingRequests.forEach (({resolve}) => resolve()) // send off all pending request this.pendingRequests.forEach (({resolve}) => resolve()) // send off all pending request

View File

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

View File

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