diff --git a/package.json b/package.json index e8f4906..0f1a86f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adiwajshing/baileys", - "version": "3.3.1", + "version": "3.3.2", "description": "WhatsApp Web API", "homepage": "https://github.com/adiwajshing/Baileys", "main": "lib/WAConnection/WAConnection.js", diff --git a/src/Tests/Common.ts b/src/Tests/Common.ts index 07b09b1..ec385bb 100644 --- a/src/Tests/Common.ts +++ b/src/Tests/Common.ts @@ -30,13 +30,13 @@ export const makeConnection = () => { return conn } -export async function sendAndRetreiveMessage(conn: WAConnection, content, type: MessageType, options: MessageOptions = {}) { - const response = await conn.sendMessage(testJid, content, type, options) - const {messages} = await conn.loadMessages(testJid, 10) +export async function sendAndRetreiveMessage(conn: WAConnection, content, type: MessageType, options: MessageOptions = {}, recipientJid = testJid) { + const response = await conn.sendMessage(recipientJid, content, type, options) + const {messages} = await conn.loadMessages(recipientJid, 10) const message = messages.find (m => m.key.id === response.key.id) assert.ok(message) - const chat = conn.chats.get(testJid) + const chat = conn.chats.get(recipientJid) assert.ok (chat.messages.get(GET_MESSAGE_ID(message.key))) assert.ok (chat.t >= (unixTimestampSeconds()-5) ) diff --git a/src/Tests/Tests.Groups.ts b/src/Tests/Tests.Groups.ts index 7794d0c..468a9fa 100644 --- a/src/Tests/Tests.Groups.ts +++ b/src/Tests/Tests.Groups.ts @@ -1,13 +1,13 @@ import { MessageType, GroupSettingChange, delay, ChatModification, whatsappID } from '../WAConnection/WAConnection' import * as assert from 'assert' -import { WAConnectionTest, testJid } from './Common' +import { WAConnectionTest, testJid, sendAndRetreiveMessage } from './Common' WAConnectionTest('Groups', (conn) => { let gid: string it('should create a group', async () => { const response = await conn.groupCreate('Cool Test Group', [testJid]) assert.ok (conn.chats.get(response.gid)) - + const {chats} = await conn.loadChats(10, null) assert.strictEqual (chats[0].jid, response.gid) // first chat should be new group @@ -24,18 +24,24 @@ WAConnectionTest('Groups', (conn) => { const metadata = await conn.groupMetadata(gid) assert.strictEqual(metadata.id, gid) assert.strictEqual(metadata.participants.filter((obj) => obj.id.split('@')[0] === testJid.split('@')[0]).length, 1) + assert.ok(conn.chats.get(gid)) + assert.ok(conn.chats.get(gid).metadata) }) it('should update the group description', async () => { const newDesc = 'Wow this was set from Baileys' - const waitForEvent = new Promise (resolve => { - conn.once ('group-update', ({jid, actor}) => { - if (jid === gid) { - assert.ok (actor, conn.user.jid) + const waitForEvent = new Promise (resolve => ( + conn.once ('group-update', ({jid, desc}) => { + if (jid === gid && desc) { + assert.strictEqual(desc, newDesc) + assert.strictEqual( + conn.chats.get(jid).metadata.desc, + newDesc + ) resolve () } }) - }) + )) await conn.groupUpdateDescription (gid, newDesc) await waitForEvent @@ -43,7 +49,7 @@ WAConnectionTest('Groups', (conn) => { assert.strictEqual(metadata.desc, newDesc) }) it('should send a message on the group', async () => { - await conn.sendMessage(gid, 'hello', MessageType.text) + await sendAndRetreiveMessage(conn, 'Hello!', MessageType.text, {}, gid) }) it('should quote a message on the group', async () => { const {messages} = await conn.loadMessages (gid, 100) @@ -61,7 +67,8 @@ WAConnectionTest('Groups', (conn) => { const waitForEvent = new Promise (resolve => { conn.once ('chat-update', ({jid, name}) => { if (jid === gid) { - assert.strictEqual (name, subject) + assert.strictEqual(name, subject) + assert.strictEqual(conn.chats.get(jid).name, subject) resolve () } }) @@ -78,6 +85,7 @@ WAConnectionTest('Groups', (conn) => { conn.once ('group-update', ({jid, announce}) => { if (jid === gid) { assert.strictEqual (announce, 'true') + assert.strictEqual(conn.chats.get(gid).metadata.announce, announce) resolve () } }) @@ -85,7 +93,7 @@ WAConnectionTest('Groups', (conn) => { await conn.groupSettingChange (gid, GroupSettingChange.messageSend, true) await waitForEvent - conn.removeAllListeners ('group-settings-update') + conn.removeAllListeners ('group-update') await delay (2000) await conn.groupSettingChange (gid, GroupSettingChange.settingsChange, true) @@ -93,10 +101,17 @@ WAConnectionTest('Groups', (conn) => { it('should promote someone', async () => { const waitForEvent = new Promise (resolve => { - conn.once ('group-participants-update', ({ jid, action }) => { + conn.once ('group-participants-update', ({ jid, action, participants }) => { if (jid === gid) { assert.strictEqual (action, 'promote') - resolve () + console.log(participants) + console.log(conn.chats.get(jid).metadata) + assert.ok( + conn.chats.get(jid).metadata.participants.find(({ id, isAdmin }) => ( + whatsappID(id) === whatsappID(participants[0]) && isAdmin + )), + ) + resolve() } }) @@ -113,6 +128,10 @@ WAConnectionTest('Groups', (conn) => { if (jid === gid) { assert.strictEqual (participants[0], testJid) assert.strictEqual (action, 'remove') + assert.deepStrictEqual( + conn.chats.get(jid).metadata.participants.find(p => whatsappID(p.id) === whatsappID(participants[0])), + undefined + ) resolve () } }) diff --git a/src/WAConnection/3.Connect.ts b/src/WAConnection/3.Connect.ts index 1851312..20b1ef3 100644 --- a/src/WAConnection/3.Connect.ts +++ b/src/WAConnection/3.Connect.ts @@ -89,7 +89,7 @@ export class WAConnection extends Base { this.conn.on('message', data => this.onMessageRecieved(data as any)) - this.conn.on ('open', async () => { + this.conn.once('open', async () => { this.startKeepAliveRequest() this.logger.info(`connected to WhatsApp Web server, authenticating via ${reconnectID ? 'reconnect' : 'takeover'}`) @@ -109,8 +109,8 @@ export class WAConnection extends Base { ) this.conn - .removeAllListeners ('error') - .removeAllListeners ('close') + .removeAllListeners('error') + .removeAllListeners('close') this.stopDebouncedTimeout () resolve ({ ...authResult, ...chatsResult }) } catch (error) { diff --git a/src/WAConnection/4.Events.ts b/src/WAConnection/4.Events.ts index a52ec7e..5504e74 100644 --- a/src/WAConnection/4.Events.ts +++ b/src/WAConnection/4.Events.ts @@ -48,7 +48,9 @@ export class WAConnection extends Base { chat.messages = oldChat.messages if (oldChat.t !== chat.t || oldChat.modify_tag !== chat.modify_tag) { const changes = shallowChanges (oldChat, chat) + delete chat.metadata // remove group metadata as that may have changed; TODO, write better mechanism for this delete changes.messages + updatedChats.push({ ...changes, jid: chat.jid }) } } @@ -138,22 +140,15 @@ export class WAConnection extends Base { // new messages this.on('CB:action,add:relay,message', json => { const message = json[2][0][2] as WAMessage - const jid = whatsappID( message.key.remoteJid ) - if (jid.endsWith('@s.whatsapp.net')) { - const contact = this.contacts[jid] - if (contact && contact?.lastKnownPresence === Presence.composing) { - contact.lastKnownPresence = Presence.available - } - } 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 } - ) + const emitGroupParticipantsUpdate = (action: WAParticipantAction) => this.emitParticipantsUpdate + (json[1].id, data[2].participants.map(whatsappID), action) + const emitGroupUpdate = (data: Partial) => this.emitGroupUpdate(json[1].id, data) + switch (data[0]) { case "promote": emitGroupParticipantsUpdate('promote') @@ -161,6 +156,12 @@ export class WAConnection extends Base { case "demote": emitGroupParticipantsUpdate('demote') break + case "desc_add": + emitGroupUpdate({ ...data[2], descOwner: data[1] }) + break + default: + this.logger.debug({ unhandled: true }, json) + break } } }) @@ -324,26 +325,24 @@ export class WAConnection extends Base { // emit deprecated this.emit('user-presence-update', update) - const contact = this.contacts[jid] - if (contact && jid.endsWith('@s.whatsapp.net')) { // if its a single chat - if (update.t) contact.lastSeen = +update.t - else if (update.type === Presence.unavailable && contact.lastKnownPresence !== Presence.unavailable) { - contact.lastSeen = unixTimestampSeconds() - } - contact.lastKnownPresence = update.type - const presence: WAPresenceData = { - lastKnownPresence: contact.lastKnownPresence, - lastSeen: contact.lastSeen, - name: contact.name || contact.vname || contact.notify + const chat = this.chats.get(chatId) + if (chat && jid.endsWith('@s.whatsapp.net')) { // if its a single chat + chat.presences = chat.presences || {} + + const presence = { ...(chat.presences[jid] || {}) } as WAPresenceData + if (update.t) presence.lastSeen = +update.t + else if (update.type === Presence.unavailable && presence.lastKnownPresence !== Presence.unavailable) { + presence.lastSeen = unixTimestampSeconds() } + presence.lastKnownPresence = update.type - const chat = this.chats.get(chatId) - if (chat) { - chat.presences = chat.presences || {} - chat.presences[jid] = presence - - return { jid: chatId, presences: { [jid]: presence } } as Partial + const contact = this.contacts[jid] + if (contact) { + presence.name = contact.name || contact.notify || contact.vname } + + chat.presences[jid] = presence + return { jid: chatId, presences: { [jid]: presence } } as Partial } } protected forwardStatusUpdate (update: WAMessageStatusUpdate) { @@ -366,12 +365,13 @@ export class WAConnection extends Base { spam: 'false', name } + this.chats.insert (chat) if (this.loadProfilePicturesForChatsAutomatically) { await this.setProfilePicture (chat) } + this.emit ('chat-new', chat) - return chat } /** find a chat or return an error */ @@ -395,9 +395,10 @@ export class WAConnection extends Base { chat.count += 1 chatUpdate.count = chat.count - const contact = this.contacts[message.participant || chat.jid] + const participant = whatsappID(message.participant || chat.jid) + const contact = chat.presences && chat.presences[participant] if (contact?.lastKnownPresence === Presence.composing) { // update presence - const update = this.applyingPresenceUpdate({ id: chat.jid, participant: message.participant || chat.jid, type: Presence.available }) + const update = this.applyingPresenceUpdate({ id: chat.jid, participant, type: Presence.available }) update && Object.assign(chatUpdate, update) } } @@ -446,10 +447,12 @@ export class WAConnection extends Base { // check if the message is an action if (message.messageStubType) { const jid = chat.jid - let actor = whatsappID (message.participant) + //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 }) + const emitParticipantsUpdate = (action: WAParticipantAction) => ( + this.emitParticipantsUpdate(jid, participants, action) + ) + const emitGroupUpdate = (update: Partial) => this.emitGroupUpdate(jid, update) switch (message.messageStubType) { case WA_MESSAGE_STUB_TYPE.GROUP_PARTICIPANT_LEAVE: @@ -480,14 +483,11 @@ export class WAConnection extends Base { const restrict = message.messageStubParameters[0] === 'on' ? 'true' : 'false' emitGroupUpdate({ restrict }) break - case WA_MESSAGE_STUB_TYPE.GROUP_CHANGE_DESCRIPTION: - 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] chatUpdate.name = chat.name + if (chat.metadata) chat.metadata.subject = chat.name break } } @@ -495,6 +495,35 @@ export class WAConnection extends Base { this.emit('chat-update', chatUpdate) } + protected emitParticipantsUpdate = (jid: string, participants: string[], action: WAParticipantAction) => { + const chat = this.chats.get(jid) + const meta = chat?.metadata + if (meta) { + switch (action) { + case 'add': + participants.forEach(id => ( + meta.participants.push({ id, isAdmin: false, isSuperAdmin: false }) + )) + break + case 'remove': + meta.participants = meta.participants.filter(p => !participants.includes(whatsappID(p.id))) + break + case 'promote': + case 'demote': + const isAdmin = action==='promote' + meta.participants.forEach(p => { + if (participants.includes(whatsappID(p.id))) p.isAdmin = isAdmin + }) + break + } + } + this.emit ('group-participants-update', { jid, participants, action }) + } + protected emitGroupUpdate = (jid: string, update: Partial) => { + const chat = this.chats.get(jid) + if (chat.metadata) Object.assign(chat.metadata, update) + this.emit ('group-update', { jid, ...update }) + } protected chatUpdatedMessage (messageIDs: string[], status: WA_MESSAGE_STATUS_TYPE, chat: WAChat) { for (let id of messageIDs) { let msg = chat.messages.get (GET_MESSAGE_ID({ id, fromMe: true })) || chat.messages.get (GET_MESSAGE_ID({ id, fromMe: false })) diff --git a/src/WAConnection/8.Groups.ts b/src/WAConnection/8.Groups.ts index c0a5b31..2cf77b1 100644 --- a/src/WAConnection/8.Groups.ts +++ b/src/WAConnection/8.Groups.ts @@ -1,7 +1,8 @@ import {WAConnection as Base} from './7.MessagesExtra' -import { WAMetric, WAFlag, WANode, WAGroupMetadata, WAGroupCreateResponse, WAGroupModification } from '../WAConnection/Constants' +import { WAMetric, WAFlag, WANode, WAGroupMetadata, WAGroupCreateResponse, WAGroupModification, BaileysError } from '../WAConnection/Constants' import { GroupSettingChange } from './Constants' import { generateMessageID } from '../WAConnection/Utils' +import { Mutex } from './Mutex' export class WAConnection extends Base { /** Generic function for group queries */ @@ -21,8 +22,26 @@ export class WAConnection extends Base { const result = await this.setQuery ([json], [WAMetric.group, 136], tag) return result } - /** Get the metadata of the group */ - groupMetadata = (jid: string) => this.query({json: ['query', 'GroupMetadata', jid], expect200: true}) as Promise + /** + * Get the metadata of the group + * Baileys automatically caches & maintains this state + */ + @Mutex(jid => jid) + async groupMetadata (jid: string) { + const chat = this.chats.get(jid) + let metadata = chat?.metadata + if (!metadata) { + if (chat?.read_only) { + metadata = await this.groupMetadataMinimal(jid) + } else { + metadata = await this.fetchGroupMetadataFromWA(jid) + } + if (chat) chat.metadata = metadata + } + return metadata + } + /** Get the metadata of the group from WA */ + fetchGroupMetadataFromWA = (jid: string) => this.query({json: ['query', 'GroupMetadata', jid], expect200: true}) as Promise /** Get the metadata (works after you've left the group also) */ groupMetadataMinimal = async (jid: string) => { const query = ['query', {type: 'group', jid: jid, epoch: this.msgCount.toString()}, null] @@ -49,18 +68,20 @@ export class WAConnection extends Base { groupCreate = async (title: string, participants: string[]) => { const response = await this.groupQuery('create', null, title, participants) as WAGroupCreateResponse const gid = response.gid + let metadata: WAGroupMetadata try { - await this.groupMetadata (gid) + metadata = await this.groupMetadata (gid) } catch (error) { this.logger.warn (`error in group creation: ${error}, switching gid & checking`) // if metadata is not available const comps = gid.replace ('@g.us', '').split ('-') response.gid = `${comps[0]}-${+comps[1] + 1}@g.us` - await this.groupMetadata (gid) + metadata = await this.groupMetadata (gid) this.logger.warn (`group ID switched from ${gid} to ${response.gid}`) } await this.chatAdd (response.gid, title) + this.chats.get(response.gid).metadata = metadata return response } /** @@ -82,7 +103,7 @@ export class WAConnection extends Base { */ groupUpdateSubject = async (jid: string, title: string) => { const chat = this.chats.get (jid) - if (chat?.name === title) throw new Error ('redundant change') + if (chat?.name === title) throw new BaileysError ('redundant change', { status: 400 }) const response = await this.groupQuery('subject', jid, title) if (chat) chat.name = title diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index b287db6..fdee690 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -180,7 +180,7 @@ export interface WAGroupMetadata { restrict?: 'true' | 'false' /** is set when the group only allows admins to write messages */ announce?: 'true' | 'false' - participants: [{ id: string; isAdmin: boolean; isSuperAdmin: boolean }] + participants: { id: string; isAdmin: boolean; isSuperAdmin: boolean }[] } export interface WAGroupModification { status: number @@ -191,7 +191,7 @@ export interface WAPresenceData { lastSeen?: number name?: string } -export interface WAContact extends WAPresenceData { +export interface WAContact { verify?: string /** name of the contact, the contact has set on their own on WA */ notify?: string @@ -227,6 +227,7 @@ export interface WAChat { messages: KeyedDB imgUrl?: string presences?: { [k: string]: WAPresenceData } + metadata?: WAGroupMetadata } export type WAChatUpdate = Partial & { jid: string, hasNewMessage?: boolean } export enum WAMetric {