Add WhatsApp reactions support

Summary:

Test Plan:
This commit is contained in:
Sash Zats
2026-01-06 20:42:05 -05:00
committed by Peter Steinberger
parent aa87d6cee8
commit 551a8d5683
12 changed files with 207 additions and 2 deletions

View File

@@ -14,6 +14,13 @@ export type ActiveWebListener = {
options?: ActiveWebSendOptions,
) => Promise<{ messageId: string }>;
sendPoll: (to: string, poll: PollInput) => Promise<{ messageId: string }>;
sendReaction: (
chatJid: string,
messageId: string,
emoji: string,
fromMe: boolean,
participant?: string,
) => Promise<void>;
sendComposingTo: (to: string) => Promise<void>;
close?: () => Promise<void>;
};

View File

@@ -566,6 +566,30 @@ export async function monitorWebInbox(options: {
const jid = toWhatsappJid(to);
await sock.sendPresenceUpdate("composing", jid);
},
/**
* Send a reaction (emoji) to a specific message.
* Pass an empty string for emoji to remove the reaction.
*/
sendReaction: async (
chatJid: string,
messageId: string,
emoji: string,
fromMe: boolean,
participant?: string,
): Promise<void> => {
const jid = toWhatsappJid(chatJid);
await sock.sendMessage(jid, {
react: {
text: emoji,
key: {
remoteJid: jid,
id: messageId,
fromMe,
participant,
},
},
});
},
} as const;
}

View File

@@ -8,16 +8,26 @@ vi.mock("./media.js", () => ({
loadWebMedia: (...args: unknown[]) => loadWebMediaMock(...args),
}));
import { sendMessageWhatsApp, sendPollWhatsApp } from "./outbound.js";
import {
sendMessageWhatsApp,
sendPollWhatsApp,
sendReactionWhatsApp,
} from "./outbound.js";
describe("web outbound", () => {
const sendComposingTo = vi.fn(async () => {});
const sendMessage = vi.fn(async () => ({ messageId: "msg123" }));
const sendPoll = vi.fn(async () => ({ messageId: "poll123" }));
const sendReaction = vi.fn(async () => {});
beforeEach(() => {
vi.clearAllMocks();
setActiveWebListener({ sendComposingTo, sendMessage, sendPoll });
setActiveWebListener({
sendComposingTo,
sendMessage,
sendPoll,
sendReaction,
});
});
afterEach(() => {
@@ -156,4 +166,18 @@ describe("web outbound", () => {
durationHours: undefined,
});
});
it("sends reactions via active listener", async () => {
await sendReactionWhatsApp("1555@s.whatsapp.net", "msg123", "✅", {
verbose: false,
fromMe: false,
});
expect(sendReaction).toHaveBeenCalledWith(
"1555@s.whatsapp.net",
"msg123",
"✅",
false,
undefined,
);
});
});

View File

@@ -91,6 +91,52 @@ export async function sendMessageWhatsApp(
throw err;
}
}
export async function sendReactionWhatsApp(
chatJid: string,
messageId: string,
emoji: string,
options: {
verbose: boolean;
fromMe?: boolean;
participant?: string;
},
): Promise<void> {
const correlationId = randomUUID();
const active = getActiveWebListener();
if (!active) {
throw new Error(
"No active gateway listener. Start the gateway before sending WhatsApp reactions.",
);
}
const logger = getChildLogger({
module: "web-outbound",
correlationId,
chatJid,
messageId,
});
try {
const jid = toWhatsappJid(chatJid);
outboundLog.info(`Sending reaction "${emoji}" -> message ${messageId}`);
logger.info({ chatJid: jid, messageId, emoji }, "sending reaction");
await active.sendReaction(
chatJid,
messageId,
emoji,
options.fromMe ?? false,
options.participant,
);
outboundLog.info(`Sent reaction "${emoji}" -> message ${messageId}`);
logger.info({ chatJid: jid, messageId, emoji }, "sent reaction");
} catch (err) {
logger.error(
{ err: String(err), chatJid, messageId, emoji },
"failed to send reaction via web session",
);
throw err;
}
}
export async function sendPollWhatsApp(
to: string,
poll: PollInput,