@openclaw/whatsapp 2026.5.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/action-runtime-api.ts +1 -0
- package/action-runtime.runtime.ts +1 -0
- package/api.ts +213 -0
- package/auth-presence.ts +80 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +3 -0
- package/config-api.ts +4 -0
- package/constants.ts +1 -0
- package/contract-api.ts +29 -0
- package/directory-contract-api.ts +4 -0
- package/dist/.boundary-tsc.stamp +1 -0
- package/dist/.boundary-tsc.tsbuildinfo +1 -0
- package/doctor-contract-api.ts +8 -0
- package/index.test.ts +13 -0
- package/index.ts +16 -0
- package/legacy-session-surface-api.ts +6 -0
- package/legacy-state-migrations-api.ts +1 -0
- package/light-runtime-api.ts +12 -0
- package/login-qr-api.ts +1 -0
- package/login-qr-runtime.ts +23 -0
- package/openclaw.plugin.json +742 -0
- package/outbound-payload-test-api.ts +1 -0
- package/package.json +74 -0
- package/runtime-api.ts +86 -0
- package/secret-contract-api.ts +4 -0
- package/security-contract-api.ts +4 -0
- package/setup-entry.test.ts +21 -0
- package/setup-entry.ts +21 -0
- package/setup-plugin-api.ts +3 -0
- package/src/account-config.ts +77 -0
- package/src/account-ids.ts +13 -0
- package/src/account-types.ts +5 -0
- package/src/accounts.test.ts +182 -0
- package/src/accounts.ts +176 -0
- package/src/accounts.whatsapp-auth.test.ts +59 -0
- package/src/action-runtime-target-auth.ts +27 -0
- package/src/action-runtime.test.ts +330 -0
- package/src/action-runtime.ts +76 -0
- package/src/active-listener.test.ts +65 -0
- package/src/active-listener.ts +17 -0
- package/src/agent-tools-login.test.ts +81 -0
- package/src/agent-tools-login.ts +113 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +27 -0
- package/src/auth-store.runtime.ts +1 -0
- package/src/auth-store.test.ts +311 -0
- package/src/auth-store.ts +502 -0
- package/src/auto-reply/config.runtime.ts +16 -0
- package/src/auto-reply/constants.ts +1 -0
- package/src/auto-reply/deliver-reply.test.ts +843 -0
- package/src/auto-reply/deliver-reply.ts +279 -0
- package/src/auto-reply/heartbeat-runner.runtime.ts +33 -0
- package/src/auto-reply/heartbeat-runner.test.ts +214 -0
- package/src/auto-reply/heartbeat-runner.ts +330 -0
- package/src/auto-reply/loggers.ts +6 -0
- package/src/auto-reply/mentions.ts +131 -0
- package/src/auto-reply/monitor/ack-reaction.test.ts +170 -0
- package/src/auto-reply/monitor/ack-reaction.ts +99 -0
- package/src/auto-reply/monitor/audio-preflight.runtime.ts +9 -0
- package/src/auto-reply/monitor/broadcast.ts +153 -0
- package/src/auto-reply/monitor/commands.ts +19 -0
- package/src/auto-reply/monitor/echo.ts +64 -0
- package/src/auto-reply/monitor/group-activation.runtime.ts +1 -0
- package/src/auto-reply/monitor/group-activation.test.ts +189 -0
- package/src/auto-reply/monitor/group-activation.ts +73 -0
- package/src/auto-reply/monitor/group-gating.audio-preflight.test.ts +103 -0
- package/src/auto-reply/monitor/group-gating.runtime.ts +8 -0
- package/src/auto-reply/monitor/group-gating.ts +217 -0
- package/src/auto-reply/monitor/group-members.test.ts +56 -0
- package/src/auto-reply/monitor/group-members.ts +65 -0
- package/src/auto-reply/monitor/inbound-context.test.ts +97 -0
- package/src/auto-reply/monitor/inbound-context.ts +92 -0
- package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +21 -0
- package/src/auto-reply/monitor/inbound-dispatch.test.ts +895 -0
- package/src/auto-reply/monitor/inbound-dispatch.ts +422 -0
- package/src/auto-reply/monitor/last-route.test.ts +37 -0
- package/src/auto-reply/monitor/last-route.ts +61 -0
- package/src/auto-reply/monitor/message-line.runtime.ts +38 -0
- package/src/auto-reply/monitor/message-line.ts +54 -0
- package/src/auto-reply/monitor/on-message.audio-preflight.test.ts +354 -0
- package/src/auto-reply/monitor/on-message.ts +291 -0
- package/src/auto-reply/monitor/peer.ts +17 -0
- package/src/auto-reply/monitor/process-message.audio-preflight.test.ts +420 -0
- package/src/auto-reply/monitor/process-message.test.ts +355 -0
- package/src/auto-reply/monitor/process-message.ts +531 -0
- package/src/auto-reply/monitor/runtime-api.ts +30 -0
- package/src/auto-reply/monitor-state.test.ts +64 -0
- package/src/auto-reply/monitor-state.ts +106 -0
- package/src/auto-reply/monitor.ts +600 -0
- package/src/auto-reply/reply-resolver.runtime.ts +1 -0
- package/src/auto-reply/session-snapshot.ts +69 -0
- package/src/auto-reply/types.ts +48 -0
- package/src/auto-reply/util.ts +63 -0
- package/src/auto-reply/web-auto-reply-monitor.test.ts +687 -0
- package/src/auto-reply/web-auto-reply-utils.test.ts +327 -0
- package/src/auto-reply.broadcast-groups.combined.test.ts +246 -0
- package/src/auto-reply.broadcast-groups.test-harness.ts +47 -0
- package/src/auto-reply.impl.ts +7 -0
- package/src/auto-reply.test-harness.ts +421 -0
- package/src/auto-reply.ts +1 -0
- package/src/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts +427 -0
- package/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts +908 -0
- package/src/auto-reply.web-auto-reply.last-route.test.ts +184 -0
- package/src/channel-actions.runtime.ts +7 -0
- package/src/channel-actions.test.ts +216 -0
- package/src/channel-actions.ts +84 -0
- package/src/channel-outbound.test.ts +131 -0
- package/src/channel-outbound.ts +36 -0
- package/src/channel-react-action.runtime.ts +7 -0
- package/src/channel-react-action.test.ts +294 -0
- package/src/channel-react-action.ts +84 -0
- package/src/channel.runtime.ts +117 -0
- package/src/channel.setup.test.ts +461 -0
- package/src/channel.setup.ts +28 -0
- package/src/channel.ts +336 -0
- package/src/command-policy.ts +7 -0
- package/src/config-accessors.test.ts +34 -0
- package/src/config-accessors.ts +22 -0
- package/src/config-schema.test.ts +121 -0
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +24 -0
- package/src/connection-controller-registry.test.ts +27 -0
- package/src/connection-controller-registry.ts +49 -0
- package/src/connection-controller.test.ts +282 -0
- package/src/connection-controller.ts +679 -0
- package/src/creds-files.ts +19 -0
- package/src/creds-persistence.ts +102 -0
- package/src/directory-config.test.ts +60 -0
- package/src/directory-config.ts +40 -0
- package/src/directory-contract.test.ts +56 -0
- package/src/doctor-contract.ts +11 -0
- package/src/doctor.test.ts +74 -0
- package/src/doctor.ts +56 -0
- package/src/group-intro.ts +15 -0
- package/src/group-policy.test.ts +36 -0
- package/src/group-policy.ts +40 -0
- package/src/group-session-contract.ts +20 -0
- package/src/group-session-key.test.ts +53 -0
- package/src/group-session-key.ts +41 -0
- package/src/heartbeat-recipients.runtime.ts +6 -0
- package/src/heartbeat-recipients.test.ts +203 -0
- package/src/heartbeat-recipients.ts +104 -0
- package/src/heartbeat.ts +34 -0
- package/src/identity.ts +164 -0
- package/src/inbound/access-control.test-harness.ts +37 -0
- package/src/inbound/access-control.test.ts +375 -0
- package/src/inbound/access-control.ts +237 -0
- package/src/inbound/dedupe.ts +132 -0
- package/src/inbound/extract.test.ts +282 -0
- package/src/inbound/extract.ts +488 -0
- package/src/inbound/lifecycle.ts +39 -0
- package/src/inbound/media.node.test.ts +83 -0
- package/src/inbound/media.ts +102 -0
- package/src/inbound/monitor.ts +890 -0
- package/src/inbound/runtime-api.ts +7 -0
- package/src/inbound/save-media.runtime.ts +1 -0
- package/src/inbound/send-api.test.ts +307 -0
- package/src/inbound/send-api.ts +141 -0
- package/src/inbound/send-result.ts +61 -0
- package/src/inbound/types.ts +106 -0
- package/src/inbound-context.contract.test.ts +32 -0
- package/src/inbound-policy.ts +231 -0
- package/src/inbound.media.test.ts +320 -0
- package/src/inbound.test.ts +290 -0
- package/src/inbound.ts +9 -0
- package/src/login-qr.test.ts +483 -0
- package/src/login-qr.ts +542 -0
- package/src/login.coverage.test.ts +133 -0
- package/src/login.test.ts +89 -0
- package/src/login.ts +70 -0
- package/src/logout.test.ts +162 -0
- package/src/media.test.ts +480 -0
- package/src/media.ts +10 -0
- package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +400 -0
- package/src/monitor-inbox.append-upsert.test-support.ts +133 -0
- package/src/monitor-inbox.behavior.test.ts +5 -0
- package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +362 -0
- package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +306 -0
- package/src/monitor-inbox.streams-inbound-messages.test-support.ts +729 -0
- package/src/monitor-inbox.test-harness.ts +304 -0
- package/src/normalize-target.ts +112 -0
- package/src/normalize.ts +6 -0
- package/src/outbound-adapter.poll.test.ts +62 -0
- package/src/outbound-adapter.sendpayload.test.ts +195 -0
- package/src/outbound-adapter.ts +36 -0
- package/src/outbound-base.test.ts +560 -0
- package/src/outbound-base.ts +244 -0
- package/src/outbound-media-contract.ts +278 -0
- package/src/outbound-media.runtime.ts +36 -0
- package/src/outbound-payload.contract.test.ts +61 -0
- package/src/outbound-send-deps.ts +1 -0
- package/src/outbound-test-support.ts +16 -0
- package/src/pairing-security.test-harness.ts +59 -0
- package/src/qr-image.ts +1 -0
- package/src/qr-terminal.ts +1 -0
- package/src/quoted-message.test.ts +83 -0
- package/src/quoted-message.ts +184 -0
- package/src/reaction-level.test.ts +94 -0
- package/src/reaction-level.ts +21 -0
- package/src/reconnect.test.ts +51 -0
- package/src/reconnect.ts +55 -0
- package/src/resolve-outbound-target.test.ts +265 -0
- package/src/resolve-outbound-target.ts +54 -0
- package/src/resolve-target.test.ts +97 -0
- package/src/runtime-api.ts +60 -0
- package/src/runtime-group-policy.ts +16 -0
- package/src/runtime.ts +9 -0
- package/src/security-contract.ts +47 -0
- package/src/security-fix.ts +71 -0
- package/src/send.test.ts +515 -0
- package/src/send.ts +287 -0
- package/src/session-contract.test.ts +31 -0
- package/src/session-contract.ts +43 -0
- package/src/session-errors.ts +125 -0
- package/src/session-route.ts +26 -0
- package/src/session.runtime.ts +8 -0
- package/src/session.test.ts +559 -0
- package/src/session.ts +330 -0
- package/src/setup-core.ts +52 -0
- package/src/setup-finalize.ts +446 -0
- package/src/setup-surface.test.ts +367 -0
- package/src/setup-surface.ts +69 -0
- package/src/setup-test-helpers.ts +216 -0
- package/src/shared.ts +291 -0
- package/src/socket-timing.test.ts +49 -0
- package/src/socket-timing.ts +38 -0
- package/src/state-migrations.ts +54 -0
- package/src/status-issues.test.ts +137 -0
- package/src/status-issues.ts +185 -0
- package/src/system-prompt.test.ts +199 -0
- package/src/system-prompt.ts +31 -0
- package/src/targets-runtime.ts +180 -0
- package/src/test-helpers.ts +690 -0
- package/src/text-runtime.test.ts +162 -0
- package/src/text-runtime.ts +11 -0
- package/src/vcard.ts +84 -0
- package/targets.ts +5 -0
- package/test-api.ts +2 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("./group-activation.js", () => ({
|
|
4
|
+
resolveGroupActivationFor: vi.fn(async () => "mention"),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
import type { MentionConfig } from "../mentions.js";
|
|
8
|
+
import type { WebInboundMsg } from "../types.js";
|
|
9
|
+
import { applyGroupGating, type GroupHistoryEntry } from "./group-gating.js";
|
|
10
|
+
|
|
11
|
+
function makeGroupAudioMsg(): WebInboundMsg {
|
|
12
|
+
return {
|
|
13
|
+
id: "msg-1",
|
|
14
|
+
from: "1203630@g.us",
|
|
15
|
+
to: "+15550000001",
|
|
16
|
+
body: "<media:audio>",
|
|
17
|
+
chatId: "1203630@g.us",
|
|
18
|
+
chatType: "group",
|
|
19
|
+
conversationId: "1203630@g.us",
|
|
20
|
+
mediaType: "audio/ogg; codecs=opus",
|
|
21
|
+
mediaPath: "/tmp/voice.ogg",
|
|
22
|
+
timestamp: 1700000000,
|
|
23
|
+
accountId: "default",
|
|
24
|
+
sender: { e164: "+15550000002", name: "Alice" },
|
|
25
|
+
} as WebInboundMsg;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeParams(msg: WebInboundMsg, groupHistories: Map<string, GroupHistoryEntry[]>) {
|
|
29
|
+
return {
|
|
30
|
+
cfg: {
|
|
31
|
+
channels: {
|
|
32
|
+
whatsapp: {
|
|
33
|
+
groupPolicy: "open",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
messages: {
|
|
37
|
+
groupChat: {
|
|
38
|
+
mentionPatterns: ["\\bopenclaw\\b"],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
} as never,
|
|
42
|
+
msg,
|
|
43
|
+
conversationId: "1203630@g.us",
|
|
44
|
+
groupHistoryKey: "whatsapp:group:1203630",
|
|
45
|
+
agentId: "main",
|
|
46
|
+
sessionKey: "agent:main:whatsapp:group:1203630",
|
|
47
|
+
baseMentionConfig: { mentionRegexes: [/\bopenclaw\b/i] } satisfies MentionConfig,
|
|
48
|
+
groupHistories,
|
|
49
|
+
groupHistoryLimit: 20,
|
|
50
|
+
groupMemberNames: new Map<string, Map<string, string>>(),
|
|
51
|
+
logVerbose: vi.fn(),
|
|
52
|
+
replyLogger: { debug: vi.fn() },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("applyGroupGating audio preflight mention text", () => {
|
|
57
|
+
let groupHistories: Map<string, GroupHistoryEntry[]>;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
groupHistories = new Map();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("defers a missing mention without storing placeholder history", async () => {
|
|
64
|
+
const msg = makeGroupAudioMsg();
|
|
65
|
+
|
|
66
|
+
const result = await applyGroupGating({
|
|
67
|
+
...makeParams(msg, groupHistories),
|
|
68
|
+
deferMissingMention: true,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(result).toEqual({ shouldProcess: false, needsMentionText: true });
|
|
72
|
+
expect(groupHistories.get("whatsapp:group:1203630")).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("accepts voice transcript text that satisfies mention gating", async () => {
|
|
76
|
+
const msg = makeGroupAudioMsg();
|
|
77
|
+
|
|
78
|
+
const result = await applyGroupGating({
|
|
79
|
+
...makeParams(msg, groupHistories),
|
|
80
|
+
mentionText: "openclaw please summarize the thread",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result).toEqual({ shouldProcess: true });
|
|
84
|
+
expect(msg.wasMentioned).toBe(true);
|
|
85
|
+
expect(groupHistories.get("whatsapp:group:1203630")).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("stores transcript text instead of the audio placeholder when mention is still missing", async () => {
|
|
89
|
+
const msg = makeGroupAudioMsg();
|
|
90
|
+
|
|
91
|
+
const result = await applyGroupGating({
|
|
92
|
+
...makeParams(msg, groupHistories),
|
|
93
|
+
mentionText: "please summarize the thread",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(result).toEqual({ shouldProcess: false });
|
|
97
|
+
expect(groupHistories.get("whatsapp:group:1203630")).toEqual([
|
|
98
|
+
expect.objectContaining({
|
|
99
|
+
body: "please summarize the thread",
|
|
100
|
+
}),
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export {
|
|
2
|
+
implicitMentionKindWhen,
|
|
3
|
+
resolveInboundMentionDecision,
|
|
4
|
+
} from "openclaw/plugin-sdk/channel-mention-gating";
|
|
5
|
+
export { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
|
|
6
|
+
export { recordPendingHistoryEntryIfEnabled } from "openclaw/plugin-sdk/reply-history";
|
|
7
|
+
export { parseActivationCommand } from "openclaw/plugin-sdk/group-activation";
|
|
8
|
+
export { normalizeE164 } from "../../text-runtime.js";
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
2
|
+
import {
|
|
3
|
+
getPrimaryIdentityId,
|
|
4
|
+
getReplyContext,
|
|
5
|
+
getSelfIdentity,
|
|
6
|
+
getSenderIdentity,
|
|
7
|
+
identitiesOverlap,
|
|
8
|
+
} from "../../identity.js";
|
|
9
|
+
import { resolveWhatsAppInboundPolicy } from "../../inbound-policy.js";
|
|
10
|
+
import type { MentionConfig } from "../mentions.js";
|
|
11
|
+
import { buildMentionConfig, debugMention, resolveOwnerList } from "../mentions.js";
|
|
12
|
+
import type { WebInboundMsg } from "../types.js";
|
|
13
|
+
import { stripMentionsForCommand } from "./commands.js";
|
|
14
|
+
import { resolveGroupActivationFor } from "./group-activation.js";
|
|
15
|
+
import {
|
|
16
|
+
hasControlCommand,
|
|
17
|
+
implicitMentionKindWhen,
|
|
18
|
+
normalizeE164,
|
|
19
|
+
parseActivationCommand,
|
|
20
|
+
recordPendingHistoryEntryIfEnabled,
|
|
21
|
+
resolveInboundMentionDecision,
|
|
22
|
+
} from "./group-gating.runtime.js";
|
|
23
|
+
import { noteGroupMember } from "./group-members.js";
|
|
24
|
+
|
|
25
|
+
export type GroupHistoryEntry = {
|
|
26
|
+
sender: string;
|
|
27
|
+
body: string;
|
|
28
|
+
timestamp?: number;
|
|
29
|
+
id?: string;
|
|
30
|
+
senderJid?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ApplyGroupGatingParams = {
|
|
34
|
+
cfg: OpenClawConfig;
|
|
35
|
+
msg: WebInboundMsg;
|
|
36
|
+
mentionText?: string;
|
|
37
|
+
deferMissingMention?: boolean;
|
|
38
|
+
conversationId: string;
|
|
39
|
+
groupHistoryKey: string;
|
|
40
|
+
agentId: string;
|
|
41
|
+
sessionKey: string;
|
|
42
|
+
baseMentionConfig: MentionConfig;
|
|
43
|
+
authDir?: string;
|
|
44
|
+
groupHistories: Map<string, GroupHistoryEntry[]>;
|
|
45
|
+
groupHistoryLimit: number;
|
|
46
|
+
groupMemberNames: Map<string, Map<string, string>>;
|
|
47
|
+
selfChatMode?: boolean;
|
|
48
|
+
logVerbose: (msg: string) => void;
|
|
49
|
+
replyLogger: { debug: (obj: unknown, msg: string) => void };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function isOwnerSender(baseMentionConfig: MentionConfig, msg: WebInboundMsg) {
|
|
53
|
+
const sender = normalizeE164(getSenderIdentity(msg).e164 ?? "");
|
|
54
|
+
if (!sender) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const owners = resolveOwnerList(baseMentionConfig, getSelfIdentity(msg).e164 ?? undefined);
|
|
58
|
+
return owners.includes(sender);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function recordPendingGroupHistoryEntry(params: {
|
|
62
|
+
msg: WebInboundMsg;
|
|
63
|
+
body?: string;
|
|
64
|
+
groupHistories: Map<string, GroupHistoryEntry[]>;
|
|
65
|
+
groupHistoryKey: string;
|
|
66
|
+
groupHistoryLimit: number;
|
|
67
|
+
}) {
|
|
68
|
+
const senderIdentity = getSenderIdentity(params.msg);
|
|
69
|
+
const sender =
|
|
70
|
+
senderIdentity.name && senderIdentity.e164
|
|
71
|
+
? `${senderIdentity.name} (${senderIdentity.e164})`
|
|
72
|
+
: (senderIdentity.name ??
|
|
73
|
+
senderIdentity.e164 ??
|
|
74
|
+
getPrimaryIdentityId(senderIdentity) ??
|
|
75
|
+
"Unknown");
|
|
76
|
+
recordPendingHistoryEntryIfEnabled({
|
|
77
|
+
historyMap: params.groupHistories,
|
|
78
|
+
historyKey: params.groupHistoryKey,
|
|
79
|
+
limit: params.groupHistoryLimit,
|
|
80
|
+
entry: {
|
|
81
|
+
sender,
|
|
82
|
+
body: params.body ?? params.msg.body,
|
|
83
|
+
timestamp: params.msg.timestamp,
|
|
84
|
+
id: params.msg.id,
|
|
85
|
+
senderJid: senderIdentity.jid ?? params.msg.senderJid,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function skipGroupMessageAndStoreHistory(
|
|
91
|
+
params: ApplyGroupGatingParams,
|
|
92
|
+
verboseMessage: string,
|
|
93
|
+
body?: string,
|
|
94
|
+
) {
|
|
95
|
+
params.logVerbose(verboseMessage);
|
|
96
|
+
recordPendingGroupHistoryEntry({
|
|
97
|
+
msg: params.msg,
|
|
98
|
+
body,
|
|
99
|
+
groupHistories: params.groupHistories,
|
|
100
|
+
groupHistoryKey: params.groupHistoryKey,
|
|
101
|
+
groupHistoryLimit: params.groupHistoryLimit,
|
|
102
|
+
});
|
|
103
|
+
return { shouldProcess: false } as const;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function applyGroupGating(params: ApplyGroupGatingParams) {
|
|
107
|
+
const sender = getSenderIdentity(params.msg);
|
|
108
|
+
const self = getSelfIdentity(params.msg, params.authDir);
|
|
109
|
+
const inboundPolicy = resolveWhatsAppInboundPolicy({
|
|
110
|
+
cfg: params.cfg,
|
|
111
|
+
accountId: params.msg.accountId,
|
|
112
|
+
selfE164: self.e164 ?? null,
|
|
113
|
+
});
|
|
114
|
+
const conversationGroupPolicy = inboundPolicy.resolveConversationGroupPolicy(
|
|
115
|
+
params.conversationId,
|
|
116
|
+
);
|
|
117
|
+
if (conversationGroupPolicy.allowlistEnabled && !conversationGroupPolicy.allowed) {
|
|
118
|
+
params.logVerbose(`Skipping group message ${params.conversationId} (not in allowlist)`);
|
|
119
|
+
return { shouldProcess: false };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
noteGroupMember(
|
|
123
|
+
params.groupMemberNames,
|
|
124
|
+
params.groupHistoryKey,
|
|
125
|
+
sender.e164 ?? undefined,
|
|
126
|
+
sender.name ?? undefined,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const baseMentionConfig = {
|
|
130
|
+
...params.baseMentionConfig,
|
|
131
|
+
allowFrom: inboundPolicy.configuredAllowFrom,
|
|
132
|
+
};
|
|
133
|
+
const mentionConfig = {
|
|
134
|
+
...buildMentionConfig(params.cfg, params.agentId),
|
|
135
|
+
allowFrom: inboundPolicy.configuredAllowFrom,
|
|
136
|
+
};
|
|
137
|
+
const mentionMsg =
|
|
138
|
+
params.mentionText !== undefined ? { ...params.msg, body: params.mentionText } : params.msg;
|
|
139
|
+
const commandBody = stripMentionsForCommand(
|
|
140
|
+
mentionMsg.body,
|
|
141
|
+
mentionConfig.mentionRegexes,
|
|
142
|
+
self.e164,
|
|
143
|
+
);
|
|
144
|
+
const activationCommand = parseActivationCommand(commandBody);
|
|
145
|
+
const owner = isOwnerSender(baseMentionConfig, params.msg);
|
|
146
|
+
const shouldBypassMention = owner && hasControlCommand(commandBody, params.cfg);
|
|
147
|
+
|
|
148
|
+
if (activationCommand.hasCommand && !owner) {
|
|
149
|
+
return skipGroupMessageAndStoreHistory(
|
|
150
|
+
params,
|
|
151
|
+
`Ignoring /activation from non-owner in group ${params.conversationId}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const mentionDebug = debugMention(mentionMsg, mentionConfig, params.authDir);
|
|
156
|
+
params.replyLogger.debug(
|
|
157
|
+
{
|
|
158
|
+
conversationId: params.conversationId,
|
|
159
|
+
wasMentioned: mentionDebug.wasMentioned,
|
|
160
|
+
...mentionDebug.details,
|
|
161
|
+
},
|
|
162
|
+
"group mention debug",
|
|
163
|
+
);
|
|
164
|
+
const wasMentioned = mentionDebug.wasMentioned;
|
|
165
|
+
const activation = await resolveGroupActivationFor({
|
|
166
|
+
cfg: params.cfg,
|
|
167
|
+
accountId: inboundPolicy.account.accountId,
|
|
168
|
+
agentId: params.agentId,
|
|
169
|
+
sessionKey: params.sessionKey,
|
|
170
|
+
conversationId: params.conversationId,
|
|
171
|
+
});
|
|
172
|
+
const requireMention = activation !== "always";
|
|
173
|
+
const replyContext = getReplyContext(params.msg, params.authDir);
|
|
174
|
+
const sharedNumberSelfChat = params.selfChatMode === true;
|
|
175
|
+
// Detect reply-to-bot: compare JIDs, LIDs, and E.164 numbers.
|
|
176
|
+
// WhatsApp may report the quoted message sender as either a phone JID
|
|
177
|
+
// (xxxxx@s.whatsapp.net) or a LID (xxxxx@lid), so we compare both.
|
|
178
|
+
// But in shared-number/selfChatMode setups, replies from the same self number
|
|
179
|
+
// should not count as implicit bot mentions unless the message explicitly
|
|
180
|
+
// mentioned the bot in text.
|
|
181
|
+
const implicitReplyToSelf = sharedNumberSelfChat && identitiesOverlap(self, sender);
|
|
182
|
+
const implicitMentionKinds = implicitMentionKindWhen(
|
|
183
|
+
"quoted_bot",
|
|
184
|
+
!implicitReplyToSelf && identitiesOverlap(self, replyContext?.sender),
|
|
185
|
+
);
|
|
186
|
+
const mentionDecision = resolveInboundMentionDecision({
|
|
187
|
+
facts: {
|
|
188
|
+
canDetectMention: true,
|
|
189
|
+
wasMentioned,
|
|
190
|
+
implicitMentionKinds,
|
|
191
|
+
},
|
|
192
|
+
policy: {
|
|
193
|
+
isGroup: true,
|
|
194
|
+
requireMention,
|
|
195
|
+
allowTextCommands: false,
|
|
196
|
+
hasControlCommand: false,
|
|
197
|
+
commandAuthorized: false,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
const effectiveWasMentioned = mentionDecision.effectiveWasMentioned || shouldBypassMention;
|
|
201
|
+
params.msg.wasMentioned = effectiveWasMentioned;
|
|
202
|
+
if (!shouldBypassMention && requireMention && mentionDecision.shouldSkip) {
|
|
203
|
+
if (params.deferMissingMention === true) {
|
|
204
|
+
params.logVerbose(
|
|
205
|
+
`Deferring group mention skip until audio preflight completes in ${params.conversationId}`,
|
|
206
|
+
);
|
|
207
|
+
return { shouldProcess: false, needsMentionText: true } as const;
|
|
208
|
+
}
|
|
209
|
+
return skipGroupMessageAndStoreHistory(
|
|
210
|
+
params,
|
|
211
|
+
`Group message stored for context (no mention detected) in ${params.conversationId}: ${mentionMsg.body}`,
|
|
212
|
+
params.mentionText,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { shouldProcess: true };
|
|
217
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatGroupMembers, noteGroupMember } from "./group-members.js";
|
|
3
|
+
|
|
4
|
+
describe("noteGroupMember", () => {
|
|
5
|
+
it("normalizes member phone numbers before storing", () => {
|
|
6
|
+
const groupMemberNames = new Map<string, Map<string, string>>();
|
|
7
|
+
|
|
8
|
+
noteGroupMember(groupMemberNames, "g1", "+1 (555) 123-4567", "Alice");
|
|
9
|
+
|
|
10
|
+
expect(groupMemberNames.get("g1")?.get("+15551234567")).toBe("Alice");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("ignores incomplete member values", () => {
|
|
14
|
+
const groupMemberNames = new Map<string, Map<string, string>>();
|
|
15
|
+
|
|
16
|
+
noteGroupMember(groupMemberNames, "g1", undefined, "Alice");
|
|
17
|
+
noteGroupMember(groupMemberNames, "g1", "+15551234567", undefined);
|
|
18
|
+
|
|
19
|
+
expect(groupMemberNames.get("g1")).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("formatGroupMembers", () => {
|
|
24
|
+
it("deduplicates participants and appends named roster members", () => {
|
|
25
|
+
const roster = new Map<string, string>([
|
|
26
|
+
["+16660000000", "Bob"],
|
|
27
|
+
["+17770000000", "Carol"],
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const formatted = formatGroupMembers({
|
|
31
|
+
participants: ["+1 (555) 000-0000", "+15550000000", "+16660000000"],
|
|
32
|
+
roster,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(formatted).toBe("+15550000000, Bob (+16660000000), Carol (+17770000000)");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("falls back to sender when no participants or roster are available", () => {
|
|
39
|
+
const formatted = formatGroupMembers({
|
|
40
|
+
participants: [],
|
|
41
|
+
roster: undefined,
|
|
42
|
+
fallbackE164: "+1 (555) 222-3333",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(formatted).toBe("+15552223333");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns undefined when no members can be resolved", () => {
|
|
49
|
+
expect(
|
|
50
|
+
formatGroupMembers({
|
|
51
|
+
participants: [],
|
|
52
|
+
roster: undefined,
|
|
53
|
+
}),
|
|
54
|
+
).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { normalizeE164 } from "../../text-runtime.js";
|
|
2
|
+
|
|
3
|
+
function appendNormalizedUnique(entries: Iterable<string>, seen: Set<string>, ordered: string[]) {
|
|
4
|
+
for (const entry of entries) {
|
|
5
|
+
const normalized = normalizeE164(entry) ?? entry;
|
|
6
|
+
if (!normalized || seen.has(normalized)) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
seen.add(normalized);
|
|
10
|
+
ordered.push(normalized);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function noteGroupMember(
|
|
15
|
+
groupMemberNames: Map<string, Map<string, string>>,
|
|
16
|
+
conversationId: string,
|
|
17
|
+
e164?: string,
|
|
18
|
+
name?: string,
|
|
19
|
+
) {
|
|
20
|
+
if (!e164 || !name) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const normalized = normalizeE164(e164);
|
|
24
|
+
const key = normalized ?? e164;
|
|
25
|
+
if (!key) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
let roster = groupMemberNames.get(conversationId);
|
|
29
|
+
if (!roster) {
|
|
30
|
+
roster = new Map();
|
|
31
|
+
groupMemberNames.set(conversationId, roster);
|
|
32
|
+
}
|
|
33
|
+
roster.set(key, name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatGroupMembers(params: {
|
|
37
|
+
participants: string[] | undefined;
|
|
38
|
+
roster: Map<string, string> | undefined;
|
|
39
|
+
fallbackE164?: string;
|
|
40
|
+
}) {
|
|
41
|
+
const { participants, roster, fallbackE164 } = params;
|
|
42
|
+
const seen = new Set<string>();
|
|
43
|
+
const ordered: string[] = [];
|
|
44
|
+
if (participants?.length) {
|
|
45
|
+
appendNormalizedUnique(participants, seen, ordered);
|
|
46
|
+
}
|
|
47
|
+
if (roster) {
|
|
48
|
+
appendNormalizedUnique(roster.keys(), seen, ordered);
|
|
49
|
+
}
|
|
50
|
+
if (ordered.length === 0 && fallbackE164) {
|
|
51
|
+
const normalized = normalizeE164(fallbackE164) ?? fallbackE164;
|
|
52
|
+
if (normalized) {
|
|
53
|
+
ordered.push(normalized);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (ordered.length === 0) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
return ordered
|
|
60
|
+
.map((entry) => {
|
|
61
|
+
const name = roster?.get(entry);
|
|
62
|
+
return name ? `${name} (${entry})` : entry;
|
|
63
|
+
})
|
|
64
|
+
.join(", ");
|
|
65
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { WhatsAppSendResult } from "../../inbound/send-result.js";
|
|
3
|
+
import {
|
|
4
|
+
resolveVisibleWhatsAppGroupHistory,
|
|
5
|
+
resolveVisibleWhatsAppReplyContext,
|
|
6
|
+
} from "./inbound-context.js";
|
|
7
|
+
|
|
8
|
+
type ReplyContextParams = Parameters<typeof resolveVisibleWhatsAppReplyContext>[0];
|
|
9
|
+
|
|
10
|
+
function acceptedSendResult(kind: "media" | "text", id: string): WhatsAppSendResult {
|
|
11
|
+
return {
|
|
12
|
+
kind,
|
|
13
|
+
messageId: id,
|
|
14
|
+
messageIds: [id],
|
|
15
|
+
keys: [{ id }],
|
|
16
|
+
providerAccepted: true,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const makeBlockedQuotedReplyMessage = (id: string): ReplyContextParams["msg"] => ({
|
|
21
|
+
id,
|
|
22
|
+
from: "123@g.us",
|
|
23
|
+
conversationId: "123@g.us",
|
|
24
|
+
to: "+2000",
|
|
25
|
+
accountId: "default",
|
|
26
|
+
chatType: "group",
|
|
27
|
+
chatId: "123@g.us",
|
|
28
|
+
body: "Current message",
|
|
29
|
+
senderName: "Alice",
|
|
30
|
+
senderJid: "111@s.whatsapp.net",
|
|
31
|
+
senderE164: "+111",
|
|
32
|
+
selfE164: "+999",
|
|
33
|
+
replyToId: "blocked-reply",
|
|
34
|
+
replyToBody: "Blocked quoted text",
|
|
35
|
+
replyToSender: "Mallory (+999)",
|
|
36
|
+
replyToSenderJid: "999@s.whatsapp.net",
|
|
37
|
+
sendComposing: async () => {},
|
|
38
|
+
reply: async () => acceptedSendResult("text", "r1"),
|
|
39
|
+
sendMedia: async () => acceptedSendResult("media", "m1"),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("whatsapp inbound context visibility", () => {
|
|
43
|
+
it("filters non-allowlisted group history from supplemental context", () => {
|
|
44
|
+
const history = resolveVisibleWhatsAppGroupHistory({
|
|
45
|
+
history: [
|
|
46
|
+
{
|
|
47
|
+
sender: "Alice (+111)",
|
|
48
|
+
body: "Allowed context",
|
|
49
|
+
senderJid: "111@s.whatsapp.net",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
sender: "Mallory (+999)",
|
|
53
|
+
body: "Blocked context",
|
|
54
|
+
senderJid: "999@s.whatsapp.net",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
mode: "allowlist",
|
|
58
|
+
groupPolicy: "allowlist",
|
|
59
|
+
groupAllowFrom: ["+111"],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(history).toEqual([
|
|
63
|
+
expect.objectContaining({
|
|
64
|
+
sender: "Alice (+111)",
|
|
65
|
+
body: "Allowed context",
|
|
66
|
+
}),
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("redacts blocked quoted replies in allowlist mode", () => {
|
|
71
|
+
const reply = resolveVisibleWhatsAppReplyContext({
|
|
72
|
+
msg: makeBlockedQuotedReplyMessage("msg-reply-1"),
|
|
73
|
+
mode: "allowlist",
|
|
74
|
+
groupPolicy: "allowlist",
|
|
75
|
+
groupAllowFrom: ["+111"],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(reply).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("keeps blocked quoted replies in allowlist_quote mode", () => {
|
|
82
|
+
const reply = resolveVisibleWhatsAppReplyContext({
|
|
83
|
+
msg: makeBlockedQuotedReplyMessage("msg-reply-2"),
|
|
84
|
+
mode: "allowlist_quote",
|
|
85
|
+
groupPolicy: "allowlist",
|
|
86
|
+
groupAllowFrom: ["+111"],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(reply).toMatchObject({
|
|
90
|
+
id: "blocked-reply",
|
|
91
|
+
body: "Blocked quoted text",
|
|
92
|
+
sender: expect.objectContaining({
|
|
93
|
+
label: "Mallory (+999)",
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
evaluateSupplementalContextVisibility,
|
|
3
|
+
filterSupplementalContextItems,
|
|
4
|
+
} from "openclaw/plugin-sdk/security-runtime";
|
|
5
|
+
import {
|
|
6
|
+
getComparableIdentityValues,
|
|
7
|
+
getReplyContext,
|
|
8
|
+
type WhatsAppIdentity,
|
|
9
|
+
type WhatsAppReplyContext,
|
|
10
|
+
} from "../../identity.js";
|
|
11
|
+
import { normalizeE164 } from "../../text-runtime.js";
|
|
12
|
+
import type { WebInboundMsg } from "../types.js";
|
|
13
|
+
|
|
14
|
+
export type GroupHistoryEntry = {
|
|
15
|
+
sender: string;
|
|
16
|
+
body: string;
|
|
17
|
+
timestamp?: number;
|
|
18
|
+
id?: string;
|
|
19
|
+
senderJid?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ContextVisibilityMode = "all" | "allowlist" | "allowlist_quote";
|
|
23
|
+
|
|
24
|
+
function isWhatsAppSupplementalSenderAllowed(params: {
|
|
25
|
+
allowFrom: string[];
|
|
26
|
+
sender?: WhatsAppIdentity | null;
|
|
27
|
+
}): boolean {
|
|
28
|
+
if (params.allowFrom.includes("*")) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
const senderValues = new Set(getComparableIdentityValues(params.sender));
|
|
32
|
+
if (senderValues.size === 0) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
for (const entry of params.allowFrom) {
|
|
36
|
+
const rawEntry = entry.trim();
|
|
37
|
+
if (!rawEntry) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const normalizedEntry = normalizeE164(rawEntry);
|
|
41
|
+
if ((normalizedEntry && senderValues.has(normalizedEntry)) || senderValues.has(rawEntry)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveVisibleWhatsAppGroupHistory(params: {
|
|
49
|
+
history: GroupHistoryEntry[];
|
|
50
|
+
mode: ContextVisibilityMode;
|
|
51
|
+
groupPolicy: "open" | "allowlist" | "disabled";
|
|
52
|
+
groupAllowFrom: string[];
|
|
53
|
+
}): GroupHistoryEntry[] {
|
|
54
|
+
if (params.groupPolicy !== "allowlist") {
|
|
55
|
+
return params.history;
|
|
56
|
+
}
|
|
57
|
+
return filterSupplementalContextItems({
|
|
58
|
+
items: params.history,
|
|
59
|
+
mode: params.mode,
|
|
60
|
+
kind: "history",
|
|
61
|
+
isSenderAllowed: (entry) =>
|
|
62
|
+
isWhatsAppSupplementalSenderAllowed({
|
|
63
|
+
allowFrom: params.groupAllowFrom,
|
|
64
|
+
sender: entry.senderJid ? { jid: entry.senderJid } : null,
|
|
65
|
+
}),
|
|
66
|
+
}).items;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function resolveVisibleWhatsAppReplyContext(params: {
|
|
70
|
+
msg: WebInboundMsg;
|
|
71
|
+
authDir?: string;
|
|
72
|
+
mode: ContextVisibilityMode;
|
|
73
|
+
groupPolicy: "open" | "allowlist" | "disabled";
|
|
74
|
+
groupAllowFrom: string[];
|
|
75
|
+
}): WhatsAppReplyContext | null {
|
|
76
|
+
const replyTo = getReplyContext(params.msg, params.authDir);
|
|
77
|
+
if (!replyTo) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const include = evaluateSupplementalContextVisibility({
|
|
81
|
+
mode: params.mode,
|
|
82
|
+
kind: "quote",
|
|
83
|
+
senderAllowed:
|
|
84
|
+
params.msg.chatType !== "group" || params.groupPolicy !== "allowlist"
|
|
85
|
+
? true
|
|
86
|
+
: isWhatsAppSupplementalSenderAllowed({
|
|
87
|
+
allowFrom: params.groupAllowFrom,
|
|
88
|
+
sender: replyTo.sender,
|
|
89
|
+
}),
|
|
90
|
+
}).include;
|
|
91
|
+
return include ? replyTo : null;
|
|
92
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createChannelReplyPipeline,
|
|
3
|
+
dispatchReplyWithBufferedBlockDispatcher,
|
|
4
|
+
finalizeInboundContext,
|
|
5
|
+
getAgentScopedMediaLocalRoots,
|
|
6
|
+
jidToE164,
|
|
7
|
+
logVerbose,
|
|
8
|
+
resolveChunkMode,
|
|
9
|
+
resolveIdentityNamePrefix,
|
|
10
|
+
resolveInboundLastRouteSessionKey,
|
|
11
|
+
resolveMarkdownTableMode,
|
|
12
|
+
resolveSendableOutboundReplyParts,
|
|
13
|
+
resolveTextChunkLimit,
|
|
14
|
+
shouldLogVerbose,
|
|
15
|
+
toLocationContext,
|
|
16
|
+
type getChildLogger,
|
|
17
|
+
type getReplyFromConfig,
|
|
18
|
+
type LoadConfigFn,
|
|
19
|
+
type ReplyPayload,
|
|
20
|
+
type resolveAgentRoute,
|
|
21
|
+
} from "./runtime-api.js";
|