diff --git a/Example/example.ts b/Example/example.ts index bbbcecc..2dbc38c 100644 --- a/Example/example.ts +++ b/Example/example.ts @@ -57,6 +57,7 @@ const startSock = async() => { await sock.sendMessage(jid, msg) } + sock.ev.on('call', item => console.log('recv call event', item)) sock.ev.on('chats.set', item => console.log(`recv ${item.chats.length} chats (is latest: ${item.isLatest})`)) sock.ev.on('messages.set', item => console.log(`recv ${item.messages.length} messages (is latest: ${item.isLatest})`)) sock.ev.on('contacts.set', item => console.log(`recv ${item.contacts.length} contacts`)) diff --git a/README.md b/README.md index 919619d..8de7ef8 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,8 @@ export type BaileysEventMap = { 'blocklist.set': { blocklist: string[] } 'blocklist.update': { blocklist: string[], type: 'add' | 'remove' } + /** Receive an update on a call, including when the call was received, rejected, accepted */ + 'call': WACallEvent[] } ``` diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 18a155e..239aef1 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -1,8 +1,8 @@ import { proto } from '../../WAProto' import { KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults' -import { BaileysEventMap, MessageReceiptType, MessageUserReceipt, SocketConfig, WAMessageStubType } from '../Types' -import { debouncedTimeout, decodeMessageStanza, delay, encodeBigEndian, generateSignalPubKey, getNextPreKeys, getStatusFromReceiptType, normalizeMessageContent, xmppPreKey, xmppSignedPreKey } from '../Utils' +import { BaileysEventMap, MessageReceiptType, MessageUserReceipt, SocketConfig, WACallEvent, WAMessageStubType } from '../Types' +import { debouncedTimeout, decodeMessageStanza, delay, encodeBigEndian, generateSignalPubKey, getCallStatusFromNode, getNextPreKeys, getStatusFromReceiptType, normalizeMessageContent, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils' import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex' import processMessage from '../Utils/process-message' import { areJidsSameUser, BinaryNode, BinaryNodeAttributes, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, isJidUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary' @@ -42,6 +42,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { ) const msgRetryMap = config.msgRetryCounterMap || { } + const callOfferData: { [id: string]: WACallEvent } = { } const historyCache = new Set() @@ -518,15 +519,43 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { }) ws.on('CB:call', async(node: BinaryNode) => { - logger.info({ node }, 'recv call') - - const [child] = getAllBinaryNodeChildren(node) - if(!!child?.tag) { - sendMessageAck(node, { class: 'call', type: child.tag }) - .catch( - error => onUnexpectedError(error, 'ack call') - ) + const { attrs } = node + const [infoChild] = getAllBinaryNodeChildren(node) + const callId = infoChild.attrs['call-id'] + const from = infoChild.attrs.from || infoChild.attrs['call-creator'] + const status = getCallStatusFromNode(infoChild) + const call: WACallEvent = { + chatId: attrs.from, + from, + id: callId, + date: new Date(+attrs.t * 1000), + offline: !!attrs.offline, + status, } + + if(status === 'offer') { + call.isVideo = !!getBinaryNodeChild(infoChild, 'video') + call.isGroup = infoChild.attrs.type === 'group' + callOfferData[call.id] = call + } + + // use existing call info to populate this event + if(callOfferData[call.id]) { + call.isVideo = callOfferData[call.id].isVideo + call.isGroup = callOfferData[call.id].isGroup + } + + // delete data once call has ended + if(status === 'reject' || status === 'accept' || status === 'timeout') { + delete callOfferData[call.id] + } + + ev.emit('call', [call]) + + await sendMessageAck(node, { class: 'call', type: infoChild.tag }) + .catch( + error => onUnexpectedError(error, 'ack call') + ) }) ws.on('CB:receipt', node => { @@ -552,6 +581,35 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { ) }) + ev.on('call', ([ call ]) => { + // missed call + group call notification message generation + if(call.status === 'timeout' || (call.status === 'offer' && call.isGroup)) { + const msg: proto.IWebMessageInfo = { + key: { + remoteJid: call.chatId, + id: call.id, + fromMe: false + }, + messageTimestamp: unixTimestampSeconds(call.date), + } + if(call.status === 'timeout') { + if(call.isGroup) { + msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_GROUP_VIDEO : WAMessageStubType.CALL_MISSED_GROUP_VOICE + } else { + msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE + } + } else { + msg.message = { call: { callKey: Buffer.from(call.id) } } + } + + const protoMsg = proto.WebMessageInfo.fromObject(msg) + ev.emit( + 'messages.upsert', + { messages: [protoMsg], type: call.offline ? 'append' : 'notify' } + ) + } + }) + return { ...sock, processMessage: processMessageLocal, diff --git a/src/Types/Call.ts b/src/Types/Call.ts new file mode 100644 index 0000000..cea184a --- /dev/null +++ b/src/Types/Call.ts @@ -0,0 +1,14 @@ + +export type WACallUpdateType = 'offer' | 'ringing' | 'timeout' | 'reject' | 'accept' + +export type WACallEvent = { + chatId: string + from: string + isGroup?: boolean + id: string + date: Date + isVideo?: boolean + status: WACallUpdateType + offline: boolean + latencyMs?: number +} \ No newline at end of file diff --git a/src/Types/Events.ts b/src/Types/Events.ts index f1c07e1..e223594 100644 --- a/src/Types/Events.ts +++ b/src/Types/Events.ts @@ -1,6 +1,7 @@ import type EventEmitter from 'events' import { proto } from '../../WAProto' import { AuthenticationCreds } from './Auth' +import { WACallEvent } from './Call' import { Chat, PresenceData } from './Chat' import { Contact } from './Contact' import { GroupMetadata, ParticipantAction } from './GroupMetadata' @@ -48,6 +49,8 @@ export type BaileysEventMap = { 'blocklist.set': { blocklist: string[] } 'blocklist.update': { blocklist: string[], type: 'add' | 'remove' } + /** Receive an update on a call, including when the call was received, rejected, accepted */ + 'call': WACallEvent[] } export interface CommonBaileysEventEmitter extends EventEmitter { diff --git a/src/Types/index.ts b/src/Types/index.ts index 9def6ea..8786e7c 100644 --- a/src/Types/index.ts +++ b/src/Types/index.ts @@ -8,6 +8,7 @@ export * from './Legacy' export * from './Socket' export * from './Events' export * from './Product' +export * from './Call' import type NodeCache from 'node-cache' import { proto } from '../../WAProto' diff --git a/src/Utils/generics.ts b/src/Utils/generics.ts index c59be15..6c560f3 100644 --- a/src/Utils/generics.ts +++ b/src/Utils/generics.ts @@ -5,7 +5,7 @@ import { platform, release } from 'os' import { Logger } from 'pino' import { proto } from '../../WAProto' import { version as baileysVersion } from '../Defaults/baileys-version.json' -import { CommonBaileysEventEmitter, ConnectionState, DisconnectReason, WAVersion } from '../Types' +import { CommonBaileysEventEmitter, ConnectionState, DisconnectReason, WACallUpdateType, WAVersion } from '../Types' import { BinaryNode, getAllBinaryNodeChildren } from '../WABinary' const PLATFORM_MAP = { @@ -287,4 +287,33 @@ export const getErrorCodeFromStreamError = (node: BinaryNode) => { reason, statusCode } +} + +export const getCallStatusFromNode = ({ tag, attrs }: BinaryNode) => { + let status: WACallUpdateType + switch (tag) { + case 'offer': + case 'offer_notice': + status = 'offer' + break + case 'terminate': + if(attrs.reason === 'timeout') { + status = 'timeout' + } else { + status = 'reject' + } + + break + case 'reject': + status = 'reject' + break + case 'accept': + status = 'accept' + break + default: + status = 'ringing' + break + } + + return status } \ No newline at end of file