fix: add whatsapp reply context

This commit is contained in:
Peter Steinberger
2025-12-23 02:26:11 +01:00
parent ffe75f3e20
commit 6550e7d562
7 changed files with 145 additions and 7 deletions

View File

@@ -1751,6 +1751,47 @@ describe("web auto-reply", () => {
expect(callArg?.Body).toContain("hello");
});
it("forwards reply-to context to resolver", async () => {
let capturedOnMessage:
| ((msg: import("./inbound.js").WebInboundMessage) => Promise<void>)
| undefined;
const listenerFactory = async (opts: {
onMessage: (
msg: import("./inbound.js").WebInboundMessage,
) => Promise<void>;
}) => {
capturedOnMessage = opts.onMessage;
return { close: vi.fn() };
};
const resolver = vi.fn().mockResolvedValue({ text: "reply" });
await monitorWebProvider(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
body: "hello",
from: "+1555",
to: "+2666",
id: "msg1",
replyToId: "q1",
replyToBody: "original",
replyToSender: "+1999",
sendComposing: vi.fn(),
reply: vi.fn(),
sendMedia: vi.fn(),
});
const callArg = resolver.mock.calls[0]?.[0] as {
ReplyToId?: string;
ReplyToBody?: string;
ReplyToSender?: string;
};
expect(callArg.ReplyToId).toBe("q1");
expect(callArg.ReplyToBody).toBe("original");
expect(callArg.ReplyToSender).toBe("+1999");
});
it("applies responsePrefix to regular replies", async () => {
setLoadConfigMock(() => ({
inbound: {

View File

@@ -1107,6 +1107,9 @@ export async function monitorWebProvider(
From: msg.from,
To: msg.to,
MessageSid: msg.id,
ReplyToId: msg.replyToId,
ReplyToBody: msg.replyToBody,
ReplyToSender: msg.replyToSender,
MediaPath: msg.mediaPath,
MediaUrl: msg.mediaUrl,
MediaType: msg.mediaType,

View File

@@ -39,6 +39,9 @@ export type WebInboundMessage = {
senderJid?: string;
senderE164?: string;
senderName?: string;
replyToId?: string;
replyToBody?: string;
replyToSender?: string;
groupSubject?: string;
groupParticipants?: string[];
mentionedJids?: string[];
@@ -187,6 +190,9 @@ export async function monitorWebInbox(options: {
body = extractMediaPlaceholder(msg.message ?? undefined);
if (!body) continue;
}
const replyContext = describeReplyContext(
msg.message as proto.IMessage | undefined,
);
let mediaPath: string | undefined;
let mediaType: string | undefined;
try {
@@ -211,10 +217,10 @@ export async function monitorWebInbox(options: {
}
};
const reply = async (text: string) => {
await sock.sendMessage(chatJid, { text });
await sock.sendMessage(chatJid, { text }, { quoted: msg });
};
const sendMedia = async (payload: AnyMessageContent) => {
await sock.sendMessage(chatJid, payload);
await sock.sendMessage(chatJid, payload, { quoted: msg });
};
const timestamp = msg.messageTimestamp
? Number(msg.messageTimestamp) * 1000
@@ -249,6 +255,9 @@ export async function monitorWebInbox(options: {
senderJid: participantJid,
senderE164: senderE164 ?? undefined,
senderName,
replyToId: replyContext?.id,
replyToBody: replyContext?.body,
replyToSender: replyContext?.sender,
groupSubject,
groupParticipants,
mentionedJids: mentionedJids ?? undefined,
@@ -443,6 +452,36 @@ export function extractMediaPlaceholder(
return undefined;
}
function describeReplyContext(rawMessage: proto.IMessage | undefined): {
id?: string;
body: string;
sender: string;
} | null {
const message = unwrapMessage(rawMessage);
if (!message) return null;
const contextInfo =
message.extendedTextMessage?.contextInfo ??
message.imageMessage?.contextInfo ??
message.videoMessage?.contextInfo ??
message.documentMessage?.contextInfo ??
message.audioMessage?.contextInfo ??
message.stickerMessage?.contextInfo ??
message.buttonsResponseMessage?.contextInfo ??
message.listResponseMessage?.contextInfo;
const quoted = contextInfo?.quotedMessage as proto.IMessage | undefined;
if (!quoted) return null;
const body = extractText(quoted) ?? extractMediaPlaceholder(quoted);
if (!body) return null;
const senderJid = contextInfo?.participant ?? undefined;
const senderE164 = senderJid ? jidToE164(senderJid) ?? senderJid : undefined;
const sender = senderE164 ?? "unknown sender";
return {
id: contextInfo?.stanzaId ? String(contextInfo.stanzaId) : undefined,
body,
sender,
};
}
async function downloadInboundMedia(
msg: proto.IWebMessageInfo,
sock: Awaited<ReturnType<typeof createWaSocket>>,

View File

@@ -107,9 +107,11 @@ describe("web monitor inbox", () => {
"composing",
"999@s.whatsapp.net",
);
expect(sock.sendMessage).toHaveBeenCalledWith("999@s.whatsapp.net", {
text: "pong",
});
expect(sock.sendMessage).toHaveBeenCalledWith(
"999@s.whatsapp.net",
{ text: "pong" },
{ quoted: expect.objectContaining({ key: { id: "abc" } }) },
);
await listener.close();
});
@@ -151,6 +153,53 @@ describe("web monitor inbox", () => {
await listener.close();
});
it("captures reply context from quoted messages", async () => {
const onMessage = vi.fn(async (msg) => {
await msg.reply("pong");
});
const listener = await monitorWebInbox({ verbose: false, onMessage });
const sock = await createWaSocket();
const upsert = {
type: "notify",
messages: [
{
key: { id: "abc", fromMe: false, remoteJid: "999@s.whatsapp.net" },
message: {
extendedTextMessage: {
text: "reply",
contextInfo: {
stanzaId: "q1",
participant: "111@s.whatsapp.net",
quotedMessage: { conversation: "original" },
},
},
},
messageTimestamp: 1_700_000_000,
pushName: "Tester",
},
],
};
sock.ev.emit("messages.upsert", upsert);
await new Promise((resolve) => setImmediate(resolve));
expect(onMessage).toHaveBeenCalledWith(
expect.objectContaining({
replyToId: "q1",
replyToBody: "original",
replyToSender: "+111",
}),
);
expect(sock.sendMessage).toHaveBeenCalledWith(
"999@s.whatsapp.net",
{ text: "pong" },
{ quoted: expect.objectContaining({ key: { id: "abc" } }) },
);
await listener.close();
});
it("captures media path for image messages", async () => {
const onMessage = vi.fn();
const listener = await monitorWebInbox({ verbose: false, onMessage });