diff --git a/README.md b/README.md index c9efeab3102..18f61b94d3f 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ For more details on saving and restoring sessions, check out the [Authentication | Send location | ✅ | | Send buttons | ❌ [(DEPRECATED)][deprecated-video] | | Send lists | ❌ [(DEPRECATED)][deprecated-video] | +| Chat with Meta AI | ✅ | | Receive location | ✅ | | Message replies | ✅ | | Join groups by invite | ✅ | diff --git a/example.js b/example.js index 7b2af48bff9..37623988708 100644 --- a/example.js +++ b/example.js @@ -73,6 +73,16 @@ client.on('message', async (msg) => { } else if (msg.body === '!ping') { // Send a new message to the same chat client.sendMessage(msg.from, 'pong'); + } else if (msg.body.startsWith('!ai ')) { + // Ask Meta AI and forward its reply back to the chat + const prompt = msg.body.slice(4); + const reply = await client.sendMetaAiMessage(prompt); + if (reply && reply.hasMedia) { + const media = await reply.downloadMedia(); + client.sendMessage(msg.from, media, { caption: reply.body }); + } else { + client.sendMessage(msg.from, reply ? reply.body : 'No response'); + } } else if (msg.body.startsWith('!sendto ')) { // Direct send a new message to specific id let number = msg.body.split(' ')[1]; diff --git a/index.d.ts b/index.d.ts index 8127770492b..a38d4dc730c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -211,11 +211,14 @@ declare namespace WAWebJS { options?: MessageSendOptions, ): Promise; + /** Sends a message to Meta AI and resolves once its reply has finished streaming */ + sendMetaAiMessage( + message: string, + options?: { timeout?: number }, + ): Promise; + /** Send a reaction to a specific messageId */ - sendReaction( - messageId: string, - reaction: string, - ): Promise; + sendReaction(messageId: string, reaction: string): Promise; /** Sends a channel admin invitation to a user, allowing them to become an admin of the channel */ sendChannelAdminInvite( diff --git a/src/Client.js b/src/Client.js index a122a8f336b..20c5e358a47 100644 --- a/src/Client.js +++ b/src/Client.js @@ -1618,6 +1618,25 @@ class Client extends EventEmitter { return sentMsg ? new Message(this, sentMsg) : undefined; } + /** + * Sends a message to Meta AI and resolves once its reply has finished streaming + * @param {string} message The prompt to send to Meta AI + * @param {object} [options] Options + * @param {number} [options.timeout=60000] How long to wait for the reply, in milliseconds + * @returns {Promise} The reply from Meta AI, which may be a text or a media message + */ + async sendMetaAiMessage(message, options = {}) { + const msg = await this.pupPage.evaluate( + (message, options) => { + return window.WWebJS.sendMetaAiMessage(message, options); + }, + message, + options, + ); + + return msg ? new Message(this, msg) : null; + } + /** * Send an emoji reaction to a specific message * @param {string} messageId - Id of the message to add the reaction. diff --git a/src/util/Injected/Utils.js b/src/util/Injected/Utils.js index dfa30a579de..1d949b72855 100644 --- a/src/util/Injected/Utils.js +++ b/src/util/Injected/Utils.js @@ -585,6 +585,79 @@ exports.LoadUtils = () => { .Msg.get(newMsgKey._serialized); }; + window.WWebJS.sendMetaAiMessage = async (message, options = {}) => { + const { Msg } = window.require('WAWebCollections'); + const { BotMsgEditType } = window.require('WAWebBotTypes'); + const chatId = + window.require('WAWebBotUtils').META_BOT_PN_WID._serialized; + const chat = await window.WWebJS.getChat(chatId, { getAsModel: false }); + + if (!chat) return null; + + const previousIds = new Set( + Msg.getModelsArray() + .filter((msg) => msg.id?.remote?._serialized === chatId) + .map((msg) => msg.id?._serialized), + ); + + // Meta AI only replies when the outgoing message carries a bot secret. + const messageSecret = window.crypto.getRandomValues(new Uint8Array(32)); + const botMessageSecret = await window + .require('WAWebBotMessageSecret') + .genBotMsgSecretFromMsgSecret(messageSecret); + + await window.WWebJS.sendMessage(chat, message, { + messageSecret, + botMessageSecret, + }); + + const getReplies = () => + Msg.getModelsArray().filter( + (msg) => + msg.id?.remote?._serialized === chatId && + msg.id?.fromMe === false && + !previousIds.has(msg.id?._serialized), + ); + const isFinished = (msg) => + msg.botEditType === BotMsgEditType.LAST || + msg.botEditType === BotMsgEditType.FULL; + + // The text reply streams in as a single message edited in place until + // the last chunk. A generated image arrives as a separate message right + // after, so wait for a finished reply plus a short quiet period to catch + // any trailing message. + const timeout = options.timeout ?? 60000; + const start = Date.now(); + let signature = ''; + let stableSince = start; + + while (Date.now() - start < timeout) { + const replies = getReplies(); + const currentSignature = replies + .map( + (msg) => + `${msg.id?._serialized}:${msg.botEditType}:${Boolean(msg.directPath)}`, + ) + .join('|'); + if (currentSignature !== signature) { + signature = currentSignature; + stableSince = Date.now(); + } + if (replies.some(isFinished) && Date.now() - stableSince >= 1500) { + break; + } + await new Promise((resolve) => setTimeout(resolve, 300)); + } + + const replies = getReplies(); + const reply = + replies.find((msg) => msg.directPath) || + replies.filter(isFinished).pop() || + replies.pop(); + + return reply ? window.WWebJS.getMessageModel(reply) : null; + }; + window.WWebJS.editMessage = async (msg, content, options = {}) => { const extraOptions = options.extraOptions || {}; delete options.extraOptions;