feat(whatsapp): handle reaction removals and improve sender detection
- Emit reaction events with isRemoval flag instead of skipping them - Fall back to chatJid for DM sender when reaction.key is missing - Add chatType and accountId to reaction logs for observability - Update tests to reflect new removal handling behavior Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6b20d5640b
commit
a0d3aaf541
@ -215,14 +215,16 @@ export async function monitorWebChannel(
|
||||
},
|
||||
});
|
||||
const senderLabel = reaction.senderE164 ?? reaction.senderJid ?? "someone";
|
||||
const text = `WhatsApp reaction added: ${reaction.emoji} by ${senderLabel} msg ${reaction.messageId}`;
|
||||
const action = reaction.isRemoval ? "removed" : "added";
|
||||
const emojiPart = reaction.isRemoval ? "" : `: ${reaction.emoji}`;
|
||||
const text = `WhatsApp reaction ${action}${emojiPart} by ${senderLabel} msg ${reaction.messageId}`;
|
||||
const contextKey = [
|
||||
"whatsapp",
|
||||
"reaction",
|
||||
"added",
|
||||
action,
|
||||
reaction.messageId,
|
||||
reaction.senderJid ?? "unknown",
|
||||
reaction.emoji,
|
||||
reaction.emoji || "removed",
|
||||
reaction.chatJid,
|
||||
].join(":");
|
||||
enqueueSystemEvent(text, { sessionKey: route.sessionKey, contextKey });
|
||||
|
||||
@ -336,23 +336,42 @@ export async function monitorWebInbox(options: {
|
||||
if (chatJid.endsWith("@status") || chatJid.endsWith("@broadcast")) continue;
|
||||
|
||||
const emoji = entry.reaction?.text ?? "";
|
||||
// Empty emoji = reaction removed; skip removals (matches Signal behavior)
|
||||
if (!emoji) continue;
|
||||
const isRemoval = !emoji;
|
||||
|
||||
const group = isJidGroup(chatJid) === true;
|
||||
// In DMs, reactionKey.fromMe means we reacted (remoteJid is the partner, not us)
|
||||
const senderJid = reactionKey?.fromMe
|
||||
? (selfJid ?? undefined)
|
||||
: (reactionKey?.participant ?? reactionKey?.remoteJid ?? undefined);
|
||||
// Determine who sent the reaction:
|
||||
// - fromMe=true → we reacted (use selfJid)
|
||||
// - Otherwise: use reaction.key metadata, fallback to chatJid for DMs
|
||||
let senderJid: string | undefined;
|
||||
if (reactionKey?.fromMe) {
|
||||
senderJid = selfJid ?? undefined;
|
||||
} else {
|
||||
// Prefer explicit sender info from reaction.key, fall back to chatJid for DMs
|
||||
senderJid =
|
||||
reactionKey?.participant ?? reactionKey?.remoteJid ?? (group ? undefined : chatJid);
|
||||
}
|
||||
const senderE164 = senderJid ? await resolveInboundJid(senderJid) : null;
|
||||
|
||||
inboundLogger.info({ emoji, messageId, chatJid, senderJid }, "inbound reaction");
|
||||
const chatType = group ? "group" : "direct";
|
||||
inboundLogger.info(
|
||||
{
|
||||
emoji: emoji || "(removed)",
|
||||
messageId,
|
||||
chatJid,
|
||||
chatType,
|
||||
senderJid,
|
||||
isRemoval,
|
||||
accountId: options.accountId,
|
||||
},
|
||||
isRemoval ? "inbound reaction removed" : "inbound reaction added",
|
||||
);
|
||||
|
||||
options.onReaction({
|
||||
messageId,
|
||||
emoji,
|
||||
isRemoval,
|
||||
chatJid,
|
||||
chatType: group ? "group" : "direct",
|
||||
chatType,
|
||||
accountId: options.accountId,
|
||||
senderJid: senderJid ?? undefined,
|
||||
senderE164: senderE164 ?? undefined,
|
||||
|
||||
@ -44,8 +44,10 @@ export type WebInboundMessage = {
|
||||
export type WebInboundReaction = {
|
||||
/** Message ID being reacted to. */
|
||||
messageId: string;
|
||||
/** Emoji text (empty string = reaction removed). */
|
||||
/** Emoji text (empty string when isRemoval=true). */
|
||||
emoji: string;
|
||||
/** True if this is a reaction removal (emoji will be empty). */
|
||||
isRemoval?: boolean;
|
||||
/** JID of the chat where the reaction occurred. */
|
||||
chatJid: string;
|
||||
chatType: "direct" | "group";
|
||||
|
||||
@ -142,7 +142,7 @@ describe("web monitor inbox – inbound reactions", () => {
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("skips reaction removals (empty emoji)", async () => {
|
||||
it("handles reaction removals (empty emoji)", async () => {
|
||||
const onReaction = vi.fn();
|
||||
|
||||
const listener = await monitorWebInbox({
|
||||
@ -169,7 +169,14 @@ describe("web monitor inbox – inbound reactions", () => {
|
||||
]);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(onReaction).not.toHaveBeenCalled();
|
||||
expect(onReaction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messageId: "msg-123",
|
||||
emoji: "",
|
||||
isRemoval: true,
|
||||
chatJid: "999@s.whatsapp.net",
|
||||
}),
|
||||
);
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
@ -416,12 +423,13 @@ describe("web monitor inbox – inbound reactions", () => {
|
||||
]);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
// In DMs without reaction.key, we use chatJid as the sender
|
||||
expect(onReaction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messageId: "msg-123",
|
||||
emoji: "🔥",
|
||||
senderJid: undefined,
|
||||
senderE164: undefined,
|
||||
senderJid: "999@s.whatsapp.net",
|
||||
chatType: "direct",
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user