Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | ✅ |
Expand Down
10 changes: 10 additions & 0 deletions example.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
11 changes: 7 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,14 @@ declare namespace WAWebJS {
options?: MessageSendOptions,
): Promise<Message>;

/** Sends a message to Meta AI and resolves once its reply has finished streaming */
sendMetaAiMessage(
message: string,
options?: { timeout?: number },
): Promise<Message | null>;

/** Send a reaction to a specific messageId */
sendReaction(
messageId: string,
reaction: string,
): Promise<void>;
sendReaction(messageId: string, reaction: string): Promise<void>;

/** Sends a channel admin invitation to a user, allowing them to become an admin of the channel */
sendChannelAdminInvite(
Expand Down
19 changes: 19 additions & 0 deletions src/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Message>} 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.
Expand Down
73 changes: 73 additions & 0 deletions src/util/Injected/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down