diff --git a/README.md b/README.md index dde99f1..805af5a 100644 --- a/README.md +++ b/README.md @@ -254,8 +254,7 @@ To note: ``` ts const info: MessageOptions = { quoted: quotedMessage, // the message you want to quote - contextInfo: { forwardingScore: 2, isForwarded: true }, // some random context info - // (can show a forwarded message with this too) + contextInfo: { forwardingScore: 2, isForwarded: true }, // some random context info (can show a forwarded message with this too) timestamp: Date(), // optional, if you want to manually set the timestamp of the message caption: "hello there!", // (for media messages) the caption to send with the media (cannot be sent with stickers though) thumbnail: "23GD#4/==", /* (for location & media messages) has to be a base 64 encoded JPEG if you want to send a custom thumb, @@ -263,13 +262,16 @@ To note: Do not enter this field if you want to automatically generate a thumb */ mimetype: Mimetype.pdf, /* (for media messages) specify the type of media (optional for all media types except documents), - import {Mimetype} from '@adiwajshing/baileys' + import {Mimetype} from '@adiwajshing/baileys' */ filename: 'somefile.pdf', // (for media messages) file name for the media /* will send audio messages as voice notes, if set to true */ ptt: true, // will detect links & generate a link preview automatically (default true) - detectLinks: true + detectLinks: true, + /** Should it send as a disappearing messages. + * By default 'chat' -- which follows the setting of the chat */ + sendEphemeral: 'chat' } ``` ## Forwarding Messages @@ -359,6 +361,22 @@ await conn.modifyChat (jid, ChatModification.delete) // will delete the chat (ca **Note:** to unmute or unpin a chat, one must pass the timestamp of the pinning or muting. This is returned by the pin & mute functions. This is also available in the `WAChat` objects of the respective chats, as a `mute` or `pin` property. +## Disappearing Messages + +``` ts +const jid = '1234@s.whatsapp.net' // can also be a group +// turn on disappearing messages +await conn.toggleDisappearingMessages( + jid, + WA_DEFAULT_EPHEMERAL // this is 1 week in seconds -- how long you want messages to appear for +) +// will automatically send as a disappearing message +await conn.sendMessage(jid, 'Hello poof!', MessageType.text) +// turn off disappearing messages +await conn.toggleDisappearingMessages(jid, 0) + +``` + ## Misc - To load chats in a paginated manner diff --git a/package.json b/package.json index 0f1a86f..7a686c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adiwajshing/baileys", - "version": "3.3.2", + "version": "3.4.0", "description": "WhatsApp Web API", "homepage": "https://github.com/adiwajshing/Baileys", "main": "lib/WAConnection/WAConnection.js", diff --git a/src/Tests/Tests.Misc.ts b/src/Tests/Tests.Misc.ts index 3667c70..cbed89c 100644 --- a/src/Tests/Tests.Misc.ts +++ b/src/Tests/Tests.Misc.ts @@ -1,8 +1,8 @@ -import { Presence, ChatModification, delay, newMessagesDB } from '../WAConnection/WAConnection' +import { Presence, ChatModification, delay, newMessagesDB, WA_DEFAULT_EPHEMERAL, MessageType } from '../WAConnection/WAConnection' import { promises as fs } from 'fs' import * as assert from 'assert' import fetch from 'node-fetch' -import { WAConnectionTest, testJid, assertChatDBIntegrity } from './Common' +import { WAConnectionTest, testJid, assertChatDBIntegrity, sendAndRetreiveMessage } from './Common' WAConnectionTest('Misc', conn => { @@ -197,4 +197,65 @@ WAConnectionTest('Misc', conn => { await task }) + + it('should toggle disappearing messages', async () => { + let chat = conn.chats.get(testJid) + if (!chat) { + // wait for chats + await new Promise(resolve => ( + conn.once('chats-received', () => resolve()) + )) + chat = conn.chats.get(testJid) + } + + const waitForChatUpdate = (ephemeralOn: boolean) => ( + new Promise(resolve => ( + conn.on('chat-update', ({ jid, ephemeral }) => { + if (jid === testJid && typeof ephemeral !== 'undefined') { + assert.strictEqual(!!(+ephemeral), ephemeralOn) + assert.strictEqual(!!(+chat.ephemeral), ephemeralOn) + resolve() + conn.removeAllListeners('chat-update') + } + }) + )) + ) + const toggleDisappearingMessages = async (on: boolean) => { + const update = waitForChatUpdate(on) + await conn.toggleDisappearingMessages(testJid, on ? WA_DEFAULT_EPHEMERAL : 0) + await update + } + + if (!chat.eph_setting_ts) { + await toggleDisappearingMessages(true) + } + + await delay(1000) + + let msg = await sendAndRetreiveMessage( + conn, + 'This will go poof 😱', + MessageType.text + ) + assert.ok(msg.message?.ephemeralMessage) + + const contextInfo = msg.message?.ephemeralMessage?.message?.extendedTextMessage?.contextInfo + assert.strictEqual(contextInfo.expiration, chat.ephemeral) + assert.strictEqual(+contextInfo.ephemeralSettingTimestamp, +chat.eph_setting_ts) + // test message deletion + await conn.deleteMessage(testJid, msg.key) + + await delay(1000) + + await toggleDisappearingMessages(false) + + await delay(1000) + + msg = await sendAndRetreiveMessage( + conn, + 'This will not go poof 😔', + MessageType.text + ) + assert.ok(msg.message.extendedTextMessage) + }) }) \ No newline at end of file diff --git a/src/WAConnection/4.Events.ts b/src/WAConnection/4.Events.ts index 1232fcd..574ecb9 100644 --- a/src/WAConnection/4.Events.ts +++ b/src/WAConnection/4.Events.ts @@ -438,6 +438,23 @@ export class WAConnection extends Base { update && Object.assign(chatUpdate, update) } } + + const ephemeralProtocolMsg = message.message?.ephemeralMessage?.message?.protocolMessage + if ( + ephemeralProtocolMsg && + ephemeralProtocolMsg.type === WAMessageProto.ProtocolMessage.ProtocolMessageType.EPHEMERAL_SETTING + ) { + chatUpdate.eph_setting_ts = message.messageTimestamp.toString() + chatUpdate.ephemeral = ephemeralProtocolMsg.ephemeralExpiration.toString() + + if (ephemeralProtocolMsg.ephemeralExpiration) { + chat.eph_setting_ts = chatUpdate.eph_setting_ts + chat.ephemeral = chatUpdate.ephemeral + } else { + delete chat.eph_setting_ts + delete chat.ephemeral + } + } const messages = chat.messages const protocolMessage = message.message?.protocolMessage @@ -471,7 +488,7 @@ export class WAConnection extends Base { messages.delete (messages.all()[0]) // delete oldest messages } // only update if it's an actual message - if (message.message) { + if (message.message && !ephemeralProtocolMsg) { this.chatUpdateTime (chat, +toNumber(message.messageTimestamp)) chatUpdate.t = chat.t } @@ -515,7 +532,7 @@ export class WAConnection extends Base { const announce = message.messageStubParameters[0] === 'on' ? 'true' : 'false' emitGroupUpdate({ announce }) break - case WA_MESSAGE_STUB_TYPE.GROUP_CHANGE_ANNOUNCE: + case WA_MESSAGE_STUB_TYPE.GROUP_CHANGE_RESTRICT: const restrict = message.messageStubParameters[0] === 'on' ? 'true' : 'false' emitGroupUpdate({ restrict }) break diff --git a/src/WAConnection/6.MessagesSend.ts b/src/WAConnection/6.MessagesSend.ts index aaf5b28..94c1dd1 100644 --- a/src/WAConnection/6.MessagesSend.ts +++ b/src/WAConnection/6.MessagesSend.ts @@ -9,7 +9,7 @@ import { WALocationMessage, WAContactMessage, WATextMessage, - WAMessageContent, WAMetric, WAFlag, WAMessage, BaileysError, WA_MESSAGE_STATUS_TYPE, WAMessageProto, MediaConnInfo, MessageTypeProto, URL_REGEX, WAUrlInfo + WAMessageContent, WAMetric, WAFlag, WAMessage, BaileysError, WA_MESSAGE_STATUS_TYPE, WAMessageProto, MediaConnInfo, MessageTypeProto, URL_REGEX, WAUrlInfo, WA_DEFAULT_EPHEMERAL } from './Constants' import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes, generateThumbnail, getMediaKeys, decodeMediaMessageBuffer, extensionForMediaMessage, whatsappID, unixTimestampSeconds, getAudioDuration } from './Utils' import { Mutex } from './Mutex' @@ -47,6 +47,22 @@ export class WAConnection extends Base { const preparedMessage = this.prepareMessageFromContent(id, content, options) return preparedMessage } + /** + * Toggles disappearing messages for the given chat + * + * @param jid the chat to toggle + * @param ephemeralExpiration 0 to disable, enter any positive number to enable disappearing messages for the specified duration; + * For the default see WA_DEFAULT_EPHEMERAL + */ + async toggleDisappearingMessages(jid: string, ephemeralExpiration?: number, opts: { waitForAck: boolean } = { waitForAck: true }) { + const message = this.prepareMessageFromContent( + jid, + this.prepareDisappearingMessageSettingContent(ephemeralExpiration), + {} + ) + await this.relayWAMessage(message, opts) + return message + } /** Prepares the message content */ async prepareMessageContent (message: string | WATextMessage | WALocationMessage | WAContactMessage | Buffer, type: MessageType, options: MessageOptions) { let m: WAMessageContent = {} @@ -85,6 +101,20 @@ export class WAConnection extends Base { } return WAMessageProto.Message.fromObject (m) } + prepareDisappearingMessageSettingContent(ephemeralExpiration?: number) { + ephemeralExpiration = ephemeralExpiration || 0 + const content: WAMessageContent = { + ephemeralMessage: { + message: { + protocolMessage: { + type: WAMessageProto.ProtocolMessage.ProtocolMessageType.EPHEMERAL_SETTING, + ephemeralExpiration + } + } + } + } + return WAMessageProto.Message.fromObject(content) + } /** Prepare a media message for sending */ async prepareMessageMedia(buffer: Buffer, mediaType: MessageType, options: MessageOptions = {}) { await this.waitForConnection () @@ -175,7 +205,7 @@ export class WAConnection extends Base { /** prepares a WAMessage for sending from the given content & options */ prepareMessageFromContent(id: string, message: WAMessageContent, options: MessageOptions) { if (!options.timestamp) options.timestamp = new Date() // set timestamp to now - + if (typeof options.sendEphemeral === 'undefined') options.sendEphemeral = 'chat' // prevent an annoying bug (WA doesn't accept sending messages with '@c.us') id = whatsappID (id) @@ -202,6 +232,28 @@ export class WAConnection extends Base { if (options?.thumbnail) { message[key].jpegThumbnail = Buffer.from(options.thumbnail, 'base64') } + + const chat = this.chats.get(id) + if ( + // if we want to send a disappearing message + ((options?.sendEphemeral === 'chat' && chat?.ephemeral) || + options?.sendEphemeral === true) && + // and it's not a protocol message -- delete, toggle disappear message + key !== 'protocolMessage' && + // already not converted to disappearing message + key !== 'ephemeralMessage' + ) { + message[key].contextInfo = { + ...(message[key].contextInfo || {}), + expiration: chat?.ephemeral || WA_DEFAULT_EPHEMERAL, + ephemeralSettingTimestamp: chat?.eph_setting_ts + } + message = { + ephemeralMessage: { + message + } + } + } message = WAMessageProto.Message.fromObject (message) const messageJSON = { diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index 2717e80..46f87ba 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -7,6 +7,7 @@ export const WS_URL = 'wss://web.whatsapp.com/ws' export const DEFAULT_ORIGIN = 'https://web.whatsapp.com' export const KEEP_ALIVE_INTERVAL_MS = 20*1000 +export const WA_DEFAULT_EPHEMERAL = 7*24*60*60 // export the WAMessage Prototypes export { proto as WAMessageProto } @@ -224,6 +225,10 @@ export interface WAChat { spam: 'false' | 'true' modify_tag: string name?: string + /** when ephemeral messages were toggled on */ + eph_setting_ts?: string + /** how long each message lasts for */ + ephemeral?: string // Baileys added properties messages: KeyedDB @@ -367,6 +372,9 @@ export interface MessageOptions { forceNewMediaOptions?: boolean /** Wait for the message to be sent to the server (default true) */ waitForAck?: boolean + /** Should it send as a disappearing messages. + * By default 'chat' -- which follows the setting of the chat */ + sendEphemeral?: 'chat' | boolean } export interface WABroadcastListInfo { status: number