diff --git a/README.md b/README.md index 27a223d..f4f9afd 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ on (event: 'open', listener: (result: WAOpenResult) => void): this on (event: 'connecting', listener: () => void): this /** when the connection has closed */ on (event: 'close', listener: (err: {reason?: DisconnectReason | string, isReconnecting: boolean}) => void): this -/** when the socket has closed */ +/** when the socket is closed */ on (event: 'ws-close', listener: (err: {reason?: DisconnectReason | string}) => void): this /** when WA updates the credentials */ on (event: 'credentials-updated', listener: (auth: AuthenticationCredentials) => void): this @@ -207,17 +207,11 @@ 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 */ -on (event: 'group-participants-remove', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this -/** when participants are promoted in a group */ -on (event: 'group-participants-promote', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this -/** when participants are demoted in a group */ -on (event: 'group-participants-demote', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this -/** when the group settings is updated */ -on (event: 'group-settings-update', listener: (update: {jid: string, restrict?: string, announce?: string, actor?: string}) => void): this -/** when the group description is updated */ -on (event: 'group-description-update', listener: (update: {jid: string, description?: string, actor?: string}) => void): this +on (event: 'group-participants-update', listener: (update: {jid: string, participants: string[], actor?: string, action: WAParticipantAction}) => void): this +/** when the group is updated */ +on (event: 'group-update', listener: (update: Partial & {jid: string, actor?: string}) => void): this +/** when WA sends back a pong */ +on (event: 'received-pong', listener: () => void): this ``` ## Sending Messages diff --git a/package.json b/package.json index d7cceb7..fab456d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adiwajshing/baileys", - "version": "3.2.4", + "version": "3.3.0", "description": "WhatsApp Web API", "homepage": "https://github.com/adiwajshing/Baileys", "main": "lib/WAConnection/WAConnection.js", diff --git a/src/Tests/Tests.Groups.ts b/src/Tests/Tests.Groups.ts index 582e7ce..c92bbb1 100644 --- a/src/Tests/Tests.Groups.ts +++ b/src/Tests/Tests.Groups.ts @@ -1,4 +1,4 @@ -import { MessageType, GroupSettingChange, delay, ChatModification } from '../WAConnection/WAConnection' +import { MessageType, GroupSettingChange, delay, ChatModification, whatsappID } from '../WAConnection/WAConnection' import * as assert from 'assert' import { WAConnectionTest, testJid } from './Common' @@ -29,7 +29,7 @@ WAConnectionTest('Groups', (conn) => { const newDesc = 'Wow this was set from Baileys' const waitForEvent = new Promise (resolve => { - conn.on ('group-description-update', ({jid, actor}) => { + conn.once ('group-update', ({jid, actor}) => { if (jid === gid) { assert.ok (actor, conn.user.jid) resolve () @@ -39,8 +39,6 @@ WAConnectionTest('Groups', (conn) => { await conn.groupUpdateDescription (gid, newDesc) await waitForEvent - conn.removeAllListeners ('group-description-update') - const metadata = await conn.groupMetadata(gid) assert.strictEqual(metadata.desc, newDesc) }) @@ -61,7 +59,7 @@ WAConnectionTest('Groups', (conn) => { it('should update the subject', async () => { const subject = 'Baileyz ' + Math.floor(Math.random()*5) const waitForEvent = new Promise (resolve => { - conn.on ('chat-update', ({jid, name}) => { + conn.once ('chat-update', ({jid, name}) => { if (jid === gid) { assert.equal (name, subject) resolve () @@ -70,14 +68,14 @@ WAConnectionTest('Groups', (conn) => { }) await conn.groupUpdateSubject(gid, subject) await waitForEvent - conn.removeAllListeners ('chat-update') const metadata = await conn.groupMetadata(gid) assert.strictEqual(metadata.subject, subject) }) + it('should update the group settings', async () => { const waitForEvent = new Promise (resolve => { - conn.on ('group-settings-update', ({jid, announce}) => { + conn.once ('group-update', ({jid, announce}) => { if (jid === gid) { assert.equal (announce, 'true') resolve () @@ -92,25 +90,42 @@ WAConnectionTest('Groups', (conn) => { await delay (2000) await conn.groupSettingChange (gid, GroupSettingChange.settingsChange, true) }) - it('should remove someone from a group', async () => { + + it('should promote someone', async () => { const waitForEvent = new Promise (resolve => { - conn.on ('group-participants-remove', ({jid, participants}) => { + conn.once ('group-participants-update', ({ jid, action }) => { if (jid === gid) { - assert.equal (participants[0], testJid) + assert.strictEqual (action, 'promote') resolve () } + }) }) + await conn.groupMakeAdmin(gid, [ testJid ]) + await waitForEvent + }) + + it('should remove someone from a group', async () => { const metadata = await conn.groupMetadata (gid) - if (metadata.participants.find(({id}) => id === testJid)) { + if (metadata.participants.find(({id}) => whatsappID(id) === testJid)) { + const waitForEvent = new Promise (resolve => { + conn.once ('group-participants-update', ({jid, participants, action}) => { + if (jid === gid) { + assert.strictEqual (participants[0], testJid) + assert.strictEqual (action, 'remove') + resolve () + } + }) + }) + await conn.groupRemove(gid, [testJid]) await waitForEvent - } - conn.removeAllListeners ('group-participants-remove') + } else console.log(`could not find testJid`) }) + it('should leave the group', async () => { const waitForEvent = new Promise (resolve => { - conn.on ('chat-update', ({jid, read_only}) => { + conn.once ('chat-update', ({jid, read_only}) => { if (jid === gid) { assert.equal (read_only, 'true') resolve () @@ -119,13 +134,12 @@ WAConnectionTest('Groups', (conn) => { }) await conn.groupLeave(gid) await waitForEvent - conn.removeAllListeners ('chat-update') await conn.groupMetadataMinimal (gid) }) it('should archive the group', async () => { const waitForEvent = new Promise (resolve => { - conn.on ('chat-update', ({jid, archive}) => { + conn.once ('chat-update', ({jid, archive}) => { if (jid === gid) { assert.equal (archive, 'true') resolve () @@ -134,11 +148,10 @@ WAConnectionTest('Groups', (conn) => { }) await conn.modifyChat(gid, ChatModification.archive) await waitForEvent - conn.removeAllListeners ('chat-update') }) it('should delete the group', async () => { const waitForEvent = new Promise (resolve => { - conn.on ('chat-update', (chat) => { + conn.once ('chat-update', (chat) => { if (chat.jid === gid) { assert.equal (chat['delete'], 'true') resolve () @@ -147,6 +160,5 @@ WAConnectionTest('Groups', (conn) => { }) await conn.deleteChat(gid) await waitForEvent - conn.removeAllListeners ('chat-update') }) }) \ No newline at end of file diff --git a/src/WAConnection/4.Events.ts b/src/WAConnection/4.Events.ts index ffc1d82..5134515 100644 --- a/src/WAConnection/4.Events.ts +++ b/src/WAConnection/4.Events.ts @@ -1,6 +1,6 @@ import * as QR from 'qrcode-terminal' import { WAConnection as Base } from './3.Connect' -import { WAMessageStatusUpdate, WAMessage, WAContact, WAChat, WAMessageProto, WA_MESSAGE_STUB_TYPE, WA_MESSAGE_STATUS_TYPE, PresenceUpdate, BaileysEvent, DisconnectReason, WANode, WAOpenResult, Presence, AuthenticationCredentials } from './Constants' +import { WAMessageStatusUpdate, WAMessage, WAContact, WAChat, WAMessageProto, WA_MESSAGE_STUB_TYPE, WA_MESSAGE_STATUS_TYPE, PresenceUpdate, BaileysEvent, DisconnectReason, WANode, WAOpenResult, Presence, AuthenticationCredentials, WAParticipantAction, WAGroupMetadata } from './Constants' import { whatsappID, unixTimestampSeconds, isGroupID, GET_MESSAGE_ID, WA_MESSAGE_ID, waMessageKey } from './Utils' import KeyedDB from '@adiwajshing/keyed-db' import { Mutex } from './Mutex' @@ -21,6 +21,23 @@ export class WAConnection extends Base { } this.chatAddMessageAppropriate (message) }) + this.on('CB:Chat,cmd:action', json => { + const data = json[1].data + if (data) { + const emitGroupParticipantsUpdate = (action: WAParticipantAction) => this.emit( + 'group-participants-update', + { participants: data[2].participants.map(whatsappID), actor: data[1], jid: json[1].id, action } + ) + switch (data[0]) { + case "promote": + emitGroupParticipantsUpdate('promote') + break + case "demote": + emitGroupParticipantsUpdate('demote') + break + } + } + }) // presence updates this.on('CB:Presence', json => { const update = json[1] as PresenceUpdate @@ -269,11 +286,14 @@ export class WAConnection extends Base { const jid = chat.jid let actor = whatsappID (message.participant) let participants: string[] + const emitParticipantsUpdate = (action: WAParticipantAction) => this.emit ('group-participants-update', { jid, actor, participants, action }) + const emitGroupUpdate = (update: Partial) => this.emit ('group-update', { jid, actor, ...update }) + switch (message.messageStubType) { case WA_MESSAGE_STUB_TYPE.GROUP_PARTICIPANT_LEAVE: case WA_MESSAGE_STUB_TYPE.GROUP_PARTICIPANT_REMOVE: participants = message.messageStubParameters.map (whatsappID) - this.emit ('group-participants-remove', { jid, actor, participants}) + emitParticipantsUpdate('remove') // mark the chat read only if you left the group if (participants.includes(this.user.jid)) { chat.read_only = 'true' @@ -288,24 +308,34 @@ export class WAConnection extends Base { delete chat.read_only this.emit ('chat-update', { jid, read_only: 'false' }) } - this.emit ('group-participants-add', { jid, participants, actor }) + emitParticipantsUpdate('add') break case WA_MESSAGE_STUB_TYPE.GROUP_CHANGE_ANNOUNCE: const announce = message.messageStubParameters[0] === 'on' ? 'true' : 'false' - this.emit ('group-settings-update', { jid, announce, actor }) + emitGroupUpdate({ announce }) break case WA_MESSAGE_STUB_TYPE.GROUP_CHANGE_ANNOUNCE: const restrict = message.messageStubParameters[0] === 'on' ? 'true' : 'false' - this.emit ('group-settings-update', { jid, restrict, actor }) + emitGroupUpdate({ restrict }) break case WA_MESSAGE_STUB_TYPE.GROUP_CHANGE_DESCRIPTION: - this.emit ('group-description-update', { jid, actor }) + const desc = message.messageStubParameters[0] + emitGroupUpdate({ desc }) break case WA_MESSAGE_STUB_TYPE.GROUP_CHANGE_SUBJECT: case WA_MESSAGE_STUB_TYPE.GROUP_CREATE: chat.name = message.messageStubParameters[0] this.emit ('chat-update', { jid, name: chat.name }) break + case WA_MESSAGE_STUB_TYPE.GROUP_PARTICIPANT_PROMOTE: + case WA_MESSAGE_STUB_TYPE.GROUP_PARTICIPANT_DEMOTE: + participants = message.messageStubParameters.map (whatsappID) + emitParticipantsUpdate( + WA_MESSAGE_STUB_TYPE.GROUP_PARTICIPANT_PROMOTE ? + 'promote' : + 'demote' + ) + break } } } @@ -357,17 +387,9 @@ export class WAConnection extends Base { /** 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 */ - on (event: 'group-participants-remove', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this - /** when participants are promoted in a group */ - on (event: 'group-participants-promote', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this - /** when participants are demoted in a group */ - on (event: 'group-participants-demote', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this - /** when the group settings is updated */ - on (event: 'group-settings-update', listener: (update: {jid: string, restrict?: string, announce?: string, actor?: string}) => void): this - /** when the group description is updated */ - on (event: 'group-description-update', listener: (update: {jid: string, description?: string, actor?: string}) => void): this + on (event: 'group-participants-update', listener: (update: {jid: string, participants: string[], actor?: string, action: WAParticipantAction}) => void): this + /** when the group is updated */ + on (event: 'group-update', listener: (update: Partial & {jid: string, actor?: string}) => void): this /** when WA sends back a pong */ on (event: 'received-pong', listener: () => void): this diff --git a/src/WAConnection/5.User.ts b/src/WAConnection/5.User.ts index 9406ba9..4b3783f 100644 --- a/src/WAConnection/5.User.ts +++ b/src/WAConnection/5.User.ts @@ -1,12 +1,12 @@ import {WAConnection as Base} from './4.Events' -import { Presence, WABroadcastListInfo, WAProfilePictureChange, ChatModification, WALoadChatOptions } from './Constants' +import { Presence, WABroadcastListInfo, WAProfilePictureChange, WALoadChatOptions } from './Constants' import { WAMessage, WANode, WAMetric, WAFlag, } from '../WAConnection/Constants' -import { generateProfilePicture, whatsappID, unixTimestampSeconds } from './Utils' +import { generateProfilePicture, whatsappID } from './Utils' import { Mutex } from './Mutex' // All user related functions -- get profile picture, set status etc. @@ -88,7 +88,7 @@ export class WAConnection extends Base { async getBroadcastListInfo(jid: string) { return this.query({json: ['query', 'contact', jid], expect200: true }) as Promise } /** Delete the chat of a given ID */ async deleteChat (jid: string) { - const response = await this.setQuery ([ ['chat', {type: 'delete', jid: jid}, null] ], [12, WAFlag.ignore]) as {status: number} + const response = await this.setQuery ([ ['chat', {type: 'delete', jid: jid}, null] ], [12, WAFlag.ignore]) const chat = this.chats.get (jid) if (chat) { this.chats.delete (chat) @@ -147,51 +147,4 @@ export class WAConnection extends Base { } return response } - /** - * Modify a given chat (archive, pin etc.) - * @param jid the ID of the person/group you are modifiying - * @param durationMs only for muting, how long to mute the chat for - */ - @Mutex ((jid, type) => jid+type) - async modifyChat (jid: string, type: ChatModification, durationMs?: number) { - jid = whatsappID (jid) - const chat = this.assertChatGet (jid) - - let chatAttrs: Record = {jid: jid} - if (type === ChatModification.mute && !durationMs) { - throw new Error('duration must be set to the timestamp of the time of pinning/unpinning of the chat') - } - - durationMs = durationMs || 0 - switch (type) { - case ChatModification.pin: - case ChatModification.mute: - const strStamp = (unixTimestampSeconds() + Math.floor(durationMs/1000)).toString() - chatAttrs.type = type - chatAttrs[type] = strStamp - break - case ChatModification.unpin: - case ChatModification.unmute: - chatAttrs.type = type.replace ('un', '') // replace 'unpin' with 'pin' - chatAttrs.previous = chat[type.replace ('un', '')] - break - default: - chatAttrs.type = type - break - } - - const response = await this.setQuery ([['chat', chatAttrs, null]]) - - if (chat) { - if (type.includes('un')) { - type = type.replace ('un', '') as ChatModification - delete chat[type.replace('un','')] - this.emit ('chat-update', { jid, [type]: false }) - } else { - chat[type] = chatAttrs[type] || 'true' - this.emit ('chat-update', { jid, [type]: chat[type] }) - } - } - return response - } } diff --git a/src/WAConnection/7.MessagesExtra.ts b/src/WAConnection/7.MessagesExtra.ts index 1319ab8..b0d6a97 100644 --- a/src/WAConnection/7.MessagesExtra.ts +++ b/src/WAConnection/7.MessagesExtra.ts @@ -1,6 +1,6 @@ import {WAConnection as Base} from './6.MessagesSend' -import { MessageType, WAMessageKey, MessageInfo, WAMessageContent, WAMetric, WAFlag, WANode, WAMessage, WAMessageProto } from './Constants' -import { whatsappID, delay, toNumber, unixTimestampSeconds, GET_MESSAGE_ID, WA_MESSAGE_ID } from './Utils' +import { MessageType, WAMessageKey, MessageInfo, WAMessageContent, WAMetric, WAFlag, WANode, WAMessage, WAMessageProto, ChatModification, BaileysError } from './Constants' +import { whatsappID, delay, toNumber, unixTimestampSeconds, GET_MESSAGE_ID, WA_MESSAGE_ID, isGroupID } from './Utils' import { Mutex } from './Mutex' export class WAConnection extends Base { @@ -325,4 +325,64 @@ export class WAConnection extends Base { await this.relayWAMessage (waMessage) return waMessage } + + + /** + * Modify a given chat (archive, pin etc.) + * @param jid the ID of the person/group you are modifiying + * @param durationMs only for muting, how long to mute the chat for + */ + @Mutex ((jid, type) => jid+type) + async modifyChat (jid: string, type: ChatModification, durationMs?: number) { + jid = whatsappID (jid) + const chat = this.assertChatGet (jid) + + let chatAttrs: Record = {jid: jid} + if (type === ChatModification.mute && !durationMs) { + throw new BaileysError( + 'duration must be set to the timestamp of the time of pinning/unpinning of the chat', + { status: 400 } + ) + } + + durationMs = durationMs || 0 + switch (type) { + case ChatModification.pin: + case ChatModification.mute: + const strStamp = (unixTimestampSeconds() + Math.floor(durationMs/1000)).toString() + chatAttrs.type = type + chatAttrs[type] = strStamp + break + case ChatModification.unpin: + case ChatModification.unmute: + chatAttrs.type = type.replace ('un', '') // replace 'unpin' with 'pin' + chatAttrs.previous = chat[type.replace ('un', '')] + break + default: + chatAttrs.type = type + const msg = (await this.loadMessages(jid, 1)).messages[0] + if (msg) { + chatAttrs.index = msg.key.id + chatAttrs.owner = msg.key.fromMe.toString() + } + if (isGroupID(jid)) { + chatAttrs.participant = this.user?.jid + } + break + } + + const response = await this.setQuery ([['chat', chatAttrs, null]], [ WAMetric.chat, WAFlag.ignore ]) + + if (chat) { + if (type.includes('un')) { + type = type.replace ('un', '') as ChatModification + delete chat[type.replace('un','')] + this.emit ('chat-update', { jid, [type]: false }) + } else { + chat[type] = chatAttrs[type] || 'true' + this.emit ('chat-update', { jid, [type]: chat[type] }) + } + } + return response + } } diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index 5056841..a3a8225 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -162,9 +162,9 @@ export interface WAGroupMetadata { descOwner?: string descId?: string /** is set when the group only allows admins to change group settings */ - restrict?: 'true' + restrict?: 'true' | 'false' /** is set when the group only allows admins to write messages */ - announce?: 'true' + announce?: 'true' | 'false' participants: [{ id: string; isAdmin: boolean; isSuperAdmin: boolean }] } export interface WAGroupModification { @@ -413,6 +413,7 @@ export interface WASendMessageResponse { messageID: string message: WAMessage } +export type WAParticipantAction = 'add' | 'remove' | 'promote' | 'demote' export type BaileysEvent = 'open' | 'connecting' | @@ -427,12 +428,8 @@ export type BaileysEvent = 'message-new' | 'message-update' | 'message-status-update' | - 'group-participants-add' | - 'group-participants-remove' | - 'group-participants-promote' | - 'group-participants-demote' | - 'group-settings-update' | - 'group-description-update' | + 'group-participants-update' | + 'group-update' | 'received-pong' | 'credentials-updated' | 'connection-validated' \ No newline at end of file