mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-05-01 06:36:23 +08:00
fix(whatsapp): send group reactions with target participant (#65512)
This commit is contained in:
@@ -136,6 +136,25 @@ describe("handleWhatsAppAction", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves LID participant ids when forwarding reactions", async () => {
|
||||
await handleWhatsAppAction(
|
||||
{
|
||||
action: "react",
|
||||
chatJid: "12345@g.us",
|
||||
messageId: "msg1",
|
||||
emoji: "🎉",
|
||||
participant: "123@lid",
|
||||
},
|
||||
enabledConfig,
|
||||
);
|
||||
expect(sendReactionWhatsApp).toHaveBeenLastCalledWith("12345@g.us", "msg1", "🎉", {
|
||||
verbose: false,
|
||||
fromMe: undefined,
|
||||
participant: "123@lid",
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it("respects reaction gating", async () => {
|
||||
const cfg = {
|
||||
channels: { whatsapp: { actions: { reactions: false } } },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readStringParam } from "openclaw/plugin-sdk/channel-actions";
|
||||
import { readStringOrNumberParam, readStringParam } from "openclaw/plugin-sdk/channel-actions";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
|
||||
export { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-actions";
|
||||
export { handleWhatsAppAction } from "./action-runtime.js";
|
||||
export { normalizeWhatsAppTarget } from "./normalize.js";
|
||||
export { readStringParam, type OpenClawConfig };
|
||||
export { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "./normalize.js";
|
||||
export { readStringOrNumberParam, readStringParam, type OpenClawConfig };
|
||||
|
||||
@@ -16,12 +16,26 @@ vi.mock("./channel-react-action.runtime.js", async () => {
|
||||
args: Record<string, unknown>;
|
||||
toolContext?: { currentMessageId?: string | number | null };
|
||||
}) => args.messageId ?? toolContext?.currentMessageId ?? null,
|
||||
readStringOrNumberParam: (params: Record<string, unknown>, key: string) => {
|
||||
const value = params[key];
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
isWhatsAppGroupJid: (value?: string | null) => (value ?? "").trim().endsWith("@g.us"),
|
||||
normalizeWhatsAppTarget: (value?: string | null) => {
|
||||
const raw = (value ?? "").trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const stripped = raw.replace(/^whatsapp:/, "");
|
||||
if (stripped.endsWith("@g.us")) {
|
||||
return stripped;
|
||||
}
|
||||
return stripped.startsWith("+") ? stripped : `+${stripped.replace(/^\+/, "")}`;
|
||||
},
|
||||
readStringParam: (
|
||||
@@ -138,11 +152,34 @@ describe("whatsapp react action messageId resolution", () => {
|
||||
});
|
||||
|
||||
it("uses context fallback when target matches current chat", async () => {
|
||||
await handleWhatsAppReactAction({
|
||||
action: "react",
|
||||
params: { emoji: "👍", to: "12345@g.us" },
|
||||
cfg: baseCfg,
|
||||
accountId: "default",
|
||||
requesterSenderId: "123@lid",
|
||||
toolContext: {
|
||||
currentChannelId: "whatsapp:12345@g.us",
|
||||
currentChannelProvider: "whatsapp",
|
||||
currentMessageId: "ctx-msg-42",
|
||||
},
|
||||
});
|
||||
expect(hoisted.handleWhatsAppAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messageId: "ctx-msg-42",
|
||||
participant: "123@lid",
|
||||
}),
|
||||
baseCfg,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps direct-chat reactions without an inferred participant", async () => {
|
||||
await handleWhatsAppReactAction({
|
||||
action: "react",
|
||||
params: { emoji: "👍", to: "+1555" },
|
||||
cfg: baseCfg,
|
||||
accountId: "default",
|
||||
requesterSenderId: "123@lid",
|
||||
toolContext: {
|
||||
currentChannelId: "whatsapp:+1555",
|
||||
currentChannelProvider: "whatsapp",
|
||||
@@ -150,7 +187,76 @@ describe("whatsapp react action messageId resolution", () => {
|
||||
},
|
||||
});
|
||||
expect(hoisted.handleWhatsAppAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ messageId: "ctx-msg-42" }),
|
||||
expect.objectContaining({
|
||||
messageId: "ctx-msg-42",
|
||||
participant: undefined,
|
||||
}),
|
||||
baseCfg,
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers explicit participant over inferred current-message participant", async () => {
|
||||
await handleWhatsAppReactAction({
|
||||
action: "react",
|
||||
params: {
|
||||
emoji: "👍",
|
||||
to: "12345@g.us",
|
||||
participant: "555@s.whatsapp.net",
|
||||
},
|
||||
cfg: baseCfg,
|
||||
accountId: "default",
|
||||
requesterSenderId: "123@lid",
|
||||
toolContext: {
|
||||
currentChannelId: "whatsapp:12345@g.us",
|
||||
currentChannelProvider: "whatsapp",
|
||||
currentMessageId: "ctx-msg-42",
|
||||
},
|
||||
});
|
||||
expect(hoisted.handleWhatsAppAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messageId: "ctx-msg-42",
|
||||
participant: "555@s.whatsapp.net",
|
||||
}),
|
||||
baseCfg,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not reuse the current-chat participant for cross-chat reactions", async () => {
|
||||
const err = await handleWhatsAppReactAction({
|
||||
action: "react",
|
||||
params: { emoji: "👍", to: "99999@g.us" },
|
||||
cfg: baseCfg,
|
||||
accountId: "default",
|
||||
requesterSenderId: "123@lid",
|
||||
toolContext: {
|
||||
currentChannelId: "whatsapp:12345@g.us",
|
||||
currentChannelProvider: "whatsapp",
|
||||
currentMessageId: "ctx-msg-42",
|
||||
},
|
||||
}).catch((e: unknown) => e);
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect((err as Error).name).toBe("ToolInputError");
|
||||
expect(hoisted.handleWhatsAppAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not infer participant when messageId is explicitly provided", async () => {
|
||||
await handleWhatsAppReactAction({
|
||||
action: "react",
|
||||
params: { emoji: "👍", to: "12345@g.us", messageId: "older-msg-7" },
|
||||
cfg: baseCfg,
|
||||
accountId: "default",
|
||||
requesterSenderId: "123@lid",
|
||||
toolContext: {
|
||||
currentChannelId: "whatsapp:12345@g.us",
|
||||
currentChannelProvider: "whatsapp",
|
||||
currentMessageId: "ctx-msg-42",
|
||||
},
|
||||
});
|
||||
expect(hoisted.handleWhatsAppAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messageId: "older-msg-7",
|
||||
participant: undefined,
|
||||
}),
|
||||
baseCfg,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
isWhatsAppGroupJid,
|
||||
resolveReactionMessageId,
|
||||
handleWhatsAppAction,
|
||||
normalizeWhatsAppTarget,
|
||||
readStringOrNumberParam,
|
||||
readStringParam,
|
||||
type OpenClawConfig,
|
||||
} from "./channel-react-action.runtime.js";
|
||||
@@ -13,6 +15,7 @@ export async function handleWhatsAppReactAction(params: {
|
||||
params: Record<string, unknown>;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
requesterSenderId?: string | null;
|
||||
toolContext?: {
|
||||
currentChannelId?: string | null;
|
||||
currentChannelProvider?: string | null;
|
||||
@@ -49,8 +52,20 @@ export async function handleWhatsAppReactAction(params: {
|
||||
readStringParam(params.params, "messageId", { required: true });
|
||||
}
|
||||
const messageId = String(messageIdRaw);
|
||||
const explicitMessageId = readStringOrNumberParam(params.params, "messageId");
|
||||
const emoji = readStringParam(params.params, "emoji", { allowEmpty: true });
|
||||
const remove = typeof params.params.remove === "boolean" ? params.params.remove : undefined;
|
||||
const explicitParticipant = readStringParam(params.params, "participant");
|
||||
const inferredParticipant =
|
||||
explicitParticipant ||
|
||||
explicitMessageId != null ||
|
||||
!isWhatsAppSource ||
|
||||
isCrossChat ||
|
||||
!isWhatsAppGroupJid(explicitTarget ?? params.toolContext?.currentChannelId ?? "")
|
||||
? undefined
|
||||
: typeof params.requesterSenderId === "string" && params.requesterSenderId.trim().length > 0
|
||||
? params.requesterSenderId.trim()
|
||||
: undefined;
|
||||
return await handleWhatsAppAction(
|
||||
{
|
||||
action: "react",
|
||||
@@ -60,7 +75,7 @@ export async function handleWhatsAppReactAction(params: {
|
||||
messageId,
|
||||
emoji,
|
||||
remove,
|
||||
participant: readStringParam(params.params, "participant"),
|
||||
participant: explicitParticipant ?? inferredParticipant,
|
||||
accountId: params.accountId ?? undefined,
|
||||
fromMe: typeof params.params.fromMe === "boolean" ? params.params.fromMe : undefined,
|
||||
},
|
||||
|
||||
@@ -136,7 +136,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> =
|
||||
describeWhatsAppMessageActions({ cfg, accountId }),
|
||||
supportsAction: ({ action }) => action === "react",
|
||||
resolveExecutionMode: ({ action }) => (action === "react" ? "gateway" : "local"),
|
||||
handleAction: async ({ action, params, cfg, accountId, toolContext }) =>
|
||||
handleAction: async ({ action, params, cfg, accountId, requesterSenderId, toolContext }) =>
|
||||
await (
|
||||
await loadWhatsAppChannelReactAction()
|
||||
).handleWhatsAppReactAction({
|
||||
@@ -144,6 +144,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> =
|
||||
params,
|
||||
cfg,
|
||||
accountId,
|
||||
requesterSenderId,
|
||||
toolContext,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -158,6 +158,42 @@ describe("createWebSendApi", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps direct-chat reactions without a participant key", async () => {
|
||||
await api.sendReaction("+1555", "msg-2", "👍", false);
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
"1555@s.whatsapp.net",
|
||||
expect.objectContaining({
|
||||
react: {
|
||||
text: "👍",
|
||||
key: expect.objectContaining({
|
||||
remoteJid: "1555@s.whatsapp.net",
|
||||
id: "msg-2",
|
||||
fromMe: false,
|
||||
participant: undefined,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves LID participants in reaction keys", async () => {
|
||||
await api.sendReaction("12345@g.us", "msg-2", "👍", false, "123@lid");
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
"12345@g.us",
|
||||
expect.objectContaining({
|
||||
react: {
|
||||
text: "👍",
|
||||
key: expect.objectContaining({
|
||||
remoteJid: "12345@g.us",
|
||||
id: "msg-2",
|
||||
fromMe: false,
|
||||
participant: "123@lid",
|
||||
}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("sends composing presence updates to the recipient JID", async () => {
|
||||
await api.sendComposingTo("+1555");
|
||||
expect(sendPresenceUpdate).toHaveBeenCalledWith("composing", "1555@s.whatsapp.net");
|
||||
|
||||
Reference in New Issue
Block a user