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
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
``` 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
- To check if a given ID is on WhatsApp
``` ts
@@ -238,12 +257,15 @@ If you want to save & process some images, videos, documents or stickers you rec
``` ts
// the presence update is fetched and called here
client.setOnPresenceUpdate (json => console.log(json.id + " presence is " + json.type))
await client.requestPresenceUpdate ("xyz@c.us")
await client.requestPresenceUpdate ("xyz@c.us") // request the update
```
- 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.
Also, append ``` @s.whatsapp.net ``` for individuals & ``` @g.us ``` for groups.
Append ``` @s.whatsapp.net ``` for individuals & ``` @g.us ``` for groups.
## Groups
- 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
``` 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"])
console.log("added successfully: " + (response.status===200))
```
- To make someone admin on a group
``` ts
const response = await client.groupMakeAdmin ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"]) // id & people to make admin
console.log("made admin successfully: " + (response.status===200))
// id & people to make admin (will throw error if it fails)
await client.groupMakeAdmin ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"])
```
- To leave a group
``` ts
const response = await client.groupLeave ("abcd-xyz@g.us")
console.log("left group successfully: " + (response.status===200))
await client.groupLeave ("abcd-xyz@g.us") // (will throw error if it fails)
```
- To get the invite code for a group
``` ts

View File

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

View File

@@ -1,5 +1,5 @@
import WAConnection from '../WAConnection/WAConnection'
import { MessageStatus, MessageStatusUpdate, PresenceUpdate } from './Constants'
import { MessageStatus, MessageStatusUpdate, PresenceUpdate, Presence } from './Constants'
import {
WAMessage,
WANode,
@@ -63,11 +63,25 @@ export default class WhatsAppWebBase extends WAConnection {
}
/** Query whether a given number is registered on WhatsApp */
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 */
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) */
getStatus = (jid: string | null) =>
this.query(['query', 'Status', jid || this.userMetaData.id]) as Promise<{ status: string }>
async getStatus (jid?: 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 */
async getProfilePicture(jid: string | null) {
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]
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
* @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)
return dict
}()
export const HKDFInfoKeys = (function () {
const dict: Record<string, string> = {}
dict[MessageType.image] = 'WhatsApp Image Keys'
dict[MessageType.video] = 'WhatsApp Audio Keys'
dict[MessageType.document] = 'WhatsApp Document Keys'
dict[MessageType.sticker] = 'WhatsApp Image Keys'
return dict
})()
export const HKDFInfoKeys = {
[MessageType.image]: 'WhatsApp Image Keys',
[MessageType.audio]: 'WhatsApp Audio Keys',
[MessageType.video]: 'WhatsApp Video Keys',
[MessageType.document]: 'WhatsApp Document Keys',
[MessageType.sticker]: 'WhatsApp Image Keys'
}
export enum Mimetype {
jpeg = 'image/jpeg',
mp4 = 'video/mp4',
@@ -108,3 +106,4 @@ export interface WALocationMessage {
address?: string
}
export type WAContactMessage = proto.ContactMessage
export type WAMessageKey = proto.IMessageKey

View File

@@ -9,11 +9,12 @@ import {
WALocationMessage,
WAContactMessage,
WASendMessageResponse,
Presence,
WAMessageKey,
} from './Constants'
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 { proto } from '../../WAMessage/WAMessage'
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
}
/**
* 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
* Search WhatsApp messages with a given text string
* @param txt the search string
* @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 = [
'action',
{ 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(
id: string,
@@ -168,7 +204,7 @@ export default class WhatsAppWebMessages extends WhatsAppWebBase {
messageTimestamp: timestamp,
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)
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')
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', () => {
it ('should correctly validate', () => {
assert.doesNotThrow (() => validateJIDForSending ('12345@s.whatsapp.net'))
@@ -102,12 +109,20 @@ WAClientTest('Misc', (client) => {
assert.ok(response)
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) => {
let gid: string
it('should create a group', async () => {
const response = await client.groupCreate('Cool Test Group', [testJid])
assert.strictEqual(response.status, 200)
gid = response.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)
})
it('should send a message on the group', async () => {
const r = await client.sendMessage(gid, 'hello', MessageType.text)
assert.strictEqual(r.status, 200)
await client.sendMessage(gid, 'hello', MessageType.text)
})
it('should update the subject', async () => {
const subject = 'V Cool Title'
const r = await client.groupUpdateSubject(gid, subject)
assert.strictEqual(r.status, 200)
await client.groupUpdateSubject(gid, subject)
const metadata = await client.groupMetadata(gid)
assert.strictEqual(metadata.subject, subject)
@@ -137,8 +150,10 @@ WAClientTest('Groups', (client) => {
await client.groupRemove(gid, [testJid])
})
it('should leave the group', async () => {
const response = await client.groupLeave(gid)
assert.strictEqual(response.status, 200)
await client.groupLeave(gid)
})
it('should archive the group', async () => {
await client.archiveChat(gid)
})
})
WAClientTest('Events', (client) => {

View File

@@ -20,9 +20,9 @@ export function validateJIDForSending (jid: string) {
}
/** Type of notification */
export function getNotificationType(message: WAMessage): [string, string] {
export function getNotificationType(message: WAMessage): [string, MessageType?] {
if (message.message) {
return ['message', Object.keys(message.message)[0]]
return ['message', Object.keys(message.message)[0] as MessageType]
} else if (message.messageStubType) {
return [WAMessageType[message.messageStubType], null]
} 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
* @param message the media message you want to decode
* @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]
/*
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)) {
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)
return trueFileName