mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
Changes
- 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:
@@ -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}`)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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' |
|
||||
|
||||
Reference in New Issue
Block a user