Added features:
-search messages
-delete messages
-archive chats
-mark chats as unread

Fixes
-decoding audio/video messages
This commit is contained in:
Adhiraj Singh
2020-07-08 12:38:51 +05:30
parent 93b43709ed
commit b9df538764
7 changed files with 142 additions and 45 deletions

View File

@@ -174,7 +174,13 @@ To note:
``` ts ``` ts
client.sendReadReceipt(id, messageID) client.sendReadReceipt(id, messageID)
``` ```
The id is in the same format as mentioned earlier. The message ID is the unique identifier of the message that you are marking as read. On a `WAMessage`, it can be accessed using ```messageID = message.key.id```.
Also, to mark a chat unread:
``` ts
client.markChatUnread(id)
```
`id` is in the same format as mentioned earlier. The message ID is the unique identifier of the message that you are marking as read. On a `WAMessage`, it can be accessed using ```messageID = message.key.id```.
## Update Presence ## Update Presence
``` ts ``` ts
@@ -206,6 +212,19 @@ If you want to save & process some images, videos, documents or stickers you rec
} }
``` ```
## Deleting Messages
``` ts
const jid = '1234@s.whatsapp.net' // can also be a group
const response = await client.sendMessage (jid, 'hello!', MessageType.text) // send a message
await client.deleteMessage (jid, {id: response.messageID, remoteJid: jid, fromMe: true}) // will delete the sent message!
```
You can also archive a chat using:
``` ts
await client.archiveChat(jid)
```
## Querying ## Querying
- To check if a given ID is on WhatsApp - To check if a given ID is on WhatsApp
``` ts ``` ts
@@ -238,12 +257,15 @@ If you want to save & process some images, videos, documents or stickers you rec
``` ts ``` ts
// the presence update is fetched and called here // the presence update is fetched and called here
client.setOnPresenceUpdate (json => console.log(json.id + " presence is " + json.type)) client.setOnPresenceUpdate (json => console.log(json.id + " presence is " + json.type))
await client.requestPresenceUpdate ("xyz@c.us") // request the update
await client.requestPresenceUpdate ("xyz@c.us") ```
- To search through messages
``` ts
const response = await client.searchMessages ('so cool', 25, 0) // get 25 messages of the first page of results
console.log (`got ${response.messages.length} messages in search`)
``` ```
Of course, replace ``` xyz ``` with an actual ID. Of course, replace ``` xyz ``` with an actual ID.
Also, append ``` @s.whatsapp.net ``` for individuals & ``` @g.us ``` for groups. Append ``` @s.whatsapp.net ``` for individuals & ``` @g.us ``` for groups.
## Groups ## Groups
- To query the metadata of a group - To query the metadata of a group
@@ -260,19 +282,17 @@ Also, append ``` @s.whatsapp.net ``` for individuals & ``` @g.us ``` for groups.
``` ```
- To add people to a group - To add people to a group
``` ts ``` ts
// id & people to add to the group // id & people to add to the group (will throw error if it fails)
const response = await client.groupAdd ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"]) const response = await client.groupAdd ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"])
console.log("added successfully: " + (response.status===200))
``` ```
- To make someone admin on a group - To make someone admin on a group
``` ts ``` ts
const response = await client.groupMakeAdmin ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"]) // id & people to make admin // id & people to make admin (will throw error if it fails)
console.log("made admin successfully: " + (response.status===200)) await client.groupMakeAdmin ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"])
``` ```
- To leave a group - To leave a group
``` ts ``` ts
const response = await client.groupLeave ("abcd-xyz@g.us") await client.groupLeave ("abcd-xyz@g.us") // (will throw error if it fails)
console.log("left group successfully: " + (response.status===200))
``` ```
- To get the invite code for a group - To get the invite code for a group
``` ts ``` ts

View File

@@ -1,6 +1,6 @@
{ {
"name": "@adiwajshing/baileys", "name": "@adiwajshing/baileys",
"version": "2.0.1", "version": "2.1.0",
"description": "WhatsApp Web API", "description": "WhatsApp Web API",
"homepage": "https://github.com/adiwajshing/Baileys", "homepage": "https://github.com/adiwajshing/Baileys",
"main": "lib/WAClient/WAClient.js", "main": "lib/WAClient/WAClient.js",

View File

@@ -1,5 +1,5 @@
import WAConnection from '../WAConnection/WAConnection' import WAConnection from '../WAConnection/WAConnection'
import { MessageStatus, MessageStatusUpdate, PresenceUpdate } from './Constants' import { MessageStatus, MessageStatusUpdate, PresenceUpdate, Presence } from './Constants'
import { import {
WAMessage, WAMessage,
WANode, WANode,
@@ -63,11 +63,25 @@ export default class WhatsAppWebBase extends WAConnection {
} }
/** Query whether a given number is registered on WhatsApp */ /** Query whether a given number is registered on WhatsApp */
isOnWhatsApp = (jid: string) => this.query(['query', 'exist', jid]).then((m) => m.status === 200) isOnWhatsApp = (jid: string) => this.query(['query', 'exist', jid]).then((m) => m.status === 200)
/**
* Tell someone about your presence -- online, typing, offline etc.
* @param jid the ID of the person/group who you are updating
* @param type your presence
*/
async updatePresence(jid: string, type: Presence) {
const json = [
'action',
{ epoch: this.msgCount.toString(), type: 'set' },
[['presence', { type: type, to: jid }, null]],
]
return this.queryExpecting200(json, [WAMetric.group, WAFlag.acknowledge]) as Promise<{ status: number }>
}
/** Request an update on the presence of a user */ /** Request an update on the presence of a user */
requestPresenceUpdate = (jid: string) => this.queryExpecting200(['action', 'presence', 'subscribe', jid]) requestPresenceUpdate = async (jid: string) => this.queryExpecting200(['action', 'presence', 'subscribe', jid])
/** Query the status of the person (see groupMetadata() for groups) */ /** Query the status of the person (see groupMetadata() for groups) */
getStatus = (jid: string | null) => async getStatus (jid?: string) {
this.query(['query', 'Status', jid || this.userMetaData.id]) as Promise<{ status: string }> return this.query(['query', 'Status', jid || this.userMetaData.id]) as Promise<{ status: string }>
}
/** Get the URL to download the profile picture of a person/group */ /** Get the URL to download the profile picture of a person/group */
async getProfilePicture(jid: string | null) { async getProfilePicture(jid: string | null) {
const response = await this.queryExpecting200(['query', 'ProfilePicThumb', jid || this.userMetaData.id]) const response = await this.queryExpecting200(['query', 'ProfilePicThumb', jid || this.userMetaData.id])
@@ -85,6 +99,18 @@ export default class WhatsAppWebBase extends WAConnection {
const json = ['query', { epoch: this.msgCount.toString(), type: 'chat' }, null] const json = ['query', { epoch: this.msgCount.toString(), type: 'chat' }, null]
return this.query(json, [WAMetric.group, WAFlag.ignore]) // this has to be an encrypted query return this.query(json, [WAMetric.group, WAFlag.ignore]) // this has to be an encrypted query
} }
/**
* Archive a given chat
* @param jid the ID of the person/group you are archiving
*/
async archiveChat(jid: string) {
const json = [
'action',
{ epoch: this.msgCount.toString(), type: 'set' },
[['chat', { type: 'archive', jid: jid }, null]],
]
return this.queryExpecting200(json, [WAMetric.group, WAFlag.acknowledge]) as Promise<{ status: number }>
}
/** /**
* Check if your phone is connected * Check if your phone is connected
* @param timeoutMs max time for the phone to respond * @param timeoutMs max time for the phone to respond

View File

@@ -40,15 +40,13 @@ export const WAMessageType = function () {
Object.keys(types).forEach(element => dict[ types[element] ] = element) Object.keys(types).forEach(element => dict[ types[element] ] = element)
return dict return dict
}() }()
export const HKDFInfoKeys = (function () { export const HKDFInfoKeys = {
[MessageType.image]: 'WhatsApp Image Keys',
const dict: Record<string, string> = {} [MessageType.audio]: 'WhatsApp Audio Keys',
dict[MessageType.image] = 'WhatsApp Image Keys' [MessageType.video]: 'WhatsApp Video Keys',
dict[MessageType.video] = 'WhatsApp Audio Keys' [MessageType.document]: 'WhatsApp Document Keys',
dict[MessageType.document] = 'WhatsApp Document Keys' [MessageType.sticker]: 'WhatsApp Image Keys'
dict[MessageType.sticker] = 'WhatsApp Image Keys' }
return dict
})()
export enum Mimetype { export enum Mimetype {
jpeg = 'image/jpeg', jpeg = 'image/jpeg',
mp4 = 'video/mp4', mp4 = 'video/mp4',
@@ -108,3 +106,4 @@ export interface WALocationMessage {
address?: string address?: string
} }
export type WAContactMessage = proto.ContactMessage export type WAContactMessage = proto.ContactMessage
export type WAMessageKey = proto.IMessageKey

View File

@@ -9,11 +9,12 @@ import {
WALocationMessage, WALocationMessage,
WAContactMessage, WAContactMessage,
WASendMessageResponse, WASendMessageResponse,
Presence, WAMessageKey,
} from './Constants' } from './Constants'
import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes } from '../WAConnection/Utils' import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes } from '../WAConnection/Utils'
import { WAMessageContent, WAMetric, WAFlag } from '../WAConnection/Constants' import { WAMessageContent, WAMetric, WAFlag, WANode, WAMessage } from '../WAConnection/Constants'
import { validateJIDForSending, generateThumbnail, getMediaKeys } from './Utils' import { validateJIDForSending, generateThumbnail, getMediaKeys } from './Utils'
import { proto } from '../../WAMessage/WAMessage'
export default class WhatsAppWebMessages extends WhatsAppWebBase { export default class WhatsAppWebMessages extends WhatsAppWebBase {
/** /**
@@ -30,17 +31,52 @@ export default class WhatsAppWebMessages extends WhatsAppWebBase {
return this.queryExpecting200(json, [WAMetric.group, WAFlag.ignore]) // encrypt and send off return this.queryExpecting200(json, [WAMetric.group, WAFlag.ignore]) // encrypt and send off
} }
/** /**
* Tell someone about your presence -- online, typing, offline etc. * Search WhatsApp messages with a given text string
* @param jid the ID of the person/group who you are updating * @param txt the search string
* @param type your presence * @param count number of results to return
* @param page page number of results
*/ */
async updatePresence(jid: string, type: Presence) { async searchMessages(txt: string, count: number, page: number) {
const json = [
'query',
{
epoch: this.msgCount.toString(),
type: 'search',
search: txt,
count: count.toString(),
page: page.toString()
},
null,
]
const response: WANode = await this.queryExpecting200(json, [WAMetric.group, WAFlag.ignore]) // encrypt and send off
const messages = response[2] ? response[2].map (row => row[2]) : []
return { last: response[1]['last'] === 'true', messages: messages as WAMessage[] }
}
/**
* Mark a given chat as unread
* @param jid
*/
async markChatUnread (jid: string) {
const json = [ const json = [
'action', 'action',
{ epoch: this.msgCount.toString(), type: 'set' }, { epoch: this.msgCount.toString(), type: 'set' },
[['presence', { type: type, to: jid }, null]], [['read', {jid: jid, type: 'false', count: '1'}, null]]
] ]
return this.queryExpecting200(json, [WAMetric.group, WAFlag.acknowledge]) as Promise<{ status: number }> return this.queryExpecting200(json, [WAMetric.group, WAFlag.ignore]) as Promise<{ status: number }>
}
/**
* Delete a message in a chat
* @param id the person or group where you're trying to delete the message
* @param messageKey key of the message you want to delete
*/
async deleteMessage (id: string, messageKey: WAMessageKey) {
const json: WAMessageContent = {
protocolMessage: {
key: messageKey,
type: proto.ProtocolMessage.PROTOCOL_MESSAGE_TYPE.REVOKE
}
}
return this.sendGenericMessage (id, json, {})
} }
async sendMessage( async sendMessage(
id: string, id: string,
@@ -168,7 +204,7 @@ export default class WhatsAppWebMessages extends WhatsAppWebBase {
messageTimestamp: timestamp, messageTimestamp: timestamp,
participant: id.includes('@g.us') ? this.userMetaData.id : null, participant: id.includes('@g.us') ? this.userMetaData.id : null,
} }
const json = ['action', { epoch: this.msgCount.toString(), type: 'relay' }, [['message', null, messageJSON]]] const json = ['action', {epoch: this.msgCount.toString(), type: 'relay'}, [['message', null, messageJSON]]]
const response = await this.queryExpecting200(json, [WAMetric.message, WAFlag.ignore], null, messageJSON.key.id) const response = await this.queryExpecting200(json, [WAMetric.message, WAFlag.ignore], null, messageJSON.key.id)
return { status: response.status as number, messageID: messageJSON.key.id } as WASendMessageResponse return { status: response.status as number, messageID: messageJSON.key.id } as WASendMessageResponse
} }

View File

@@ -60,7 +60,14 @@ WAClientTest('Messages', (client) => {
const file = await decodeMediaMessage(message.message, './Media/received_img') const file = await decodeMediaMessage(message.message, './Media/received_img')
assert.strictEqual(message.message.imageMessage.contextInfo.stanzaId, messages[0].key.id) assert.strictEqual(message.message.imageMessage.contextInfo.stanzaId, messages[0].key.id)
}) })
it('should send a text message & delete it', async () => {
const message = await sendAndRetreiveMessage(client, 'hello fren', MessageType.text)
assert.strictEqual(message.message.conversation, 'hello fren')
await createTimeout (2000)
await client.deleteMessage (testJid, message.key)
})
}) })
describe('Validate WhatsApp IDs', () => { describe('Validate WhatsApp IDs', () => {
it ('should correctly validate', () => { it ('should correctly validate', () => {
assert.doesNotThrow (() => validateJIDForSending ('12345@s.whatsapp.net')) assert.doesNotThrow (() => validateJIDForSending ('12345@s.whatsapp.net'))
@@ -102,12 +109,20 @@ WAClientTest('Misc', (client) => {
assert.ok(response) assert.ok(response)
assert.rejects(client.getProfilePicture('abcd@s.whatsapp.net')) assert.rejects(client.getProfilePicture('abcd@s.whatsapp.net'))
}) })
it('should mark a chat unread', async () => {
const response = await client.markChatUnread(testJid)
assert.ok(response)
})
it('should return search results', async () => {
const response = await client.searchMessages('Adh', 25, 0)
assert.ok (response.messages)
assert.ok (response.messages.length >= 0)
})
}) })
WAClientTest('Groups', (client) => { WAClientTest('Groups', (client) => {
let gid: string let gid: string
it('should create a group', async () => { it('should create a group', async () => {
const response = await client.groupCreate('Cool Test Group', [testJid]) const response = await client.groupCreate('Cool Test Group', [testJid])
assert.strictEqual(response.status, 200)
gid = response.gid gid = response.gid
console.log('created group: ' + gid) console.log('created group: ' + gid)
}) })
@@ -122,13 +137,11 @@ WAClientTest('Groups', (client) => {
assert.strictEqual(metadata.participants.filter((obj) => obj.id.split('@')[0] === testJid.split('@')[0]).length, 1) assert.strictEqual(metadata.participants.filter((obj) => obj.id.split('@')[0] === testJid.split('@')[0]).length, 1)
}) })
it('should send a message on the group', async () => { it('should send a message on the group', async () => {
const r = await client.sendMessage(gid, 'hello', MessageType.text) await client.sendMessage(gid, 'hello', MessageType.text)
assert.strictEqual(r.status, 200)
}) })
it('should update the subject', async () => { it('should update the subject', async () => {
const subject = 'V Cool Title' const subject = 'V Cool Title'
const r = await client.groupUpdateSubject(gid, subject) await client.groupUpdateSubject(gid, subject)
assert.strictEqual(r.status, 200)
const metadata = await client.groupMetadata(gid) const metadata = await client.groupMetadata(gid)
assert.strictEqual(metadata.subject, subject) assert.strictEqual(metadata.subject, subject)
@@ -137,8 +150,10 @@ WAClientTest('Groups', (client) => {
await client.groupRemove(gid, [testJid]) await client.groupRemove(gid, [testJid])
}) })
it('should leave the group', async () => { it('should leave the group', async () => {
const response = await client.groupLeave(gid) await client.groupLeave(gid)
assert.strictEqual(response.status, 200) })
it('should archive the group', async () => {
await client.archiveChat(gid)
}) })
}) })
WAClientTest('Events', (client) => { WAClientTest('Events', (client) => {

View File

@@ -20,9 +20,9 @@ export function validateJIDForSending (jid: string) {
} }
/** Type of notification */ /** Type of notification */
export function getNotificationType(message: WAMessage): [string, string] { export function getNotificationType(message: WAMessage): [string, MessageType?] {
if (message.message) { if (message.message) {
return ['message', Object.keys(message.message)[0]] return ['message', Object.keys(message.message)[0] as MessageType]
} else if (message.messageStubType) { } else if (message.messageStubType) {
return [WAMessageType[message.messageStubType], null] return [WAMessageType[message.messageStubType], null]
} else { } else {
@@ -87,8 +87,9 @@ export async function generateThumbnail(buffer: Buffer, mediaType: MessageType,
* Decode a media message (video, image, document, audio) & save it to the given file * Decode a media message (video, image, document, audio) & save it to the given file
* @param message the media message you want to decode * @param message the media message you want to decode
* @param filename the name of the file where the media will be saved * @param filename the name of the file where the media will be saved
* @param attachExtension should the correct extension be applied automatically to the file
*/ */
export async function decodeMediaMessage(message: WAMessageContent, filename: string) { export async function decodeMediaMessage(message: WAMessageContent, filename: string, attachExtension: boolean=true) {
const getExtension = (mimetype) => mimetype.split(';')[0].split('/')[1] const getExtension = (mimetype) => mimetype.split(';')[0].split('/')[1]
/* /*
One can infer media type from the key in the message One can infer media type from the key in the message
@@ -132,7 +133,7 @@ export async function decodeMediaMessage(message: WAMessageContent, filename: st
if (sign.equals(mac)) { if (sign.equals(mac)) {
const decrypted = aesDecryptWithIV(file, cipherKey, iv) // decrypt media const decrypted = aesDecryptWithIV(file, cipherKey, iv) // decrypt media
const trueFileName = filename + '.' + getExtension(messageContent.mimetype) const trueFileName = attachExtension ? (filename + '.' + getExtension(messageContent.mimetype)) : filename
fs.writeFileSync(trueFileName, decrypted) fs.writeFileSync(trueFileName, decrypted)
return trueFileName return trueFileName