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 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 = [
|
const contextKey = [
|
||||||
"whatsapp",
|
"whatsapp",
|
||||||
"reaction",
|
"reaction",
|
||||||
"added",
|
action,
|
||||||
reaction.messageId,
|
reaction.messageId,
|
||||||
reaction.senderJid ?? "unknown",
|
reaction.senderJid ?? "unknown",
|
||||||
reaction.emoji,
|
reaction.emoji || "removed",
|
||||||
reaction.chatJid,
|
reaction.chatJid,
|
||||||
].join(":");
|
].join(":");
|
||||||
enqueueSystemEvent(text, { sessionKey: route.sessionKey, contextKey });
|
enqueueSystemEvent(text, { sessionKey: route.sessionKey, contextKey });
|
||||||
|
|||||||
@ -336,23 +336,42 @@ export async function monitorWebInbox(options: {
|
|||||||
if (chatJid.endsWith("@status") || chatJid.endsWith("@broadcast")) continue;
|
if (chatJid.endsWith("@status") || chatJid.endsWith("@broadcast")) continue;
|
||||||
|
|
||||||
const emoji = entry.reaction?.text ?? "";
|
const emoji = entry.reaction?.text ?? "";
|
||||||
// Empty emoji = reaction removed; skip removals (matches Signal behavior)
|
const isRemoval = !emoji;
|
||||||
if (!emoji) continue;
|
|
||||||
|
|
||||||
const group = isJidGroup(chatJid) === true;
|
const group = isJidGroup(chatJid) === true;
|
||||||
// In DMs, reactionKey.fromMe means we reacted (remoteJid is the partner, not us)
|
// Determine who sent the reaction:
|
||||||
const senderJid = reactionKey?.fromMe
|
// - fromMe=true → we reacted (use selfJid)
|
||||||
? (selfJid ?? undefined)
|
// - Otherwise: use reaction.key metadata, fallback to chatJid for DMs
|
||||||
: (reactionKey?.participant ?? reactionKey?.remoteJid ?? undefined);
|
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;
|
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({
|
options.onReaction({
|
||||||
messageId,
|
messageId,
|
||||||
emoji,
|
emoji,
|
||||||
|
isRemoval,
|
||||||
chatJid,
|
chatJid,
|
||||||
chatType: group ? "group" : "direct",
|
chatType,
|
||||||
accountId: options.accountId,
|
accountId: options.accountId,
|
||||||
senderJid: senderJid ?? undefined,
|
senderJid: senderJid ?? undefined,
|
||||||
senderE164: senderE164 ?? undefined,
|
senderE164: senderE164 ?? undefined,
|
||||||
|
|||||||
@ -44,8 +44,10 @@ export type WebInboundMessage = {
|
|||||||
export type WebInboundReaction = {
|
export type WebInboundReaction = {
|
||||||
/** Message ID being reacted to. */
|
/** Message ID being reacted to. */
|
||||||
messageId: string;
|
messageId: string;
|
||||||
/** Emoji text (empty string = reaction removed). */
|
/** Emoji text (empty string when isRemoval=true). */
|
||||||
emoji: string;
|
emoji: string;
|
||||||
|
/** True if this is a reaction removal (emoji will be empty). */
|
||||||
|
isRemoval?: boolean;
|
||||||
/** JID of the chat where the reaction occurred. */
|
/** JID of the chat where the reaction occurred. */
|
||||||
chatJid: string;
|
chatJid: string;
|
||||||
chatType: "direct" | "group";
|
chatType: "direct" | "group";
|
||||||
|
|||||||
@ -142,7 +142,7 @@ describe("web monitor inbox – inbound reactions", () => {
|
|||||||
await listener.close();
|
await listener.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips reaction removals (empty emoji)", async () => {
|
it("handles reaction removals (empty emoji)", async () => {
|
||||||
const onReaction = vi.fn();
|
const onReaction = vi.fn();
|
||||||
|
|
||||||
const listener = await monitorWebInbox({
|
const listener = await monitorWebInbox({
|
||||||
@ -169,7 +169,14 @@ describe("web monitor inbox – inbound reactions", () => {
|
|||||||
]);
|
]);
|
||||||
await new Promise((resolve) => setImmediate(resolve));
|
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();
|
await listener.close();
|
||||||
});
|
});
|
||||||
@ -416,12 +423,13 @@ describe("web monitor inbox – inbound reactions", () => {
|
|||||||
]);
|
]);
|
||||||
await new Promise((resolve) => setImmediate(resolve));
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
|
||||||
|
// In DMs without reaction.key, we use chatJid as the sender
|
||||||
expect(onReaction).toHaveBeenCalledWith(
|
expect(onReaction).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
messageId: "msg-123",
|
messageId: "msg-123",
|
||||||
emoji: "🔥",
|
emoji: "🔥",
|
||||||
senderJid: undefined,
|
senderJid: "999@s.whatsapp.net",
|
||||||
senderE164: undefined,
|
chatType: "direct",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user