From 084761dc31278490398be758bc6d84635bf4f082 Mon Sep 17 00:00:00 2001 From: vini Date: Sun, 7 Sep 2025 08:08:18 -0300 Subject: [PATCH] feat: eventMessage sending / call link creation (#1677) --- src/Defaults/index.ts | 2 ++ src/Socket/chats.ts | 23 +++++++++++++++++++++++ src/Socket/messages-send.ts | 13 +++++++++++++ src/Types/Message.ts | 15 +++++++++++++++ src/Utils/messages.ts | 24 +++++++++++++++++++++++- 5 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index fd5bb74a..118f567f 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -10,6 +10,8 @@ const { version } = defaultVersion export const UNAUTHORIZED_CODES = [401, 403, 419] export const DEFAULT_ORIGIN = 'https://web.whatsapp.com' +export const CALL_VIDEO_PREFIX = 'https://call.whatsapp.com/video/' +export const CALL_AUDIO_PREFIX = 'https://call.whatsapp.com/voice/' export const DEF_CALLBACK_PREFIX = 'CB:' export const DEF_TAG_PREFIX = 'TAG:' export const PHONE_CONNECTION_CB = 'CB:Pong' diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index b1ebb55c..3c29c399 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -633,6 +633,28 @@ export const makeChatsSocket = (config: SocketConfig) => { return child?.attrs?.url } + const createCallLink = async (type: 'audio' | 'video', event?: { startTime: number }, timeoutMs?: number) => { + const result = await query( + { + tag: 'call', + attrs: { + id: generateMessageTag(), + to: '@call' + }, + content: [ + { + tag: 'link_create', + attrs: { media: type }, + content: event ? [{ tag: 'event', attrs: { start_time: String(event.startTime) } }] : undefined + } + ] + }, + timeoutMs + ) + const child = getBinaryNodeChild(result, 'link_create') + return child?.attrs?.token + } + const sendPresenceUpdate = async (type: WAPresence, toJid?: string) => { const me = authState.creds.me! if (type === 'available' || type === 'unavailable') { @@ -1139,6 +1161,7 @@ export const makeChatsSocket = (config: SocketConfig) => { return { ...sock, + createCallLink, getBotListV2, processingMutex, fetchPrivacySettings, diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 99a30343..4050d829 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -655,6 +655,10 @@ export const makeMessagesSocket = (config: SocketConfig) => { return 'poll' } + if (message.eventMessage) { + return 'event' + } + return 'text' } @@ -817,12 +821,14 @@ export const makeMessagesSocket = (config: SocketConfig) => { }), //TODO: CACHE getProfilePicUrl: sock.profilePictureUrl, + getCallLink: sock.createCallLink, upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options, messageId: generateMessageIDV2(sock.user?.id), ...options }) + const isEventMsg = 'event' in content && !!content.event const isDeleteMsg = 'delete' in content && !!content.delete const isEditMsg = 'edit' in content && !!content.edit const isPinMsg = 'pin' in content && !!content.pin @@ -848,6 +854,13 @@ export const makeMessagesSocket = (config: SocketConfig) => { polltype: 'creation' } } as BinaryNode) + } else if (isEventMsg) { + additionalNodes.push({ + tag: 'meta', + attrs: { + event_type: 'creation' + } + } as BinaryNode) } if ('cachedGroupMetadata' in options) { diff --git a/src/Types/Message.ts b/src/Types/Message.ts index ff73c7d6..d6ce0944 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -131,6 +131,19 @@ export type PollMessageOptions = { toAnnouncementGroup?: boolean } +export type EventMessageOptions = { + name: string + description?: string + startDate: Date + endDate?: Date + location?: WALocationMessage + call?: 'audio' | 'video' + isCancelled?: boolean + isScheduleCall?: boolean + extraGuestsAllowed?: boolean + messageSecret?: Uint8Array +} + type SharePhoneNumber = { sharePhoneNumber: boolean } @@ -203,6 +216,7 @@ export type AnyRegularMessageContent = ( Contextable & Editable) | AnyMediaMessageContent + | { event: EventMessageOptions } | ({ poll: PollMessageOptions } & Mentionable & @@ -327,6 +341,7 @@ export type MediaGenerationOptions = { export type MessageContentGenerationOptions = MediaGenerationOptions & { getUrlInfo?: (text: string) => Promise getProfilePicUrl?: (jid: string, type: 'image' | 'preview') => Promise + getCallLink?: (type: 'audio' | 'video', event?: { startTime: number }) => Promise jid?: string } export type MessageGenerationOptions = MessageContentGenerationOptions & MessageGenerationOptionsFromContent diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index 32ef39b7..a872553d 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -4,7 +4,7 @@ import { randomBytes } from 'crypto' import { promises as fs } from 'fs' import { type Transform } from 'stream' import { proto } from '../../WAProto/index.js' -import { MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults' +import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults' import type { AnyMediaMessageContent, AnyMessageContent, @@ -490,6 +490,28 @@ export const generateWAMessageContent = async ( }) } else if ('listReply' in message) { m.listResponseMessage = { ...message.listReply } + } else if ('event' in message) { + m.eventMessage = {} + const startTime = Math.floor(message.event.startDate.getTime() / 1000) + + if (message.event.call && options.getCallLink) { + const token = await options.getCallLink(message.event.call, { startTime }) + m.eventMessage.joinLink = (message.event.call === 'audio' ? CALL_AUDIO_PREFIX : CALL_VIDEO_PREFIX) + token + } + + m.messageContextInfo = { + // encKey + messageSecret: message.event.messageSecret || randomBytes(32) + } + + m.eventMessage.name = message.event.name + m.eventMessage.description = message.event.description + m.eventMessage.startTime = startTime + m.eventMessage.endTime = message.event.endDate ? message.event.endDate.getTime() / 1000 : undefined + m.eventMessage.isCanceled = message.event.isCancelled ?? false + m.eventMessage.extraGuestsAllowed = message.event.extraGuestsAllowed + m.eventMessage.isScheduleCall = message.event.isScheduleCall ?? false + m.eventMessage.location = message.event.location } else if ('poll' in message) { message.poll.selectableCount ||= 0 message.poll.toAnnouncementGroup ||= false