fix: tighten WhatsApp ack reactions and migrate config (#629) (thanks @pasogott)
This commit is contained in:
@@ -26,6 +26,7 @@ export type ResolvedWhatsAppAccount = {
|
||||
textChunkLimit?: number;
|
||||
mediaMaxMb?: number;
|
||||
blockStreaming?: boolean;
|
||||
ackReaction?: WhatsAppAccountConfig["ackReaction"];
|
||||
groups?: WhatsAppAccountConfig["groups"];
|
||||
};
|
||||
|
||||
@@ -129,6 +130,7 @@ export function resolveWhatsAppAccount(params: {
|
||||
mediaMaxMb: accountCfg?.mediaMaxMb ?? params.cfg.whatsapp?.mediaMaxMb,
|
||||
blockStreaming:
|
||||
accountCfg?.blockStreaming ?? params.cfg.whatsapp?.blockStreaming,
|
||||
ackReaction: accountCfg?.ackReaction ?? params.cfg.whatsapp?.ackReaction,
|
||||
groups: accountCfg?.groups ?? params.cfg.whatsapp?.groups,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,6 +76,17 @@ describe("WhatsApp ack reaction logic", () => {
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("should not react when message id is missing", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
whatsapp: { ackReaction: { emoji: "👀", direct: true } },
|
||||
};
|
||||
expect(
|
||||
shouldSendReaction(cfg, {
|
||||
chatType: "direct",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("group chat - always mode", () => {
|
||||
@@ -257,4 +268,18 @@ describe("WhatsApp ack reaction logic", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("legacy config is ignored", () => {
|
||||
it("does not use messages.ackReaction for WhatsApp", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
messages: { ackReaction: "👀", ackReactionScope: "all" },
|
||||
};
|
||||
expect(
|
||||
shouldSendReaction(cfg, {
|
||||
id: "m1",
|
||||
chatType: "direct",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -826,6 +826,7 @@ export async function monitorWebProvider(
|
||||
...baseCfg,
|
||||
whatsapp: {
|
||||
...baseCfg.whatsapp,
|
||||
ackReaction: account.ackReaction,
|
||||
messagePrefix: account.messagePrefix,
|
||||
allowFrom: account.allowFrom,
|
||||
groupAllowFrom: account.groupAllowFrom,
|
||||
@@ -1149,101 +1150,6 @@ export async function monitorWebProvider(
|
||||
status.lastMessageAt = Date.now();
|
||||
status.lastEventAt = status.lastMessageAt;
|
||||
emitStatus();
|
||||
|
||||
// Send ack reaction immediately upon message receipt
|
||||
if (msg.id) {
|
||||
const ackConfig = cfg.whatsapp?.ackReaction;
|
||||
// Backward compatibility: support old messages.ackReaction format (legacy, undocumented)
|
||||
const messages = cfg.messages as
|
||||
| undefined
|
||||
| (typeof cfg.messages & {
|
||||
ackReaction?: string;
|
||||
ackReactionScope?: string;
|
||||
});
|
||||
const legacyEmoji = messages?.ackReaction;
|
||||
const legacyScope = messages?.ackReactionScope;
|
||||
let emoji = (ackConfig?.emoji ?? "").trim();
|
||||
let directEnabled = ackConfig?.direct ?? true;
|
||||
let groupMode = ackConfig?.group ?? "mentions";
|
||||
|
||||
// Fallback to legacy config if new config is not set
|
||||
if (!emoji && typeof legacyEmoji === "string") {
|
||||
emoji = legacyEmoji.trim();
|
||||
if (legacyScope === "all") {
|
||||
directEnabled = true;
|
||||
groupMode = "always";
|
||||
} else if (legacyScope === "direct") {
|
||||
directEnabled = true;
|
||||
groupMode = "never";
|
||||
} else if (legacyScope === "group-all") {
|
||||
directEnabled = false;
|
||||
groupMode = "always";
|
||||
} else if (legacyScope === "group-mentions") {
|
||||
directEnabled = false;
|
||||
groupMode = "mentions";
|
||||
}
|
||||
}
|
||||
|
||||
const conversationIdForCheck = msg.conversationId ?? msg.from;
|
||||
|
||||
const shouldSendReaction = () => {
|
||||
if (!emoji) return false;
|
||||
|
||||
// Direct chat logic
|
||||
if (msg.chatType === "direct") {
|
||||
return directEnabled;
|
||||
}
|
||||
|
||||
// Group chat logic
|
||||
if (msg.chatType === "group") {
|
||||
if (groupMode === "never") return false;
|
||||
if (groupMode === "always") {
|
||||
// Always react to group messages
|
||||
return true;
|
||||
}
|
||||
if (groupMode === "mentions") {
|
||||
// Check if group has requireMention setting
|
||||
const activation = resolveGroupActivationFor({
|
||||
agentId: route.agentId,
|
||||
sessionKey: route.sessionKey,
|
||||
conversationId: conversationIdForCheck,
|
||||
});
|
||||
// If group activation is "always" (requireMention=false), react to all
|
||||
if (activation === "always") return true;
|
||||
// Otherwise, only react if bot was mentioned
|
||||
return msg.wasMentioned === true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (shouldSendReaction()) {
|
||||
replyLogger.info(
|
||||
{ chatId: msg.chatId, messageId: msg.id, emoji },
|
||||
"sending ack reaction",
|
||||
);
|
||||
sendReactionWhatsApp(msg.chatId, msg.id, emoji, {
|
||||
verbose,
|
||||
fromMe: false,
|
||||
participant: msg.senderJid,
|
||||
accountId: route.accountId,
|
||||
}).catch((err) => {
|
||||
replyLogger.warn(
|
||||
{
|
||||
error: formatError(err),
|
||||
chatId: msg.chatId,
|
||||
messageId: msg.id,
|
||||
},
|
||||
"failed to send ack reaction",
|
||||
);
|
||||
logVerbose(
|
||||
`WhatsApp ack reaction failed for chat ${msg.chatId}: ${formatError(err)}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const conversationId = msg.conversationId ?? msg.from;
|
||||
let combinedBody = buildLine(msg, route.agentId);
|
||||
let shouldClearGroupHistory = false;
|
||||
@@ -1294,6 +1200,64 @@ export async function monitorWebProvider(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send ack reaction immediately upon message receipt (post-gating)
|
||||
if (msg.id) {
|
||||
const ackConfig = cfg.whatsapp?.ackReaction;
|
||||
const emoji = (ackConfig?.emoji ?? "").trim();
|
||||
const directEnabled = ackConfig?.direct ?? true;
|
||||
const groupMode = ackConfig?.group ?? "mentions";
|
||||
const conversationIdForCheck = msg.conversationId ?? msg.from;
|
||||
|
||||
const shouldSendReaction = () => {
|
||||
if (!emoji) return false;
|
||||
|
||||
if (msg.chatType === "direct") {
|
||||
return directEnabled;
|
||||
}
|
||||
|
||||
if (msg.chatType === "group") {
|
||||
if (groupMode === "never") return false;
|
||||
if (groupMode === "always") return true;
|
||||
if (groupMode === "mentions") {
|
||||
const activation = resolveGroupActivationFor({
|
||||
agentId: route.agentId,
|
||||
sessionKey: route.sessionKey,
|
||||
conversationId: conversationIdForCheck,
|
||||
});
|
||||
if (activation === "always") return true;
|
||||
return msg.wasMentioned === true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (shouldSendReaction()) {
|
||||
replyLogger.info(
|
||||
{ chatId: msg.chatId, messageId: msg.id, emoji },
|
||||
"sending ack reaction",
|
||||
);
|
||||
sendReactionWhatsApp(msg.chatId, msg.id, emoji, {
|
||||
verbose,
|
||||
fromMe: false,
|
||||
participant: msg.senderJid,
|
||||
accountId: route.accountId,
|
||||
}).catch((err) => {
|
||||
replyLogger.warn(
|
||||
{
|
||||
error: formatError(err),
|
||||
chatId: msg.chatId,
|
||||
messageId: msg.id,
|
||||
},
|
||||
"failed to send ack reaction",
|
||||
);
|
||||
logVerbose(
|
||||
`WhatsApp ack reaction failed for chat ${msg.chatId}: ${formatError(err)}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const correlationId = msg.id ?? newConnectionId();
|
||||
replyLogger.info(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user