From b9df538764911172f3a0f9157927f0bfa93c6db1 Mon Sep 17 00:00:00 2001 From: Adhiraj Singh Date: Wed, 8 Jul 2020 12:38:51 +0530 Subject: [PATCH] V2.1 Added features: -search messages -delete messages -archive chats -mark chats as unread Fixes -decoding audio/video messages --- README.md | 42 ++++++++++++++++++++++-------- package.json | 2 +- src/WAClient/Base.ts | 34 +++++++++++++++++++++--- src/WAClient/Constants.ts | 17 ++++++------ src/WAClient/Messages.ts | 54 ++++++++++++++++++++++++++++++++------- src/WAClient/Tests.ts | 29 ++++++++++++++++----- src/WAClient/Utils.ts | 9 ++++--- 7 files changed, 142 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 4bd3bc9..56ac732 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index 2c5c556..03eb390 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/WAClient/Base.ts b/src/WAClient/Base.ts index 7880ba5..519953a 100644 --- a/src/WAClient/Base.ts +++ b/src/WAClient/Base.ts @@ -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 diff --git a/src/WAClient/Constants.ts b/src/WAClient/Constants.ts index b7c4860..82423b3 100644 --- a/src/WAClient/Constants.ts +++ b/src/WAClient/Constants.ts @@ -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 = {} - 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 diff --git a/src/WAClient/Messages.ts b/src/WAClient/Messages.ts index 0d4e7f8..f4cadbc 100644 --- a/src/WAClient/Messages.ts +++ b/src/WAClient/Messages.ts @@ -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 } diff --git a/src/WAClient/Tests.ts b/src/WAClient/Tests.ts index affe315..2f7df87 100644 --- a/src/WAClient/Tests.ts +++ b/src/WAClient/Tests.ts @@ -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) => { diff --git a/src/WAClient/Utils.ts b/src/WAClient/Utils.ts index 48af9cb..6c348cf 100644 --- a/src/WAClient/Utils.ts +++ b/src/WAClient/Utils.ts @@ -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