import {WAConnection as Base} from './6.MessagesSend' 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 { @Mutex () async loadAllUnreadMessages () { const tasks = this.chats.all() .filter(chat => chat.count > 0) .map (chat => this.loadMessages(chat.jid, chat.count)) const list = await Promise.all (tasks) const combined: WAMessage[] = [] list.forEach (({messages}) => combined.push(...messages)) return combined } /** Get the message info, who has read it, who its been delivered to */ @Mutex ((jid, messageID) => jid+messageID) async messageInfo (jid: string, messageID: string) { const query = ['query', {type: 'message_info', index: messageID, jid: jid, epoch: this.msgCount.toString()}, null] const response = (await this.query ({json: query, binaryTags: [22, WAFlag.ignore], expect200: true}))[2] const info: MessageInfo = {reads: [], deliveries: []} if (response) { const reads = response.filter (node => node[0] === 'read') if (reads[0]) { info.reads = reads[0][2].map (item => item[1]) } const deliveries = response.filter (node => node[0] === 'delivery') if (deliveries[0]) { info.deliveries = deliveries[0][2].map (item => item[1]) } } return info } /** * Marks a chat as read/unread; updates the chat object too * @param jid the ID of the person/group whose message you want to mark read * @param unread unreads the chat, if true */ @Mutex (jid => jid) async chatRead (jid: string, type: 'unread' | 'read' = 'read') { jid = whatsappID (jid) const chat = this.assertChatGet (jid) const message = (await this.loadMessages(jid, 1)).messages[0] const count = type === 'unread' ? -2 : Math.abs(chat.count) if (type === 'unread' || chat.count !== 0) { await this.sendReadReceipt (jid, message.key, count) } chat.count = type === 'unread' ? -1 : 0 this.emit ('chat-update', {jid, count: chat.count}) } /** * Sends a read receipt for a given message; * does not update the chat do @see chatRead * @param jid the ID of the person/group whose message you want to mark read * @param messageKey the key of the message * @param count number of messages to read, set to < 0 to unread a message */ async sendReadReceipt(jid: string, messageKey: WAMessageKey, count: number) { const attributes = { jid, count: count.toString(), index: messageKey?.id, participant: messageKey?.participant || undefined, owner: messageKey?.fromMe?.toString() } const read = await this.setQuery ([['read', attributes, null]], [ WAMetric.read, WAFlag.ignore ]) return read } async fetchMessagesFromWA (jid: string, count: number, indexMessage?: { id?: string; fromMe?: boolean }, mostRecentFirst: boolean = true) { const json = [ 'query', { epoch: this.msgCount.toString(), type: 'message', jid: jid, kind: mostRecentFirst ? 'before' : 'after', count: count.toString(), index: indexMessage?.id, owner: indexMessage?.fromMe === false ? 'false' : 'true', }, null, ] const response = await this.query({json, binaryTags: [WAMetric.queryMessages, WAFlag.ignore], expect200: false}) return (response[2] as WANode[])?.map(item => item[2] as WAMessage) || [] } /** * Load the conversation with a group or person * @param count the number of messages to load * @param cursor the data for which message to offset the query by * @param mostRecentFirst retreive the most recent message first or retreive from the converation start */ @Mutex (jid => jid) async loadMessages ( jid: string, count: number, cursor?: { id?: string; fromMe?: boolean }, mostRecentFirst: boolean = true ) { jid = whatsappID(jid) const retreive = (count: number, indexMessage: any) => this.fetchMessagesFromWA (jid, count, indexMessage, mostRecentFirst) const chat = this.chats.get (jid) const hasCursor = cursor?.id && typeof cursor?.fromMe !== 'undefined' const cursorValue = hasCursor && chat.messages.get (GET_MESSAGE_ID(cursor)) let messages: WAMessage[] if (chat?.messages && mostRecentFirst && (!hasCursor || cursorValue)) { messages = chat.messages.paginatedByValue (cursorValue, count, null, 'before') const diff = count - messages.length if (diff < 0) { messages = messages.slice(-count) // get the last X messages } else if (diff > 0) { const fMessage = chat.messages.all()[0] let fepoch = (fMessage && fMessage['epoch']) || 0 const extra = await retreive (diff, messages[0]?.key || cursor) // add to DB for (let i = extra.length-1;i >= 0; i--) { const m = extra[i] fepoch -= 1 m['epoch'] = fepoch if(chat.messages.length < this.maxCachedMessages && !chat.messages.get (WA_MESSAGE_ID(m))) { chat.messages.insert(m) } } messages.unshift (...extra) } } else messages = await retreive (count, cursor) if (messages[0]) cursor = { id: messages[0].key.id, fromMe: messages[0].key.fromMe } else cursor = null return {messages, cursor} } /** * Load the entire friggin conversation with a group or person * @param onMessage callback for every message retreived * @param chunkSize the number of messages to load in a single request * @param mostRecentFirst retreive the most recent message first or retreive from the converation start */ loadAllMessages(jid: string, onMessage: (m: WAMessage) => void, chunkSize = 25, mostRecentFirst = true) { let offsetID = null const loadMessage = async () => { const {messages} = await this.loadMessages(jid, chunkSize, offsetID, mostRecentFirst) // callback with most recent message first (descending order of date) let lastMessage if (mostRecentFirst) { for (let i = messages.length - 1; i >= 0; i--) { onMessage(messages[i]) lastMessage = messages[i] } } else { for (let i = 0; i < messages.length; i++) { onMessage(messages[i]) lastMessage = messages[i] } } // if there are still more messages if (messages.length >= chunkSize) { offsetID = lastMessage.key // get the last message return new Promise((resolve, reject) => { // send query after 200 ms setTimeout(() => loadMessage().then(resolve).catch(reject), 200) }) } } return loadMessage() as Promise } /** * Find a message in a given conversation * @param chunkSize the number of messages to load in a single request * @param onMessage callback for every message retreived, if return true -- the loop will break */ async findMessage (jid: string, chunkSize: number, onMessage: (m: WAMessage) => boolean) { const chat = this.chats.get (whatsappID(jid)) let count = chat?.messages?.all().length || chunkSize let offsetID while (true) { const {messages, cursor} = await this.loadMessages(jid, count, offsetID, true) // callback with most recent message first (descending order of date) for (let i = messages.length - 1; i >= 0; i--) { if (onMessage(messages[i])) return } if (messages.length === 0) return // if there are more messages offsetID = cursor await delay (200) } } /** * Loads all messages sent after a specific date */ async messagesReceivedAfter (date: Date, onlyUnrespondedMessages = false) { const stamp = unixTimestampSeconds (date) // find the index where the chat timestamp becomes greater const idx = this.chats.all ().findIndex (c => c.t < stamp) // all chats before that index -- i.e. all chats that were updated after that const chats = this.chats.all ().slice (0, idx) const messages: WAMessage[] = [] await Promise.all ( chats.map (async chat => { await this.findMessage (chat.jid, 5, m => { if (toNumber(m.messageTimestamp) < stamp || (onlyUnrespondedMessages && m.key.fromMe)) return true messages.push (m) }) }) ) return messages } /** Load a single message specified by the ID */ async loadMessage (jid: string, id: string) { let message: WAMessage jid = whatsappID (jid) const chat = this.chats.get (jid) if (chat) { // see if message is present in cache message = chat.messages.get (GET_MESSAGE_ID({ id, fromMe: true })) || chat.messages.get (GET_MESSAGE_ID({ id, fromMe: false })) } if (!message) { // load the message before the given message let messages = (await this.loadMessages (jid, 1, {id, fromMe: true})).messages if (!messages[0]) messages = (await this.loadMessages (jid, 1, {id, fromMe: false})).messages // the message after the loaded message is the message required const actual = await this.loadMessages (jid, 1, messages[0] && messages[0].key, false) message = actual.messages[0] } return message } /** * Search WhatsApp messages with a given text string * @param txt the search string * @param inJid the ID of the chat to search in, set to null to search all chats * @param count number of results to return * @param page page number of results (starts from 1) */ async searchMessages(txt: string, inJid: string | null, count: number, page: number) { const json = [ 'query', { epoch: this.msgCount.toString(), type: 'search', search: Buffer.from(txt, 'utf-8'), count: count.toString(), page: page.toString(), jid: inJid }, null, ] const response: WANode = await this.query({json, binaryTags: [24, WAFlag.ignore], expect200: true}) // encrypt and send off const messages = response[2] ? response[2].map (row => row[2]) : [] return { last: response[1]['last'] === 'true', messages: messages as WAMessage[] } } /** * Delete a message in a chat for yourself * @param messageKey key of the message you want to delete */ @Mutex (m => m.remoteJid) async clearMessage (messageKey: WAMessageKey) { const tag = Math.round(Math.random ()*1000000) const attrs: WANode = [ 'chat', { jid: messageKey.remoteJid, modify_tag: tag.toString(), type: 'clear' }, [ ['item', {owner: `${messageKey.fromMe}`, index: messageKey.id}, null] ] ] const result = await this.setQuery ([attrs]) const chat = this.chats.get (whatsappID(messageKey.remoteJid)) if (chat) { const value = chat.messages.get (GET_MESSAGE_ID(messageKey)) value && chat.messages.delete (value) } return result } /** * Delete a message in a chat for everyone * @param id the person or group where you're trying to delete the message * @param messageKey key of the message you want to delete */ async deleteMessage (id: string, messageKey: WAMessageKey) { const json: WAMessageContent = { protocolMessage: { key: messageKey, type: WAMessageProto.ProtocolMessage.PROTOCOL_MESSAGE_TYPE.REVOKE } } const waMessage = this.prepareMessageFromContent (id, json, {}) await this.relayWAMessage (waMessage) return waMessage } /** * Forward a message like WA does * @param id the id to forward the message to * @param message the message to forward * @param forceForward will show the message as forwarded even if it is from you */ async forwardMessage(id: string, message: WAMessage, forceForward: boolean=false) { const content = message.message if (!content) throw new Error ('no content in message') let key = Object.keys(content)[0] let score = content[key].contextInfo?.forwardingScore || 0 score += message.key.fromMe && !forceForward ? 0 : 1 if (key === MessageType.text) { content[MessageType.extendedText] = { text: content[key] } delete content[MessageType.text] key = MessageType.extendedText } if (score > 0) content[key].contextInfo = { forwardingScore: score, isForwarded: true } else content[key].contextInfo = {} const waMessage = this.prepareMessageFromContent (id, content, {}) 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 } }