mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:50:42 +00:00
feat(whatsapp): support newsletter targets in message tool
This commit is contained in:
@@ -8,6 +8,11 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Plugins/CLI: include package dependency install state in `openclaw plugins list --json` so scripts can spot missing plugin dependencies without runtime-loading plugins.
|
||||
- Plugins/update: on the beta OpenClaw update channel, default-line npm and ClawHub plugin updates try `@beta` first and fall back to default/latest when no plugin beta release exists.
|
||||
- Channels: add Yuanbao channel docs entrance so the Tencent Yuanbao bot appears in the channel listing and sidebar navigation. (#73443) Thanks @loongfay.
|
||||
- Active Memory: add optional per-conversation `allowedChatIds` and `deniedChatIds` filters so operators can enable recall only for selected direct, group, or channel conversations while keeping broad sessions skipped. (#67977) Thanks @quengh.
|
||||
- Active Memory: return bounded partial recall summaries when the hidden memory sub-agent times out, including the default temporary-transcript path, so useful recovered context is not discarded. (#73219) Thanks @joeykrug.
|
||||
- Docker setup: add `OPENCLAW_SKIP_ONBOARDING` so automated Docker installs can skip the interactive onboarding step while still applying gateway defaults. (#55518) Thanks @jinjimz.
|
||||
- Channels/WhatsApp: support explicit WhatsApp Channel/Newsletter `@newsletter` outbound message targets with channel session metadata instead of DM routing. Fixes #13417; carries forward the narrow target idea from #13424 while addressing routing/session review concerns; refs #62697. Thanks @vincentkoc and @agentz-manfred.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
|
||||
- The reconnect watchdog follows WhatsApp Web transport activity, not only inbound app-message volume: quiet linked-device sessions stay up while transport frames continue, but a transport stall forces reconnect well before the later remote disconnect path.
|
||||
- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs to the agent main session).
|
||||
- Group sessions are isolated (`agent:<agentId>:whatsapp:group:<jid>`).
|
||||
- WhatsApp Channels/Newsletters can be explicit outbound targets with their native `@newsletter` JID. Outbound newsletter sends use channel session metadata (`agent:<agentId>:whatsapp:channel:<jid>`) rather than DM session semantics.
|
||||
- WhatsApp Web transport honors standard proxy environment variables on the gateway host (`HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY` / lowercase variants). Prefer host-level proxy config over channel-specific WhatsApp proxy settings.
|
||||
- When `messages.removeAckAfterReply` is enabled, OpenClaw clears the WhatsApp ack reaction after a visible reply is delivered.
|
||||
|
||||
@@ -214,6 +215,8 @@ content and identifiers.
|
||||
|
||||
`allowFrom` accepts E.164-style numbers (normalized internally).
|
||||
|
||||
`allowFrom` is a DM sender access-control list. It does not gate explicit outbound sends to WhatsApp group JIDs or `@newsletter` channel JIDs.
|
||||
|
||||
Multi-account override: `channels.whatsapp.accounts.<id>.dmPolicy` (and `allowFrom`) take precedence over channel-level defaults for that account.
|
||||
|
||||
Runtime behavior details:
|
||||
|
||||
@@ -32,7 +32,7 @@ openclaw message send --channel slack --target user:U012ABCDEF --message "hello"
|
||||
|
||||
## ID formats (by channel)
|
||||
|
||||
- WhatsApp: `+15551234567` (DM), `1234567890-1234567890@g.us` (group)
|
||||
- WhatsApp: `+15551234567` (DM), `1234567890-1234567890@g.us` (group), `120363123456789@newsletter` (Channel/Newsletter outbound target)
|
||||
- Telegram: `@username` or numeric chat id; groups are numeric ids
|
||||
- Slack: `user:U…` and `channel:C…`
|
||||
- Discord: `user:<id>` and `channel:<id>`
|
||||
|
||||
@@ -26,7 +26,7 @@ Channel selection:
|
||||
|
||||
Target formats (`--target`):
|
||||
|
||||
- WhatsApp: E.164 or group JID
|
||||
- WhatsApp: E.164, group JID, or WhatsApp Channel/Newsletter JID (`...@newsletter`)
|
||||
- Telegram: chat id or `@username`
|
||||
- Discord: `channel:<id>` or `user:<id>` (or `<@id>` mention; raw numeric ids are treated as channels)
|
||||
- Google Chat: `spaces/<spaceId>` or `users/<userId>`
|
||||
@@ -76,7 +76,7 @@ Name lookup:
|
||||
- Telegram only: `--thread-id` (forum topic id)
|
||||
- Slack only: `--thread-id` (thread timestamp; `--reply-to` uses the same field)
|
||||
- Telegram + Discord: `--silent`
|
||||
- WhatsApp only: `--gif-playback`
|
||||
- WhatsApp only: `--gif-playback`; WhatsApp Channels/Newsletters are addressed with their native `@newsletter` JID.
|
||||
|
||||
- `poll`
|
||||
- Channels: WhatsApp/Telegram/Discord/Matrix/Microsoft Teams
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { checkWhatsAppHeartbeatReady } from "./heartbeat.js";
|
||||
import {
|
||||
isWhatsAppGroupJid,
|
||||
isWhatsAppNewsletterJid,
|
||||
looksLikeWhatsAppTargetId,
|
||||
normalizeWhatsAppMessagingTarget,
|
||||
normalizeWhatsAppTarget,
|
||||
@@ -56,7 +57,11 @@ function parseWhatsAppExplicitTarget(raw: string) {
|
||||
}
|
||||
return {
|
||||
to: normalized,
|
||||
chatType: isWhatsAppGroupJid(normalized) ? ("group" as const) : ("direct" as const),
|
||||
chatType: isWhatsAppGroupJid(normalized)
|
||||
? ("group" as const)
|
||||
: isWhatsAppNewsletterJid(normalized)
|
||||
? ("channel" as const)
|
||||
: ("direct" as const),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,7 +122,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> =
|
||||
inferTargetChatType: ({ to }) => parseWhatsAppExplicitTarget(to)?.chatType,
|
||||
targetResolver: {
|
||||
looksLikeId: looksLikeWhatsAppTargetId,
|
||||
hint: "<E.164|group JID>",
|
||||
hint: "<E.164|group JID|newsletter JID>",
|
||||
},
|
||||
},
|
||||
directory: {
|
||||
|
||||
@@ -260,6 +260,18 @@ describe("createWebSendApi", () => {
|
||||
expect(sendPresenceUpdate).toHaveBeenCalledWith("composing", "1555@s.whatsapp.net");
|
||||
});
|
||||
|
||||
it("does not send composing presence to newsletter JIDs", async () => {
|
||||
await api.sendComposingTo("120363401234567890@newsletter");
|
||||
expect(sendPresenceUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves newsletter JIDs for outbound sends", async () => {
|
||||
await api.sendMessage("120363401234567890@newsletter", "hello");
|
||||
expect(sendMessage).toHaveBeenCalledWith("120363401234567890@newsletter", {
|
||||
text: "hello",
|
||||
});
|
||||
});
|
||||
|
||||
it("sends media as document when mediaType is undefined", async () => {
|
||||
const mediaBuffer = Buffer.from("test");
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
WAPresence,
|
||||
} from "@whiskeysockets/baileys";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
|
||||
import { isWhatsAppNewsletterJid } from "../normalize.js";
|
||||
import { buildQuotedMessageOptions } from "../quoted-message.js";
|
||||
import { toWhatsappJid } from "../text-runtime.js";
|
||||
import {
|
||||
@@ -135,6 +136,9 @@ export function createWebSendApi(params: {
|
||||
},
|
||||
sendComposingTo: async (to: string): Promise<void> => {
|
||||
const jid = toWhatsappJid(to);
|
||||
if (isWhatsAppNewsletterJid(jid)) {
|
||||
return;
|
||||
}
|
||||
await params.sock.sendPresenceUpdate("composing", jid);
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -5,6 +5,7 @@ const WHATSAPP_USER_JID_RE = /^(\d+)(?::\d+)?@s\.whatsapp\.net$/i;
|
||||
const WHATSAPP_LEGACY_USER_JID_RE = /^(\d+)@c\.us$/i;
|
||||
const WHATSAPP_LID_RE = /^(\d+)@lid$/i;
|
||||
const NON_WHATSAPP_PROVIDER_PREFIX_RE = /^[a-z][a-z0-9-]*:/i;
|
||||
const WHATSAPP_NEWSLETTER_JID_RE = /^([0-9]+)@newsletter$/i;
|
||||
|
||||
function stripWhatsAppTargetPrefixes(value: string): string {
|
||||
let candidate = value.trim();
|
||||
@@ -30,6 +31,11 @@ export function isWhatsAppGroupJid(value: string): boolean {
|
||||
return /^[0-9]+(-[0-9]+)*$/.test(localPart);
|
||||
}
|
||||
|
||||
export function isWhatsAppNewsletterJid(value: string): boolean {
|
||||
const candidate = stripWhatsAppTargetPrefixes(value);
|
||||
return WHATSAPP_NEWSLETTER_JID_RE.test(candidate);
|
||||
}
|
||||
|
||||
export function isWhatsAppUserTarget(value: string): boolean {
|
||||
const candidate = stripWhatsAppTargetPrefixes(value);
|
||||
return (
|
||||
@@ -64,6 +70,10 @@ export function normalizeWhatsAppTarget(value: string): string | null {
|
||||
const localPart = candidate.slice(0, candidate.length - "@g.us".length);
|
||||
return `${localPart}@g.us`;
|
||||
}
|
||||
if (isWhatsAppNewsletterJid(candidate)) {
|
||||
const match = candidate.match(WHATSAPP_NEWSLETTER_JID_RE);
|
||||
return match ? `${match[1]}@newsletter` : null;
|
||||
}
|
||||
if (isWhatsAppUserTarget(candidate)) {
|
||||
const phone = extractUserJidPhone(candidate);
|
||||
if (!phone) {
|
||||
@@ -106,6 +116,7 @@ export function looksLikeWhatsAppTargetId(raw: string): boolean {
|
||||
return (
|
||||
/^whatsapp:/i.test(trimmed) ||
|
||||
isWhatsAppGroupJid(trimmed) ||
|
||||
isWhatsAppNewsletterJid(trimmed) ||
|
||||
isWhatsAppUserTarget(trimmed) ||
|
||||
normalizeWhatsAppTarget(trimmed) !== null
|
||||
);
|
||||
|
||||
@@ -2,5 +2,7 @@ export {
|
||||
looksLikeWhatsAppTargetId,
|
||||
normalizeWhatsAppMessagingTarget,
|
||||
isWhatsAppGroupJid,
|
||||
isWhatsAppNewsletterJid,
|
||||
isWhatsAppUserTarget,
|
||||
normalizeWhatsAppTarget,
|
||||
} from "./normalize-target.js";
|
||||
|
||||
@@ -130,6 +130,29 @@ describe("resolveWhatsAppOutboundTarget", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("newsletter JID handling", () => {
|
||||
it("returns success for valid newsletter JID without applying DM allowFrom", () => {
|
||||
vi.mocked(normalize.normalizeWhatsAppTarget).mockReturnValueOnce(
|
||||
"120363123456789@newsletter",
|
||||
);
|
||||
vi.mocked(normalize.isWhatsAppGroupJid).mockReturnValueOnce(false);
|
||||
vi.mocked(normalize.isWhatsAppNewsletterJid).mockReturnValueOnce(true);
|
||||
|
||||
expectResolutionOk(
|
||||
{
|
||||
to: "120363123456789@newsletter",
|
||||
allowFrom: [SECONDARY_TARGET],
|
||||
mode: "implicit",
|
||||
},
|
||||
"120363123456789@newsletter",
|
||||
);
|
||||
expect(vi.mocked(normalize.normalizeWhatsAppTarget)).toHaveBeenCalledOnce();
|
||||
expect(vi.mocked(normalize.normalizeWhatsAppTarget)).toHaveBeenCalledWith(
|
||||
"120363123456789@newsletter",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("implicit/heartbeat mode with allowList", () => {
|
||||
it("allows message when wildcard is present", () => {
|
||||
mockNormalizedDirectMessage(PRIMARY_TARGET, PRIMARY_TARGET);
|
||||
@@ -154,14 +177,14 @@ describe("resolveWhatsAppOutboundTarget", () => {
|
||||
allowFrom: [SECONDARY_TARGET],
|
||||
mode: "implicit",
|
||||
},
|
||||
`Target "${SECONDARY_TARGET}" is not listed in the configured WhatsApp allowFrom policy.`,
|
||||
`Target "${PRIMARY_TARGET}" is not listed in the configured WhatsApp allowFrom policy.`,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the normalized target in the allowFrom error message", () => {
|
||||
vi.mocked(normalize.normalizeWhatsAppTarget)
|
||||
.mockReturnValueOnce(SECONDARY_TARGET)
|
||||
.mockReturnValueOnce(PRIMARY_TARGET);
|
||||
.mockReturnValueOnce(PRIMARY_TARGET)
|
||||
.mockReturnValueOnce(SECONDARY_TARGET);
|
||||
vi.mocked(normalize.isWhatsAppGroupJid).mockReturnValueOnce(false);
|
||||
|
||||
expectResolutionErrorMessage(
|
||||
@@ -189,8 +212,8 @@ describe("resolveWhatsAppOutboundTarget", () => {
|
||||
|
||||
it("filters out invalid normalized entries from allowList", () => {
|
||||
vi.mocked(normalize.normalizeWhatsAppTarget)
|
||||
.mockReturnValueOnce(null)
|
||||
.mockReturnValueOnce("+11234567890")
|
||||
.mockReturnValueOnce(null)
|
||||
.mockReturnValueOnce("+11234567890");
|
||||
vi.mocked(normalize.isWhatsAppGroupJid).mockReturnValueOnce(false);
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { missingTargetError } from "openclaw/plugin-sdk/channel-feedback";
|
||||
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "./normalize-target.js";
|
||||
import {
|
||||
isWhatsAppGroupJid,
|
||||
isWhatsAppNewsletterJid,
|
||||
normalizeWhatsAppTarget,
|
||||
} from "./normalize-target.js";
|
||||
|
||||
export type WhatsAppOutboundTargetResolution =
|
||||
| { ok: true; to: string }
|
||||
@@ -15,6 +19,24 @@ export function resolveWhatsAppOutboundTarget(params: {
|
||||
mode: string | null | undefined;
|
||||
}): WhatsAppOutboundTargetResolution {
|
||||
const trimmed = params.to?.trim() ?? "";
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("WhatsApp", "<E.164|group JID|newsletter JID>"),
|
||||
};
|
||||
}
|
||||
|
||||
const normalizedTo = normalizeWhatsAppTarget(trimmed);
|
||||
if (!normalizedTo) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("WhatsApp", "<E.164|group JID|newsletter JID>"),
|
||||
};
|
||||
}
|
||||
if (isWhatsAppGroupJid(normalizedTo) || isWhatsAppNewsletterJid(normalizedTo)) {
|
||||
return { ok: true, to: normalizedTo };
|
||||
}
|
||||
|
||||
const allowListRaw = (params.allowFrom ?? [])
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter(Boolean);
|
||||
@@ -23,32 +45,14 @@ export function resolveWhatsAppOutboundTarget(params: {
|
||||
.filter((entry) => entry !== "*")
|
||||
.map((entry) => normalizeWhatsAppTarget(entry))
|
||||
.filter((entry): entry is string => Boolean(entry));
|
||||
|
||||
if (trimmed) {
|
||||
const normalizedTo = normalizeWhatsAppTarget(trimmed);
|
||||
if (!normalizedTo) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("WhatsApp", "<E.164|group JID>"),
|
||||
};
|
||||
}
|
||||
if (isWhatsAppGroupJid(normalizedTo)) {
|
||||
return { ok: true, to: normalizedTo };
|
||||
}
|
||||
if (hasWildcard || allowList.length === 0) {
|
||||
return { ok: true, to: normalizedTo };
|
||||
}
|
||||
if (allowList.includes(normalizedTo)) {
|
||||
return { ok: true, to: normalizedTo };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: whatsappAllowFromPolicyError(normalizedTo),
|
||||
};
|
||||
if (hasWildcard || allowList.length === 0) {
|
||||
return { ok: true, to: normalizedTo };
|
||||
}
|
||||
if (allowList.includes(normalizedTo)) {
|
||||
return { ok: true, to: normalizedTo };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("WhatsApp", "<E.164|group JID>"),
|
||||
error: whatsappAllowFromPolicyError(normalizedTo),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
isWhatsAppGroupJid,
|
||||
isWhatsAppNewsletterJid,
|
||||
looksLikeWhatsAppTargetId,
|
||||
isWhatsAppUserTarget,
|
||||
normalizeWhatsAppMessagingTarget,
|
||||
@@ -16,6 +17,15 @@ describe("normalizeWhatsAppTarget", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves newsletter JIDs", () => {
|
||||
expect(normalizeWhatsAppTarget("120363401234567890@newsletter")).toBe(
|
||||
"120363401234567890@newsletter",
|
||||
);
|
||||
expect(normalizeWhatsAppTarget("WhatsApp:120363401234567890@NEWSLETTER")).toBe(
|
||||
"120363401234567890@newsletter",
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes direct JIDs to E.164", () => {
|
||||
expect(normalizeWhatsAppTarget("1555123@s.whatsapp.net")).toBe("+1555123");
|
||||
});
|
||||
@@ -40,6 +50,7 @@ describe("normalizeWhatsAppTarget", () => {
|
||||
expect(normalizeWhatsAppTarget("group:123456789-987654321@g.us")).toBeNull();
|
||||
expect(normalizeWhatsAppTarget(" WhatsApp:Group:123456789-987654321@G.US ")).toBeNull();
|
||||
expect(normalizeWhatsAppTarget("abc@s.whatsapp.net")).toBeNull();
|
||||
expect(normalizeWhatsAppTarget("abc@newsletter")).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects non-WhatsApp provider-prefixed phone-like targets", () => {
|
||||
@@ -68,6 +79,17 @@ describe("isWhatsAppUserTarget", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWhatsAppNewsletterJid", () => {
|
||||
it("detects newsletter JIDs with or without prefixes", () => {
|
||||
expect(isWhatsAppNewsletterJid("120363401234567890@newsletter")).toBe(true);
|
||||
expect(isWhatsAppNewsletterJid("whatsapp:120363401234567890@newsletter")).toBe(true);
|
||||
expect(isWhatsAppNewsletterJid("120363401234567890@NEWSLETTER")).toBe(true);
|
||||
expect(isWhatsAppNewsletterJid("abc@newsletter")).toBe(false);
|
||||
expect(isWhatsAppNewsletterJid("120363401234567890@g.us")).toBe(false);
|
||||
expect(isWhatsAppNewsletterJid("+1555123")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWhatsAppGroupJid", () => {
|
||||
it("detects group JIDs with or without prefixes", () => {
|
||||
expect(isWhatsAppGroupJid("120363401234567890@g.us")).toBe(true);
|
||||
@@ -91,6 +113,7 @@ describe("looksLikeWhatsAppTargetId", () => {
|
||||
it("detects common WhatsApp target forms", () => {
|
||||
expect(looksLikeWhatsAppTargetId("whatsapp:+15555550123")).toBe(true);
|
||||
expect(looksLikeWhatsAppTargetId("15555550123@c.us")).toBe(true);
|
||||
expect(looksLikeWhatsAppTargetId("120363401234567890@newsletter")).toBe(true);
|
||||
expect(looksLikeWhatsAppTargetId("+15555550123")).toBe(true);
|
||||
expect(looksLikeWhatsAppTargetId("")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -144,6 +144,25 @@ describe("web outbound", () => {
|
||||
expect(sendMessage).toHaveBeenCalledWith("+1555", "hi", undefined, undefined);
|
||||
});
|
||||
|
||||
it("sends newsletter messages via the active listener without composing presence", async () => {
|
||||
const result = await sendMessageWhatsApp("120363401234567890@newsletter", "hi", {
|
||||
verbose: false,
|
||||
cfg: WHATSAPP_TEST_CFG,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
messageId: "msg123",
|
||||
toJid: "120363401234567890@newsletter",
|
||||
});
|
||||
expect(sendComposingTo).not.toHaveBeenCalled();
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
"120363401234567890@newsletter",
|
||||
"hi",
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses configured defaultAccount when outbound accountId is omitted", async () => {
|
||||
hoisted.controllerListeners.clear();
|
||||
hoisted.controllerListeners.set("work", {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "./accounts.js";
|
||||
import { getRegisteredWhatsAppConnectionController } from "./connection-controller-registry.js";
|
||||
import type { ActiveWebListener, ActiveWebSendOptions } from "./inbound/types.js";
|
||||
import { isWhatsAppNewsletterJid } from "./normalize.js";
|
||||
import {
|
||||
normalizeWhatsAppPayloadText,
|
||||
prepareWhatsAppOutboundMedia,
|
||||
@@ -142,7 +143,9 @@ export async function sendMessageWhatsApp(
|
||||
}
|
||||
outboundLog.info(`Sending message -> ${redactedJid}${primaryMediaUrl ? " (media)" : ""}`);
|
||||
logger.info({ jid: redactedJid, hasMedia: Boolean(primaryMediaUrl) }, "sending message");
|
||||
await active.sendComposingTo(to);
|
||||
if (!isWhatsAppNewsletterJid(jid)) {
|
||||
await active.sendComposingTo(to);
|
||||
}
|
||||
const hasExplicitAccountId = Boolean(options.accountId?.trim());
|
||||
const accountId = hasExplicitAccountId ? resolvedAccountId : undefined;
|
||||
const sendOptions: ActiveWebSendOptions | undefined =
|
||||
@@ -192,7 +195,9 @@ export async function sendTypingWhatsApp(
|
||||
cfg,
|
||||
accountId: options.accountId,
|
||||
});
|
||||
await active.sendComposingTo(to);
|
||||
if (!isWhatsAppNewsletterJid(toWhatsappJid(to))) {
|
||||
await active.sendComposingTo(to);
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendReactionWhatsApp(
|
||||
|
||||
41
extensions/whatsapp/src/session-route.test.ts
Normal file
41
extensions/whatsapp/src/session-route.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveWhatsAppOutboundSessionRoute } from "./session-route.js";
|
||||
|
||||
describe("resolveWhatsAppOutboundSessionRoute", () => {
|
||||
it("routes newsletter JIDs as channel sessions", () => {
|
||||
const route = resolveWhatsAppOutboundSessionRoute({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
target: "120363401234567890@newsletter",
|
||||
});
|
||||
|
||||
expect(route).toMatchObject({
|
||||
sessionKey: "agent:main:whatsapp:channel:120363401234567890@newsletter",
|
||||
baseSessionKey: "agent:main:whatsapp:channel:120363401234567890@newsletter",
|
||||
peer: {
|
||||
kind: "channel",
|
||||
id: "120363401234567890@newsletter",
|
||||
},
|
||||
chatType: "channel",
|
||||
from: "120363401234567890@newsletter",
|
||||
to: "120363401234567890@newsletter",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps direct user targets on direct session semantics", () => {
|
||||
const route = resolveWhatsAppOutboundSessionRoute({
|
||||
cfg: { session: { dmScope: "per-channel-peer" } },
|
||||
agentId: "main",
|
||||
target: "+15551234567",
|
||||
});
|
||||
|
||||
expect(route).toMatchObject({
|
||||
sessionKey: "agent:main:whatsapp:direct:+15551234567",
|
||||
peer: {
|
||||
kind: "direct",
|
||||
id: "+15551234567",
|
||||
},
|
||||
chatType: "direct",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,11 @@ import {
|
||||
buildChannelOutboundSessionRoute,
|
||||
type ChannelOutboundSessionRouteParams,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "./normalize.js";
|
||||
import {
|
||||
isWhatsAppGroupJid,
|
||||
isWhatsAppNewsletterJid,
|
||||
normalizeWhatsAppTarget,
|
||||
} from "./normalize.js";
|
||||
|
||||
export function resolveWhatsAppOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) {
|
||||
const normalized = normalizeWhatsAppTarget(params.target);
|
||||
@@ -10,16 +14,18 @@ export function resolveWhatsAppOutboundSessionRoute(params: ChannelOutboundSessi
|
||||
return null;
|
||||
}
|
||||
const isGroup = isWhatsAppGroupJid(normalized);
|
||||
const isNewsletter = isWhatsAppNewsletterJid(normalized);
|
||||
const chatType = isGroup ? "group" : isNewsletter ? "channel" : "direct";
|
||||
return buildChannelOutboundSessionRoute({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
channel: "whatsapp",
|
||||
accountId: params.accountId,
|
||||
peer: {
|
||||
kind: isGroup ? "group" : "direct",
|
||||
kind: chatType,
|
||||
id: normalized,
|
||||
},
|
||||
chatType: isGroup ? "group" : "direct",
|
||||
chatType,
|
||||
from: normalized,
|
||||
to: normalized,
|
||||
});
|
||||
|
||||
@@ -208,7 +208,7 @@ export function createWhatsAppPluginBase(params: {
|
||||
},
|
||||
setupWizard: params.setupWizard,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
chatTypes: ["direct", "group", "channel"],
|
||||
polls: true,
|
||||
reactions: true,
|
||||
media: true,
|
||||
|
||||
Reference in New Issue
Block a user