diff --git a/Media/cat.jpeg b/Media/cat.jpeg new file mode 100644 index 0000000..5063d09 Binary files /dev/null and b/Media/cat.jpeg differ diff --git a/README.md b/README.md index 9b2198a..d08c608 100644 --- a/README.md +++ b/README.md @@ -161,13 +161,6 @@ Implement the following callbacks in your code: ``` ts client.setOnUnexpectedDisconnect (reason => console.log ("disconnected unexpectedly: " + reason) ) ``` -- Called when you log into WhatsApp Web somewhere else - ``` ts - client.setOnTakenOver (async () => { - // reconnect to gain connection back here - await client.connect () - }) - ``` ## Sending Messages Send like, all types of messages with a single function: @@ -296,7 +289,8 @@ await client.deleteChat (jid) // will delete the chat (can be a group or broadca **Note:** to unmute or unpin a chat, one must pass the timestamp of the pinning or muting. This is returned by the pin & mute functions. This is also available in the `WAChat` objects of the respective chats, as a `mute` or `pin` property. -## Querying +## Misc + - To check if a given ID is on WhatsApp ``` ts const id = 'xyz@s.whatsapp.net' @@ -324,6 +318,12 @@ await client.deleteChat (jid) // will delete the chat (can be a group or broadca const ppUrl = await client.getProfilePicture ("xyz@g.us") // leave empty to get your own console.log("download profile picture from: " + ppUrl) ``` +- To change your display picture or a group's + ``` ts + const jid = '111234567890-1594482450@g.us' // can be your own too + const img = fs.readFileSync ('new-profile-picture.jpeg') // can be PNG also + await client.updateProfilePicture (jid, newPP) + ``` - To get someone's presence (if they're typing, online) ``` ts // the presence update is fetched and called here @@ -353,10 +353,21 @@ Append ``` @s.whatsapp.net ``` for individuals & ``` @g.us ``` for groups. // 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"]) ``` -- To make someone admin on a group +- To make/demote admins on a group ``` ts // 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"]) + await client.groupMakeAdmin ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"]) + await client.groupDemoteAdmin ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"]) // demote admins + ``` +- To change group settings + ``` ts + import { GroupSettingChange } from '@adiwajshing/baileys' + // only allow admins to send messages + await client.groupSettingChange ("abcd-xyz@g.us", GroupSettingChange.messageSend, true) + // allow everyone to modify the group's settings -- like display picture etc. + await client.groupSettingChange ("abcd-xyz@g.us", GroupSettingChange.settingChange, false) + // only allow admins to modify the group's settings + await client.groupSettingChange ("abcd-xyz@g.us", GroupSettingChange.settingChange, true) ``` - To leave a group ``` ts diff --git a/package.json b/package.json index 3135095..cab0d39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adiwajshing/baileys", - "version": "2.1.0", + "version": "2.2.0", "description": "WhatsApp Web API", "homepage": "https://github.com/adiwajshing/Baileys", "main": "lib/WAClient/WAClient.js", diff --git a/src/Binary/Encoder.ts b/src/Binary/Encoder.ts index a70882f..d4122ce 100644 --- a/src/Binary/Encoder.ts +++ b/src/Binary/Encoder.ts @@ -2,12 +2,12 @@ import { WA } from './Constants' import { proto } from '../../WAMessage/WAMessage' export default class Encoder { - data: Array = [] + data: number[] = [] pushByte(value: number) { this.data.push(value & 0xff) } - pushInt(value: number, n: number, littleEndian = false) { + pushInt(value: number, n: number, littleEndian=false) { for (let i = 0; i < n; i++) { const curShift = littleEndian ? i : n - 1 - i this.data.push((value >> (curShift * 8)) & 0xff) @@ -16,17 +16,17 @@ export default class Encoder { pushInt20(value: number) { this.pushBytes([(value >> 16) & 0x0f, (value >> 8) & 0xff, value & 0xff]) } - pushBytes(bytes: Uint8Array | Array) { - this.data.push.apply(this.data, bytes) + pushBytes(bytes: Uint8Array | Buffer | number[]) { + bytes.forEach (b => this.data.push(b)) + //this.data.push.apply(this.data, bytes) } pushString(str: string) { const bytes = Buffer.from (str, 'utf-8') this.pushBytes(bytes) } writeByteLength(length: number) { - if (length >= 4294967296) { - throw new Error('string too large to encode: ' + length) - } + if (length >= 4294967296) throw new Error('string too large to encode: ' + length) + if (length >= 1 << 20) { this.pushByte(WA.Tags.BINARY_32) this.pushInt(length, 4) // 32 bit integer @@ -101,19 +101,18 @@ export default class Encoder { this.pushBytes([WA.Tags.LIST_16, listSize]) } } - writeChildren(children: string | Array | Object) { - if (!children) { - return - } + writeChildren(children: string | Array | Buffer | Object) { + if (!children) return if (typeof children === 'string') { this.writeString(children, true) + } else if (Buffer.isBuffer(children)) { + this.writeByteLength (children.length) + this.pushBytes(children) } else if (Array.isArray(children)) { this.writeListStart(children.length) - children.forEach((c) => { - if (c) this.writeNode(c) - }) - } else if (typeof children === 'object') { + children.forEach(c => c && this.writeNode(c)) + } else if (typeof children === 'object') { const buffer = WA.Message.encode(children as proto.WebMessageInfo).finish() this.writeByteLength(buffer.length) this.pushBytes(buffer) diff --git a/src/Binary/Tests.ts b/src/Binary/Tests.ts index 703089b..18669c8 100644 --- a/src/Binary/Tests.ts +++ b/src/Binary/Tests.ts @@ -3,7 +3,7 @@ import Encoder from './Encoder' import Decoder from './Decoder' describe('Binary Coding Tests', () => { - const testVectors: [[string, Object]] = [ + const testVectors: [string, Object][] = [ [ 'f806092f5a0a10f804f80234fc6c0a350a1b39313735323938373131313740732e77686174736170702e6e657410011a143345423030393637354537454433374141424632122b0a292a7069616e6f20726f6f6d2074696d696e6773206172653a2a0a20363a3030414d2d31323a3030414d18b3faa7f3052003f80234fc4c0a410a1b39313735323938373131313740732e77686174736170702e6e657410001a20304643454335333330463634393239433645394132434646443242433845414418bdfaa7f305c00101f80234fc930a350a1b39313735323938373131313740732e77686174736170702e6e657410011a14334542303033433742353339414644303937353312520a50536f727279206672656e2c204920636f756c646e277420756e6465727374616e6420274c69627261272e2054797065202768656c702720746f206b6e6f77207768617420616c6c20492063616e20646f18c1faa7f3052003f80234fc540a410a1b39313735323938373131313740732e77686174736170702e6e657410001a20413132333042384436423041314437393345433241453245413043313638443812090a076c69627261727918c2faa7f305', [ @@ -62,12 +62,20 @@ describe('Binary Coding Tests', () => { ], ], ], + [ + 'f8063f2dfafc0831323334353637385027fc0431323334f801f80228fc0701020304050607', + [ + 'picture', + {jid: '12345678@c.us', id: '1234'}, + [['image', null, Buffer.from([1,2,3,4,5,6,7])]] + ] + ] ] const encoder = new Encoder() const decoder = new Decoder() it('should decode strings', () => { - testVectors.forEach((pair) => { + testVectors.forEach(pair => { const buff = Buffer.from(pair[0], 'hex') const decoded = decoder.read(buff) diff --git a/src/WAClient/Base.ts b/src/WAClient/Base.ts index 7bbfeb8..ab47e47 100644 --- a/src/WAClient/Base.ts +++ b/src/WAClient/Base.ts @@ -1,5 +1,5 @@ import WAConnection from '../WAConnection/WAConnection' -import { MessageStatusUpdate, PresenceUpdate, Presence, WABroadcastListInfo } from './Constants' +import { MessageStatusUpdate, PresenceUpdate, Presence, WABroadcastListInfo, WAProfilePictureChange } from './Constants' import { WAMessage, WANode, @@ -8,6 +8,9 @@ import { MessageLogLevel, WATag, } from '../WAConnection/Constants' +import { generateProfilePicture } from '../WAClient/Utils' +import { generateMessageTag } from '../WAConnection/Utils' + export default class WhatsAppWebBase extends WAConnection { /** Set the callback for message status updates (when a message is delivered, read etc.) */ @@ -185,9 +188,22 @@ export default class WhatsAppWebBase extends WAConnection { } return loadMessage() as Promise } + async updateProfilePicture (jid: string, img: Buffer) { + const data = await generateProfilePicture (img) + const tag = generateMessageTag (this.msgCount) + const query: WANode = [ + 'picture', + { jid: jid, id: tag, type: 'set' }, + [ + ['image', null, data.img], + ['preview', null, data.preview] + ] + ] + return this.setQuery ([query], [14, 136], tag) as Promise + } /** Generic function for action, set queries */ - async setQuery (nodes: WANode[], binaryTags: WATag = [WAMetric.group, WAFlag.ignore]) { + async setQuery (nodes: WANode[], binaryTags: WATag = [WAMetric.group, WAFlag.ignore], tag?: string) { const json = ['action', {epoch: this.msgCount.toString(), type: 'set'}, nodes] - return this.queryExpecting200(json, binaryTags) as Promise<{status: number}> + return this.queryExpecting200(json, binaryTags, null, tag) as Promise<{status: number}> } } diff --git a/src/WAClient/Constants.ts b/src/WAClient/Constants.ts index 87860ea..97b1d3d 100644 --- a/src/WAClient/Constants.ts +++ b/src/WAClient/Constants.ts @@ -81,6 +81,11 @@ export interface WAUrlInfo { description: string jpegThumbnail?: Buffer } +export interface WAProfilePictureChange { + status: number + tag: string + eurl: string +} export interface MessageInfo { reads: {jid: string, t: string}[] deliveries: {jid: string, t: string}[] @@ -94,7 +99,11 @@ export interface MessageStatusUpdate { /** Message IDs read/delivered */ ids: string[] /** Status of the Message IDs */ - type: proto.WebMessageInfo.WEB_MESSAGE_INFO_STUBTYPE + type: proto.WebMessageInfo.WEB_MESSAGE_INFO_STATUS +} +export enum GroupSettingChange { + messageSend = 'announcement', + settingsChange = 'locked', } export interface PresenceUpdate { id: string diff --git a/src/WAClient/Groups.ts b/src/WAClient/Groups.ts index 8e15421..5e274cc 100644 --- a/src/WAClient/Groups.ts +++ b/src/WAClient/Groups.ts @@ -1,23 +1,24 @@ import WhatsAppWebBase from './Base' import { WAMessage, WAMetric, WAFlag, WANode, WAGroupMetadata, WAGroupCreateResponse, WAGroupModification } from '../WAConnection/Constants' +import { GroupSettingChange } from './Constants' import { generateMessageTag } from '../WAConnection/Utils' export default class WhatsAppWebGroups extends WhatsAppWebBase { /** Generic function for group queries */ - async groupQuery(type: string, jid?: string, subject?: string, participants?: string[]) { + async groupQuery(type: string, jid?: string, subject?: string, participants?: string[], additionalNodes?: WANode[]) { + const tag = generateMessageTag(this.msgCount) const json: WANode = [ 'group', { author: this.userMetaData.id, - id: generateMessageTag(), + id: tag, type: type, jid: jid, subject: subject, }, - participants ? participants.map((str) => ['participant', { jid: str }, null]) : [], + participants ? participants.map(str => ['participant', { jid: str }, null]) : additionalNodes, ] - const q = ['action', { type: 'set', epoch: this.msgCount.toString() }, [json]] - return this.queryExpecting200(q, [WAMetric.group, WAFlag.ignore]) + return this.setQuery ([json], [WAMetric.group, WAFlag.ignore], tag) } /** Get the metadata of the group */ groupMetadata = (jid: string) => this.queryExpecting200(['query', 'GroupMetadata', jid]) as Promise @@ -79,6 +80,22 @@ export default class WhatsAppWebGroups extends WhatsAppWebBase { */ groupMakeAdmin = (jid: string, participants: string[]) => this.groupQuery('promote', jid, null, participants) as Promise + /** + * Make demote an admin on the group + * @param jid the ID of the group + * @param participants the people to make admin + */ + groupDemoteAdmin = (jid: string, participants: string[]) => + this.groupQuery('demote', jid, null, participants) as Promise + /** + * Make demote an admin on the group + * @param jid the ID of the group + * @param participants the people to make admin + */ + groupSettingChange = (jid: string, setting: GroupSettingChange, onlyAdmins: boolean) => { + const node: WANode = [ setting, {value: onlyAdmins ? 'true' : 'false'}, null ] + return this.groupQuery('prop', jid, null, null, [node]) as Promise<{status: number}> + } /** Get the invite link of the given group */ async groupInviteCode(jid: string) { const json = ['query', 'inviteCode', jid] diff --git a/src/WAClient/Messages.ts b/src/WAClient/Messages.ts index 55e12ab..03ac316 100644 --- a/src/WAClient/Messages.ts +++ b/src/WAClient/Messages.ts @@ -326,7 +326,8 @@ export default class WhatsAppWebMessages extends WhatsAppWebGroups { status: WAMessageProto.proto.WebMessageInfo.WEB_MESSAGE_INFO_STATUS.PENDING } 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 flag = id === this.userMetaData.id ? WAFlag.acknowledge : WAFlag.ignore // acknowledge when sending message to oneself + const response = await this.queryExpecting200(json, [WAMetric.message, flag], null, messageJSON.key.id) return { status: response.status as number, messageID: messageJSON.key.id, diff --git a/src/WAClient/Tests.ts b/src/WAClient/Tests.ts index b698258..458cf4a 100644 --- a/src/WAClient/Tests.ts +++ b/src/WAClient/Tests.ts @@ -1,7 +1,8 @@ import { WAClient } from './WAClient' -import { MessageType, MessageOptions, Mimetype, Presence, ChatModification } from './Constants' +import { MessageType, MessageOptions, Mimetype, Presence, ChatModification, GroupSettingChange } from './Constants' import * as fs from 'fs' import * as assert from 'assert' +import fetch from 'node-fetch' import { decodeMediaMessage, validateJIDForSending } from './Utils' import { promiseTimeout, createTimeout } from '../WAConnection/Utils' @@ -117,6 +118,20 @@ WAClientTest('Misc', (client) => { it('should return the stories', async () => { await client.getStories() }) + it('should change the profile picture', async () => { + await createTimeout (5000) + + const ppUrl = await client.getProfilePicture(client.userMetadata.id) + const fetched = await fetch(ppUrl, { headers: { Origin: 'https://web.whatsapp.com' } }) + const buff = await fetched.buffer () + + const newPP = fs.readFileSync ('./Media/cat.jpeg') + const response = await client.updateProfilePicture (client.userMetadata.id, newPP) + + await createTimeout (10000) + + await client.updateProfilePicture (client.userMetaData.id, buff) // revert back + }) it('should return the profile picture', async () => { const response = await client.getProfilePicture(testJid) assert.ok(response) @@ -177,6 +192,11 @@ WAClientTest('Groups', (client) => { const metadata = await client.groupMetadata(gid) assert.strictEqual(metadata.subject, subject) }) + it('should update the group settings', async () => { + await client.groupSettingChange (gid, GroupSettingChange.messageSend, true) + await createTimeout (5000) + await client.groupSettingChange (gid, GroupSettingChange.settingsChange, true) + }) it('should remove someone from a group', async () => { await client.groupRemove(gid, [testJid]) }) @@ -204,18 +224,4 @@ WAClientTest('Events', (client) => { const response = await client.sendMessage(testJid, 'My Name Jeff', MessageType.text) await promiseTimeout(10000, waitForUpdate()) }) - /*it ('should update me on presence', async () => { - //client.logUnhandledMessages = true - client.setOnPresenceUpdate (presence => { - console.log (presence) - }) - const response = await client.requestPresenceUpdate (client.userMetaData) - assert.strictEqual (response.status, 200) - await createTimeout (25000) - })*/ }) -/*WAClientTest ('Testz', client => { - it ('should work', async () => { - - }) -})*/ \ No newline at end of file diff --git a/src/WAClient/Utils.ts b/src/WAClient/Utils.ts index a707a65..c67176b 100644 --- a/src/WAClient/Utils.ts +++ b/src/WAClient/Utils.ts @@ -67,6 +67,15 @@ export const compressImage = async (buffer: Buffer) => { const jimp = await Jimp.read (buffer) return jimp.resize(48, 48).getBufferAsync (Jimp.MIME_JPEG) } +export const generateProfilePicture = async (buffer: Buffer) => { + const jimp = await Jimp.read (buffer) + const min = Math.min(jimp.getWidth (), jimp.getHeight ()) + const cropped = jimp.crop (0, 0, min, min) + return { + img: await cropped.resize(640, 640).getBufferAsync (Jimp.MIME_JPEG), + preview: await cropped.resize(96, 96).getBufferAsync (Jimp.MIME_JPEG) + } +} /** generates a thumbnail for a given media, if required */ export async function generateThumbnail(buffer: Buffer, mediaType: MessageType, info: MessageOptions) { if (info.thumbnail === null || info.thumbnail) { diff --git a/src/WAConnection/Utils.ts b/src/WAConnection/Utils.ts index bff4163..bf81c52 100644 --- a/src/WAConnection/Utils.ts +++ b/src/WAConnection/Utils.ts @@ -66,7 +66,7 @@ export function promiseTimeout(ms: number, promise: Promise) { // whatsapp requires a message tag for every message, we just use the timestamp as one export function generateMessageTag(epoch?: number) { let tag = new Date().getTime().toString() - if (epoch) tag += '-' + epoch // attach epoch if provided + if (epoch) tag += '.--' + epoch // attach epoch if provided return tag } // generate a random 16 byte client ID