diff --git a/README.md b/README.md index 47dba5a..b97efdf 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,11 @@ type SocketConfig = { fetchAgent?: Agent /** should the QR be printed in the terminal */ printQRInTerminal: boolean + /** + * fetch a message from your store + * implement this so that messages failed to send (solves the "this message can take a while" issue) can be retried + * */ + getMessage: (key: proto.IMessageKey) => Promise } ``` diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index 1f53db2..9a542e7 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -27,7 +27,8 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = { logger: P().child({ class: 'baileys' }), printQRInTerminal: false, emitOwnEvents: true, - defaultQueryTimeoutMs: 60_000 + defaultQueryTimeoutMs: 60_000, + getMessage: async() => undefined } export const MEDIA_PATH_MAP: { [T in MediaType]: string } = { diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 46921d7..06044e3 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -6,6 +6,7 @@ import { proto } from "../../WAProto" import { KEY_BUNDLE_TYPE } from "../Defaults" import { makeChatsSocket } from "./chats" import { extractGroupMetadata } from "./groups" +import { Boom } from "@hapi/boom" const getStatusFromReceiptType = (type: string | undefined) => { if(type === 'read' || type === 'read-self') { @@ -24,6 +25,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { ev, authState, ws, + assertSession, assertingPreKeys, sendNode, relayMessage, @@ -464,30 +466,64 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } }) + const sendMessagesAgain = async(key: proto.IMessageKey, ids: string[]) => { + const participant = key.participant || key.remoteJid + await assertSession(participant, true) + + logger.debug({ key, ids }, 'recv retry request, forced new session') + + const msgs = await Promise.all( + ids.map(id => ( + config.getMessage({ ...key, id }) + )) + ) + const missingMsgIdx = msgs.findIndex(m => !m) + if(missingMsgIdx >= 0) { + throw new Boom( + `recv request to retry message, but message "${ids[missingMsgIdx]}" not available`, + { statusCode: 404, data: { key } } + ) + } + + for(let i = 0; i < msgs.length;i++) { + await relayMessage(key.remoteJid, msgs[i], { + messageId: ids[i], + participant + }) + } + + } + const handleReceipt = async(node: BinaryNode) => { const { attrs, content } = node - const status = getStatusFromReceiptType(attrs.type) + const remoteJid = attrs.recipient || attrs.from + const fromMe = attrs.recipient ? false : true + const ids = [attrs.id] + if(Array.isArray(content)) { + const items = getBinaryNodeChildren(content[0], 'item') + ids.push(...items.map(i => i.attrs.id)) + } + + const key: proto.IMessageKey = { + remoteJid, + id: '', + fromMe, + participant: attrs.participant + } + + const status = getStatusFromReceiptType(attrs.type) if(typeof status !== 'undefined' && !areJidsSameUser(attrs.from, authState.creds.me?.id)) { - const ids = [attrs.id] - if(Array.isArray(content)) { - const items = getBinaryNodeChildren(content[0], 'item') - ids.push(...items.map(i => i.attrs.id)) - } - - const remoteJid = attrs.recipient || attrs.from - const fromMe = attrs.recipient ? false : true ev.emit('messages.update', ids.map(id => ({ - key: { - remoteJid, - id: id, - fromMe, - participant: attrs.participant - }, + key: { ...key, id }, update: { status } }))) } + if(attrs.type === 'retry') { + await sendMessagesAgain(key, ids) + } + await sendMessageAck(node, { class: 'receipt', type: attrs.type }) } diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index cd988b7..61e7abb 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -238,8 +238,10 @@ export const makeMessagesSocket = (config: SocketConfig) => { const relayMessage = async( jid: string, message: proto.IMessage, - { messageId: msgId, additionalAttributes, cachedGroupMetadata }: MessageRelayOptions + { messageId: msgId, participant, additionalAttributes, cachedGroupMetadata }: MessageRelayOptions ) => { + const meId = authState.creds.me!.id + const { user, server } = jidDecode(jid) const isGroup = server === 'g.us' msgId = msgId || generateMessageID() @@ -250,16 +252,23 @@ export const makeMessagesSocket = (config: SocketConfig) => { const destinationJid = jidEncode(user, isGroup ? 'g.us' : 's.whatsapp.net') + const devices: JidWithDevice[] = [] + if(participant) { + const { user, device } = jidDecode(participant) + devices.push({ user, device }) + } + if(isGroup) { - const { ciphertext, senderKeyDistributionMessageKey } = await encryptSenderKeyMsgSignalProto(destinationJid, encodedMsg, authState.creds.me!.id, authState) + const { ciphertext, senderKeyDistributionMessageKey } = await encryptSenderKeyMsgSignalProto(destinationJid, encodedMsg, meId, authState) let groupData = cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined if(!groupData) groupData = await groupMetadata(jid) - const participantsList = groupData.participants.map(p => p.id) - const devices = await getUSyncDevices(participantsList, false) - - logger.debug(`got ${devices.length} additional devices`) + if(!participant) { + const participantsList = groupData.participants.map(p => p.id) + const devices = await getUSyncDevices(participantsList, false) + devices.push(...devices) + } const encSenderKeyMsg = encodeWAMessage({ senderKeyDistributionMessage: { @@ -304,7 +313,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { content: binaryNodeContent } } else { - const { user: meUser } = jidDecode(authState.creds.me!.id!) + const { user: meUser } = jidDecode(meId) const messageToMyself: proto.IMessage = { deviceSentMessage: { @@ -314,15 +323,13 @@ export const makeMessagesSocket = (config: SocketConfig) => { } const encodedMeMsg = encodeWAMessage(messageToMyself) - participants.push( - await createParticipantNode(jidEncode(user, 's.whatsapp.net'), encodedMsg) - ) - participants.push( - await createParticipantNode(jidEncode(meUser, 's.whatsapp.net'), encodedMeMsg) - ) - const devices = await getUSyncDevices([ authState.creds.me!.id!, jid ], true) - - logger.debug(`got ${devices.length} additional devices`) + if(!participant) { + devices.push({ user }) + devices.push({ user: meUser }) + + const additionalDevices = await getUSyncDevices([ meId, jid ], true) + devices.push(...additionalDevices) + } for(const { user, device } of devices) { const isMe = user === meUser @@ -363,7 +370,8 @@ export const makeMessagesSocket = (config: SocketConfig) => { content: proto.ADVSignedDeviceIdentity.encode(authState.creds.account).finish() }) } - logger.debug({ msgId }, 'sending message') + + logger.debug({ msgId }, `sending message to ${devices.length} devices`) await sendNode(stanza) diff --git a/src/Types/Message.ts b/src/Types/Message.ts index fa3bc14..903a5b8 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -117,6 +117,8 @@ export type AnyMessageContent = AnyRegularMessageContent | { export type MessageRelayOptions = { messageId?: string + /** only send to a specific participant */ + participant?: string additionalAttributes?: { [_: string]: string } cachedGroupMetadata?: (jid: string) => Promise //cachedDevices?: (jid: string) => Promise diff --git a/src/Types/index.ts b/src/Types/index.ts index d911a2d..113a92d 100644 --- a/src/Types/index.ts +++ b/src/Types/index.ts @@ -18,6 +18,7 @@ import { ConnectionState } from './State' import { GroupMetadata, ParticipantAction } from './GroupMetadata' import { MessageInfoUpdate, MessageUpdateType, WAMessage, WAMessageUpdate, WAMessageKey } from './Message' +import { proto } from '../../WAProto' export type WAVersion = [number, number, number] export type WABrowserDescription = [string, string, string] @@ -54,6 +55,11 @@ export type SocketConfig = { mediaCache?: NodeCache /** map to store the retry counts for failed messages */ msgRetryCounterMap?: { [msgId: string]: number } + /** + * fetch a message from your store + * implement this so that messages failed to send (solves the "this message can take a while" issue) can be retried + * */ + getMessage: (key: proto.IMessageKey) => Promise } export enum DisconnectReason {