mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-26 16:06:16 +00:00
WhatsApp: move action runtime into extension
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
export * from "./src/active-listener.js";
|
||||
export * from "./src/action-runtime.js";
|
||||
export * from "./src/agent-tools-login.js";
|
||||
export * from "./src/auth-store.js";
|
||||
export * from "./src/auto-reply.js";
|
||||
|
||||
27
extensions/whatsapp/src/action-runtime-target-auth.ts
Normal file
27
extensions/whatsapp/src/action-runtime-target-auth.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ToolAuthorizationError } from "../../../src/agents/tools/common.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { resolveWhatsAppOutboundTarget } from "../../../src/whatsapp/resolve-outbound-target.js";
|
||||
import { resolveWhatsAppAccount } from "./accounts.js";
|
||||
|
||||
export function resolveAuthorizedWhatsAppOutboundTarget(params: {
|
||||
cfg: OpenClawConfig;
|
||||
chatJid: string;
|
||||
accountId?: string;
|
||||
actionLabel: string;
|
||||
}): { to: string; accountId: string } {
|
||||
const account = resolveWhatsAppAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
const resolution = resolveWhatsAppOutboundTarget({
|
||||
to: params.chatJid,
|
||||
allowFrom: account.allowFrom ?? [],
|
||||
mode: "implicit",
|
||||
});
|
||||
if (!resolution.ok) {
|
||||
throw new ToolAuthorizationError(
|
||||
`WhatsApp ${params.actionLabel} blocked: chatJid "${params.chatJid}" is not in the configured allowFrom list for account "${account.accountId}".`,
|
||||
);
|
||||
}
|
||||
return { to: resolution.to, accountId: account.accountId };
|
||||
}
|
||||
176
extensions/whatsapp/src/action-runtime.test.ts
Normal file
176
extensions/whatsapp/src/action-runtime.test.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { handleWhatsAppAction, whatsAppActionRuntime } from "./action-runtime.js";
|
||||
|
||||
const originalWhatsAppActionRuntime = { ...whatsAppActionRuntime };
|
||||
const sendReactionWhatsApp = vi.fn(async () => undefined);
|
||||
|
||||
const enabledConfig = {
|
||||
channels: { whatsapp: { actions: { reactions: true } } },
|
||||
} as OpenClawConfig;
|
||||
|
||||
describe("handleWhatsAppAction", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
Object.assign(whatsAppActionRuntime, originalWhatsAppActionRuntime, {
|
||||
sendReactionWhatsApp,
|
||||
});
|
||||
});
|
||||
|
||||
it("adds reactions", async () => {
|
||||
await handleWhatsAppAction(
|
||||
{
|
||||
action: "react",
|
||||
chatJid: "123@s.whatsapp.net",
|
||||
messageId: "msg1",
|
||||
emoji: "✅",
|
||||
},
|
||||
enabledConfig,
|
||||
);
|
||||
expect(sendReactionWhatsApp).toHaveBeenLastCalledWith("+123", "msg1", "✅", {
|
||||
verbose: false,
|
||||
fromMe: undefined,
|
||||
participant: undefined,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it("removes reactions on empty emoji", async () => {
|
||||
await handleWhatsAppAction(
|
||||
{
|
||||
action: "react",
|
||||
chatJid: "123@s.whatsapp.net",
|
||||
messageId: "msg1",
|
||||
emoji: "",
|
||||
},
|
||||
enabledConfig,
|
||||
);
|
||||
expect(sendReactionWhatsApp).toHaveBeenLastCalledWith("+123", "msg1", "", {
|
||||
verbose: false,
|
||||
fromMe: undefined,
|
||||
participant: undefined,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it("removes reactions when remove flag set", async () => {
|
||||
await handleWhatsAppAction(
|
||||
{
|
||||
action: "react",
|
||||
chatJid: "123@s.whatsapp.net",
|
||||
messageId: "msg1",
|
||||
emoji: "✅",
|
||||
remove: true,
|
||||
},
|
||||
enabledConfig,
|
||||
);
|
||||
expect(sendReactionWhatsApp).toHaveBeenLastCalledWith("+123", "msg1", "", {
|
||||
verbose: false,
|
||||
fromMe: undefined,
|
||||
participant: undefined,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it("passes account scope and sender flags", async () => {
|
||||
await handleWhatsAppAction(
|
||||
{
|
||||
action: "react",
|
||||
chatJid: "123@s.whatsapp.net",
|
||||
messageId: "msg1",
|
||||
emoji: "🎉",
|
||||
accountId: "work",
|
||||
fromMe: true,
|
||||
participant: "999@s.whatsapp.net",
|
||||
},
|
||||
enabledConfig,
|
||||
);
|
||||
expect(sendReactionWhatsApp).toHaveBeenLastCalledWith("+123", "msg1", "🎉", {
|
||||
verbose: false,
|
||||
fromMe: true,
|
||||
participant: "999@s.whatsapp.net",
|
||||
accountId: "work",
|
||||
});
|
||||
});
|
||||
|
||||
it("respects reaction gating", async () => {
|
||||
const cfg = {
|
||||
channels: { whatsapp: { actions: { reactions: false } } },
|
||||
} as OpenClawConfig;
|
||||
await expect(
|
||||
handleWhatsAppAction(
|
||||
{
|
||||
action: "react",
|
||||
chatJid: "123@s.whatsapp.net",
|
||||
messageId: "msg1",
|
||||
emoji: "✅",
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
).rejects.toThrow(/WhatsApp reactions are disabled/);
|
||||
});
|
||||
|
||||
it("applies default account allowFrom when accountId is omitted", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
actions: { reactions: true },
|
||||
allowFrom: ["111@s.whatsapp.net"],
|
||||
accounts: {
|
||||
[DEFAULT_ACCOUNT_ID]: {
|
||||
allowFrom: ["222@s.whatsapp.net"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
await expect(
|
||||
handleWhatsAppAction(
|
||||
{
|
||||
action: "react",
|
||||
chatJid: "111@s.whatsapp.net",
|
||||
messageId: "msg1",
|
||||
emoji: "✅",
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
name: "ToolAuthorizationError",
|
||||
status: 403,
|
||||
});
|
||||
});
|
||||
|
||||
it("routes to resolved default account when no accountId is provided", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
actions: { reactions: true },
|
||||
accounts: {
|
||||
work: {
|
||||
allowFrom: ["123@s.whatsapp.net"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
await handleWhatsAppAction(
|
||||
{
|
||||
action: "react",
|
||||
chatJid: "123@s.whatsapp.net",
|
||||
messageId: "msg1",
|
||||
emoji: "✅",
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
|
||||
expect(sendReactionWhatsApp).toHaveBeenLastCalledWith("+123", "msg1", "✅", {
|
||||
verbose: false,
|
||||
fromMe: undefined,
|
||||
participant: undefined,
|
||||
accountId: "work",
|
||||
});
|
||||
});
|
||||
});
|
||||
60
extensions/whatsapp/src/action-runtime.ts
Normal file
60
extensions/whatsapp/src/action-runtime.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import {
|
||||
createActionGate,
|
||||
jsonResult,
|
||||
readReactionParams,
|
||||
readStringParam,
|
||||
} from "../../../src/agents/tools/common.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { resolveAuthorizedWhatsAppOutboundTarget } from "./action-runtime-target-auth.js";
|
||||
import { sendReactionWhatsApp } from "./send.js";
|
||||
|
||||
export const whatsAppActionRuntime = {
|
||||
resolveAuthorizedWhatsAppOutboundTarget,
|
||||
sendReactionWhatsApp,
|
||||
};
|
||||
|
||||
export async function handleWhatsAppAction(
|
||||
params: Record<string, unknown>,
|
||||
cfg: OpenClawConfig,
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
const isActionEnabled = createActionGate(cfg.channels?.whatsapp?.actions);
|
||||
|
||||
if (action === "react") {
|
||||
if (!isActionEnabled("reactions")) {
|
||||
throw new Error("WhatsApp reactions are disabled.");
|
||||
}
|
||||
const chatJid = readStringParam(params, "chatJid", { required: true });
|
||||
const messageId = readStringParam(params, "messageId", { required: true });
|
||||
const { emoji, remove, isEmpty } = readReactionParams(params, {
|
||||
removeErrorMessage: "Emoji is required to remove a WhatsApp reaction.",
|
||||
});
|
||||
const participant = readStringParam(params, "participant");
|
||||
const accountId = readStringParam(params, "accountId");
|
||||
const fromMeRaw = params.fromMe;
|
||||
const fromMe = typeof fromMeRaw === "boolean" ? fromMeRaw : undefined;
|
||||
|
||||
// Resolve account + allowFrom via shared account logic so auth and routing stay aligned.
|
||||
const resolved = whatsAppActionRuntime.resolveAuthorizedWhatsAppOutboundTarget({
|
||||
cfg,
|
||||
chatJid,
|
||||
accountId,
|
||||
actionLabel: "reaction",
|
||||
});
|
||||
|
||||
const resolvedEmoji = remove ? "" : emoji;
|
||||
await whatsAppActionRuntime.sendReactionWhatsApp(resolved.to, messageId, resolvedEmoji, {
|
||||
verbose: false,
|
||||
fromMe,
|
||||
participant: participant ?? undefined,
|
||||
accountId: resolved.accountId,
|
||||
});
|
||||
if (!remove && !isEmpty) {
|
||||
return jsonResult({ ok: true, added: emoji });
|
||||
}
|
||||
return jsonResult({ ok: true, removed: true });
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported WhatsApp action: ${action}`);
|
||||
}
|
||||
Reference in New Issue
Block a user