@gakr-gakr/whatsapp 0.1.0

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.
Files changed (159) hide show
  1. package/action-runtime-api.ts +1 -0
  2. package/action-runtime.runtime.ts +1 -0
  3. package/api.ts +67 -0
  4. package/auth-presence.ts +80 -0
  5. package/autobot.plugin.json +23 -0
  6. package/channel-config-api.ts +1 -0
  7. package/channel-plugin-api.ts +3 -0
  8. package/config-api.ts +4 -0
  9. package/constants.ts +1 -0
  10. package/contract-api.ts +29 -0
  11. package/directory-contract-api.ts +4 -0
  12. package/doctor-contract-api.ts +8 -0
  13. package/index.ts +16 -0
  14. package/legacy-session-surface-api.ts +6 -0
  15. package/legacy-state-migrations-api.ts +1 -0
  16. package/light-runtime-api.ts +12 -0
  17. package/login-qr-api.ts +1 -0
  18. package/login-qr-runtime.ts +23 -0
  19. package/outbound-payload-test-api.ts +1 -0
  20. package/package.json +76 -0
  21. package/runtime-api.ts +84 -0
  22. package/secret-contract-api.ts +4 -0
  23. package/security-contract-api.ts +4 -0
  24. package/setup-entry.ts +21 -0
  25. package/setup-plugin-api.ts +3 -0
  26. package/src/account-config.ts +77 -0
  27. package/src/account-ids.ts +17 -0
  28. package/src/account-types.ts +5 -0
  29. package/src/accounts.ts +176 -0
  30. package/src/action-runtime-target-auth.ts +27 -0
  31. package/src/action-runtime.ts +76 -0
  32. package/src/active-listener.ts +17 -0
  33. package/src/agent-tools-login.ts +113 -0
  34. package/src/approval-auth.ts +27 -0
  35. package/src/auth-store.runtime.ts +1 -0
  36. package/src/auth-store.ts +494 -0
  37. package/src/auto-reply/config.runtime.ts +16 -0
  38. package/src/auto-reply/constants.ts +1 -0
  39. package/src/auto-reply/deliver-reply.ts +332 -0
  40. package/src/auto-reply/loggers.ts +6 -0
  41. package/src/auto-reply/mentions.ts +131 -0
  42. package/src/auto-reply/monitor/ack-reaction.ts +99 -0
  43. package/src/auto-reply/monitor/audio-preflight.runtime.ts +9 -0
  44. package/src/auto-reply/monitor/broadcast.ts +153 -0
  45. package/src/auto-reply/monitor/commands.ts +19 -0
  46. package/src/auto-reply/monitor/echo.ts +64 -0
  47. package/src/auto-reply/monitor/group-activation.runtime.ts +1 -0
  48. package/src/auto-reply/monitor/group-activation.ts +73 -0
  49. package/src/auto-reply/monitor/group-gating.runtime.ts +8 -0
  50. package/src/auto-reply/monitor/group-gating.ts +218 -0
  51. package/src/auto-reply/monitor/group-members.ts +65 -0
  52. package/src/auto-reply/monitor/inbound-context.ts +92 -0
  53. package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +22 -0
  54. package/src/auto-reply/monitor/inbound-dispatch.ts +749 -0
  55. package/src/auto-reply/monitor/last-route.ts +61 -0
  56. package/src/auto-reply/monitor/listener-log.ts +28 -0
  57. package/src/auto-reply/monitor/message-line.runtime.ts +38 -0
  58. package/src/auto-reply/monitor/message-line.ts +54 -0
  59. package/src/auto-reply/monitor/on-message.ts +333 -0
  60. package/src/auto-reply/monitor/peer.ts +17 -0
  61. package/src/auto-reply/monitor/process-message.ts +584 -0
  62. package/src/auto-reply/monitor/runtime-api.ts +36 -0
  63. package/src/auto-reply/monitor/status-reaction.ts +108 -0
  64. package/src/auto-reply/monitor-state.ts +114 -0
  65. package/src/auto-reply/monitor.ts +720 -0
  66. package/src/auto-reply/reply-resolver.runtime.ts +1 -0
  67. package/src/auto-reply/types.ts +48 -0
  68. package/src/auto-reply/util.ts +62 -0
  69. package/src/auto-reply.impl.ts +6 -0
  70. package/src/auto-reply.ts +1 -0
  71. package/src/channel-actions.runtime.ts +7 -0
  72. package/src/channel-actions.ts +85 -0
  73. package/src/channel-outbound.ts +87 -0
  74. package/src/channel-react-action.runtime.ts +10 -0
  75. package/src/channel-react-action.ts +247 -0
  76. package/src/channel.runtime.ts +117 -0
  77. package/src/channel.setup.ts +32 -0
  78. package/src/channel.ts +356 -0
  79. package/src/command-policy.ts +7 -0
  80. package/src/config-accessors.ts +22 -0
  81. package/src/config-schema.ts +6 -0
  82. package/src/config-ui-hints.ts +24 -0
  83. package/src/connection-controller-registry.ts +49 -0
  84. package/src/connection-controller.ts +680 -0
  85. package/src/creds-files.ts +19 -0
  86. package/src/creds-persistence.ts +71 -0
  87. package/src/directory-config.ts +40 -0
  88. package/src/doctor-contract.ts +11 -0
  89. package/src/doctor.ts +56 -0
  90. package/src/document-filename.ts +17 -0
  91. package/src/group-intro.ts +15 -0
  92. package/src/group-policy.ts +40 -0
  93. package/src/group-session-contract.ts +20 -0
  94. package/src/group-session-key.ts +42 -0
  95. package/src/heartbeat.ts +34 -0
  96. package/src/identity.ts +164 -0
  97. package/src/inbound/access-control.ts +187 -0
  98. package/src/inbound/dedupe.ts +132 -0
  99. package/src/inbound/extract.ts +484 -0
  100. package/src/inbound/lifecycle.ts +39 -0
  101. package/src/inbound/media.ts +128 -0
  102. package/src/inbound/monitor.ts +1042 -0
  103. package/src/inbound/outbound-mentions.ts +260 -0
  104. package/src/inbound/runtime-api.ts +7 -0
  105. package/src/inbound/save-media.runtime.ts +1 -0
  106. package/src/inbound/send-api.ts +203 -0
  107. package/src/inbound/send-result.ts +109 -0
  108. package/src/inbound/types.ts +107 -0
  109. package/src/inbound-policy.ts +215 -0
  110. package/src/inbound.ts +9 -0
  111. package/src/login-qr.ts +542 -0
  112. package/src/login.ts +83 -0
  113. package/src/media.ts +10 -0
  114. package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +417 -0
  115. package/src/monitor-inbox.append-upsert.test-support.ts +133 -0
  116. package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +418 -0
  117. package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +308 -0
  118. package/src/monitor-inbox.streams-inbound-messages.test-support.ts +824 -0
  119. package/src/normalize-target.ts +148 -0
  120. package/src/normalize.ts +8 -0
  121. package/src/outbound-adapter.ts +36 -0
  122. package/src/outbound-base.ts +256 -0
  123. package/src/outbound-media-contract.ts +307 -0
  124. package/src/outbound-media.runtime.ts +41 -0
  125. package/src/outbound-send-deps.ts +1 -0
  126. package/src/outbound-test-support.ts +16 -0
  127. package/src/qa-driver.runtime.ts +189 -0
  128. package/src/qr-image.ts +1 -0
  129. package/src/qr-terminal.ts +1 -0
  130. package/src/quoted-message.ts +184 -0
  131. package/src/reaction-level.ts +24 -0
  132. package/src/reconnect.ts +55 -0
  133. package/src/resolve-outbound-target.ts +58 -0
  134. package/src/runtime-api.ts +59 -0
  135. package/src/runtime-group-policy.ts +16 -0
  136. package/src/runtime.ts +9 -0
  137. package/src/security-contract.ts +47 -0
  138. package/src/security-fix.ts +71 -0
  139. package/src/send.ts +342 -0
  140. package/src/session-contract.ts +43 -0
  141. package/src/session-errors.ts +125 -0
  142. package/src/session-route.ts +32 -0
  143. package/src/session.runtime.ts +8 -0
  144. package/src/session.ts +327 -0
  145. package/src/setup-core.ts +52 -0
  146. package/src/setup-finalize.ts +450 -0
  147. package/src/setup-surface.ts +71 -0
  148. package/src/setup-test-helpers.ts +217 -0
  149. package/src/shared.ts +291 -0
  150. package/src/socket-timing.ts +38 -0
  151. package/src/state-migrations.ts +55 -0
  152. package/src/status-issues.ts +185 -0
  153. package/src/system-prompt.ts +31 -0
  154. package/src/targets-runtime.ts +221 -0
  155. package/src/text-runtime.ts +18 -0
  156. package/src/vcard.ts +84 -0
  157. package/targets.ts +5 -0
  158. package/test-api.ts +2 -0
  159. package/tsconfig.json +16 -0
@@ -0,0 +1,153 @@
1
+ import type { AckReactionHandle } from "autobot/plugin-sdk/channel-feedback";
2
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
3
+ import type { resolveAgentRoute } from "autobot/plugin-sdk/routing";
4
+ import { buildAgentSessionKey, deriveLastRoutePolicy } from "autobot/plugin-sdk/routing";
5
+ import {
6
+ buildAgentMainSessionKey,
7
+ DEFAULT_MAIN_KEY,
8
+ normalizeAgentId,
9
+ } from "autobot/plugin-sdk/routing";
10
+ import { resolveWhatsAppGroupSessionRoute } from "../../group-session-key.js";
11
+ import { formatError } from "../../session.js";
12
+ import { whatsappInboundLog } from "../loggers.js";
13
+ import type { WebInboundMsg } from "../types.js";
14
+ import type { GroupHistoryEntry } from "./inbound-context.js";
15
+
16
+ function buildBroadcastRouteKeys(params: {
17
+ cfg: AutoBotConfig;
18
+ msg: WebInboundMsg;
19
+ route: ReturnType<typeof resolveAgentRoute>;
20
+ peerId: string;
21
+ agentId: string;
22
+ }) {
23
+ const sessionKey = buildAgentSessionKey({
24
+ agentId: params.agentId,
25
+ channel: "whatsapp",
26
+ accountId: params.route.accountId,
27
+ peer: {
28
+ kind: params.msg.chatType === "group" ? "group" : "direct",
29
+ id: params.peerId,
30
+ },
31
+ dmScope: params.cfg.session?.dmScope,
32
+ identityLinks: params.cfg.session?.identityLinks,
33
+ });
34
+ const mainSessionKey = buildAgentMainSessionKey({
35
+ agentId: params.agentId,
36
+ mainKey: DEFAULT_MAIN_KEY,
37
+ });
38
+
39
+ return {
40
+ sessionKey,
41
+ mainSessionKey,
42
+ lastRoutePolicy: deriveLastRoutePolicy({
43
+ sessionKey,
44
+ mainSessionKey,
45
+ }),
46
+ };
47
+ }
48
+
49
+ export async function maybeBroadcastMessage(params: {
50
+ cfg: AutoBotConfig;
51
+ msg: WebInboundMsg;
52
+ peerId: string;
53
+ route: ReturnType<typeof resolveAgentRoute>;
54
+ groupHistoryKey: string;
55
+ groupHistories: Map<string, GroupHistoryEntry[]>;
56
+ processMessage: (
57
+ msg: WebInboundMsg,
58
+ route: ReturnType<typeof resolveAgentRoute>,
59
+ groupHistoryKey: string,
60
+ opts?: {
61
+ groupHistory?: GroupHistoryEntry[];
62
+ suppressGroupHistoryClear?: boolean;
63
+ preflightAudioTranscript?: string | null;
64
+ ackAlreadySent?: boolean;
65
+ ackReaction?: AckReactionHandle | null;
66
+ },
67
+ ) => Promise<boolean>;
68
+ preflightAudioTranscript?: string | null;
69
+ ackAlreadySent?: boolean;
70
+ ackReaction?: AckReactionHandle | null;
71
+ }) {
72
+ const broadcastAgents = params.cfg.broadcast?.[params.peerId];
73
+ if (!broadcastAgents || !Array.isArray(broadcastAgents)) {
74
+ return false;
75
+ }
76
+ if (broadcastAgents.length === 0) {
77
+ return false;
78
+ }
79
+
80
+ const strategy = params.cfg.broadcast?.strategy || "parallel";
81
+ whatsappInboundLog.info(`Broadcasting message to ${broadcastAgents.length} agents (${strategy})`);
82
+
83
+ const agentIds = params.cfg.agents?.list?.map((agent) => normalizeAgentId(agent.id));
84
+ const hasKnownAgents = (agentIds?.length ?? 0) > 0;
85
+ const groupHistorySnapshot =
86
+ params.msg.chatType === "group"
87
+ ? (params.groupHistories.get(params.groupHistoryKey) ?? [])
88
+ : undefined;
89
+
90
+ const processForAgent = async (agentId: string): Promise<boolean> => {
91
+ const normalizedAgentId = normalizeAgentId(agentId);
92
+ if (hasKnownAgents && !agentIds?.includes(normalizedAgentId)) {
93
+ whatsappInboundLog.warn(`Broadcast agent ${agentId} not found in agents.list; skipping`);
94
+ return false;
95
+ }
96
+ const routeKeys = buildBroadcastRouteKeys({
97
+ cfg: params.cfg,
98
+ msg: params.msg,
99
+ route: params.route,
100
+ peerId: params.peerId,
101
+ agentId: normalizedAgentId,
102
+ });
103
+ const baseAgentRoute = {
104
+ ...params.route,
105
+ agentId: normalizedAgentId,
106
+ ...routeKeys,
107
+ };
108
+ const agentRoute =
109
+ params.msg.chatType === "group"
110
+ ? resolveWhatsAppGroupSessionRoute(baseAgentRoute)
111
+ : baseAgentRoute;
112
+
113
+ try {
114
+ const opts: {
115
+ groupHistory?: GroupHistoryEntry[];
116
+ suppressGroupHistoryClear: true;
117
+ preflightAudioTranscript?: string | null;
118
+ ackAlreadySent?: boolean;
119
+ ackReaction?: AckReactionHandle | null;
120
+ } = {
121
+ groupHistory: groupHistorySnapshot,
122
+ suppressGroupHistoryClear: true,
123
+ };
124
+ if (params.preflightAudioTranscript !== undefined) {
125
+ opts.preflightAudioTranscript = params.preflightAudioTranscript;
126
+ }
127
+ if (params.ackAlreadySent === true) {
128
+ opts.ackAlreadySent = true;
129
+ }
130
+ if (params.ackReaction !== undefined) {
131
+ opts.ackReaction = params.ackReaction;
132
+ }
133
+ return await params.processMessage(params.msg, agentRoute, params.groupHistoryKey, opts);
134
+ } catch (err) {
135
+ whatsappInboundLog.error(`Broadcast agent ${agentId} failed: ${formatError(err)}`);
136
+ return false;
137
+ }
138
+ };
139
+
140
+ if (strategy === "sequential") {
141
+ for (const agentId of broadcastAgents) {
142
+ await processForAgent(agentId);
143
+ }
144
+ } else {
145
+ await Promise.allSettled(broadcastAgents.map(processForAgent));
146
+ }
147
+
148
+ if (params.msg.chatType === "group") {
149
+ params.groupHistories.set(params.groupHistoryKey, []);
150
+ }
151
+
152
+ return true;
153
+ }
@@ -0,0 +1,19 @@
1
+ export function stripMentionsForCommand(
2
+ text: string,
3
+ mentionRegexes: RegExp[],
4
+ selfE164?: string | null,
5
+ ) {
6
+ let result = text;
7
+ for (const re of mentionRegexes) {
8
+ result = result.replace(re, " ");
9
+ }
10
+ if (selfE164) {
11
+ // `selfE164` is usually like "+1234"; strip down to digits so we can match "+?1234" safely.
12
+ const digits = selfE164.replace(/\D/g, "");
13
+ if (digits) {
14
+ const pattern = new RegExp(`\\+?${digits}`, "g");
15
+ result = result.replace(pattern, " ");
16
+ }
17
+ }
18
+ return result.replace(/\s+/g, " ").trim();
19
+ }
@@ -0,0 +1,64 @@
1
+ export type EchoTracker = {
2
+ rememberText: (
3
+ text: string | undefined,
4
+ opts: {
5
+ combinedBody?: string;
6
+ combinedBodySessionKey?: string;
7
+ logVerboseMessage?: boolean;
8
+ },
9
+ ) => void;
10
+ has: (key: string) => boolean;
11
+ forget: (key: string) => void;
12
+ buildCombinedKey: (params: { sessionKey: string; combinedBody: string }) => string;
13
+ };
14
+
15
+ export function createEchoTracker(params: {
16
+ maxItems?: number;
17
+ logVerbose?: (msg: string) => void;
18
+ }): EchoTracker {
19
+ const recentlySent = new Set<string>();
20
+ const maxItems = Math.max(1, params.maxItems ?? 100);
21
+
22
+ const buildCombinedKey = (p: { sessionKey: string; combinedBody: string }) =>
23
+ `combined:${p.sessionKey}:${p.combinedBody}`;
24
+
25
+ const trim = () => {
26
+ while (recentlySent.size > maxItems) {
27
+ const firstKey = recentlySent.values().next().value;
28
+ if (!firstKey) {
29
+ break;
30
+ }
31
+ recentlySent.delete(firstKey);
32
+ }
33
+ };
34
+
35
+ const rememberText: EchoTracker["rememberText"] = (text, opts) => {
36
+ if (!text) {
37
+ return;
38
+ }
39
+ recentlySent.add(text);
40
+ if (opts.combinedBody && opts.combinedBodySessionKey) {
41
+ recentlySent.add(
42
+ buildCombinedKey({
43
+ sessionKey: opts.combinedBodySessionKey,
44
+ combinedBody: opts.combinedBody,
45
+ }),
46
+ );
47
+ }
48
+ if (opts.logVerboseMessage) {
49
+ params.logVerbose?.(
50
+ `Added to echo detection set (size now: ${recentlySent.size}): ${text.slice(0, 50)}...`,
51
+ );
52
+ }
53
+ trim();
54
+ };
55
+
56
+ return {
57
+ rememberText,
58
+ has: (key) => recentlySent.has(key),
59
+ forget: (key) => {
60
+ recentlySent.delete(key);
61
+ },
62
+ buildCombinedKey,
63
+ };
64
+ }
@@ -0,0 +1 @@
1
+ export { normalizeGroupActivation } from "autobot/plugin-sdk/group-activation";
@@ -0,0 +1,73 @@
1
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
2
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "autobot/plugin-sdk/routing";
3
+ import { updateSessionStore } from "autobot/plugin-sdk/session-store-runtime";
4
+ import { resolveWhatsAppLegacyGroupSessionKey } from "../../group-session-key.js";
5
+ import { resolveWhatsAppInboundPolicy } from "../../inbound-policy.js";
6
+ import { loadSessionStore, resolveStorePath } from "../config.runtime.js";
7
+ import { normalizeGroupActivation } from "./group-activation.runtime.js";
8
+
9
+ function hasNamedWhatsAppAccounts(cfg: AutoBotConfig) {
10
+ const accountIds = Object.keys(cfg.channels?.whatsapp?.accounts ?? {});
11
+ return accountIds.some((accountId) => normalizeAccountId(accountId) !== DEFAULT_ACCOUNT_ID);
12
+ }
13
+
14
+ function isActivationOnlyEntry(
15
+ entry:
16
+ | {
17
+ groupActivation?: unknown;
18
+ sessionId?: unknown;
19
+ updatedAt?: unknown;
20
+ }
21
+ | undefined,
22
+ ) {
23
+ return (
24
+ entry?.groupActivation !== undefined &&
25
+ typeof entry?.sessionId !== "string" &&
26
+ typeof entry?.updatedAt !== "number"
27
+ );
28
+ }
29
+
30
+ export async function resolveGroupActivationFor(params: {
31
+ cfg: AutoBotConfig;
32
+ accountId?: string | null;
33
+ agentId: string;
34
+ sessionKey: string;
35
+ conversationId: string;
36
+ }) {
37
+ const storePath = resolveStorePath(params.cfg.session?.store, {
38
+ agentId: params.agentId,
39
+ });
40
+ const store = loadSessionStore(storePath);
41
+ const legacySessionKey = resolveWhatsAppLegacyGroupSessionKey({
42
+ sessionKey: params.sessionKey,
43
+ accountId: params.accountId,
44
+ });
45
+ const legacyEntry = legacySessionKey ? store[legacySessionKey] : undefined;
46
+ const scopedEntry = store[params.sessionKey];
47
+ const normalizedAccountId = normalizeAccountId(params.accountId);
48
+ const ignoreScopedActivation =
49
+ normalizedAccountId === DEFAULT_ACCOUNT_ID &&
50
+ hasNamedWhatsAppAccounts(params.cfg) &&
51
+ isActivationOnlyEntry(scopedEntry);
52
+ const activation =
53
+ (ignoreScopedActivation ? undefined : scopedEntry?.groupActivation) ??
54
+ legacyEntry?.groupActivation;
55
+ if (activation !== undefined && scopedEntry?.groupActivation === undefined) {
56
+ await updateSessionStore(storePath, (nextStore) => {
57
+ const nextScopedEntry = nextStore[params.sessionKey];
58
+ if (nextScopedEntry?.groupActivation !== undefined) {
59
+ return;
60
+ }
61
+ nextStore[params.sessionKey] = {
62
+ ...nextScopedEntry,
63
+ groupActivation: activation,
64
+ };
65
+ });
66
+ }
67
+ const requireMention = resolveWhatsAppInboundPolicy({
68
+ cfg: params.cfg,
69
+ accountId: params.accountId,
70
+ }).resolveConversationRequireMention(params.conversationId);
71
+ const defaultActivation = !requireMention ? "always" : "mention";
72
+ return normalizeGroupActivation(activation) ?? defaultActivation;
73
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ implicitMentionKindWhen,
3
+ resolveInboundMentionDecision,
4
+ } from "autobot/plugin-sdk/channel-mention-gating";
5
+ export { hasControlCommand } from "autobot/plugin-sdk/command-detection";
6
+ export { createChannelHistoryWindow } from "autobot/plugin-sdk/reply-history";
7
+ export { parseActivationCommand } from "autobot/plugin-sdk/group-activation";
8
+ export { normalizeE164 } from "../../text-runtime.js";
@@ -0,0 +1,218 @@
1
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
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
+ createChannelHistoryWindow,
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: AutoBotConfig;
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
+ createChannelHistoryWindow({ historyMap: params.groupHistories }).record({
77
+ historyKey: params.groupHistoryKey,
78
+ limit: params.groupHistoryLimit,
79
+ entry: {
80
+ sender,
81
+ body: params.body ?? params.msg.body,
82
+ timestamp: params.msg.timestamp,
83
+ id: params.msg.id,
84
+ senderJid: senderIdentity.jid ?? params.msg.senderJid,
85
+ },
86
+ });
87
+ }
88
+
89
+ function skipGroupMessageAndStoreHistory(
90
+ params: ApplyGroupGatingParams,
91
+ verboseMessage: string,
92
+ body?: string,
93
+ ) {
94
+ params.logVerbose(verboseMessage);
95
+ recordPendingGroupHistoryEntry({
96
+ msg: params.msg,
97
+ body,
98
+ groupHistories: params.groupHistories,
99
+ groupHistoryKey: params.groupHistoryKey,
100
+ groupHistoryLimit: params.groupHistoryLimit,
101
+ });
102
+ return { shouldProcess: false } as const;
103
+ }
104
+
105
+ export async function applyGroupGating(params: ApplyGroupGatingParams) {
106
+ const sender = getSenderIdentity(params.msg);
107
+ const self = getSelfIdentity(params.msg, params.authDir);
108
+ const inboundPolicy = resolveWhatsAppInboundPolicy({
109
+ cfg: params.cfg,
110
+ accountId: params.msg.accountId,
111
+ selfE164: self.e164 ?? null,
112
+ });
113
+ const conversationGroupPolicy = inboundPolicy.resolveConversationGroupPolicy(
114
+ params.conversationId,
115
+ );
116
+ if (conversationGroupPolicy.allowlistEnabled && !conversationGroupPolicy.allowed) {
117
+ params.logVerbose(
118
+ `Dropping message from unregistered WhatsApp group ${params.conversationId}. Add the group JID to channels.whatsapp.groups, or add "*" there to admit all groups. Sender authorization still applies.`,
119
+ );
120
+ return { shouldProcess: false };
121
+ }
122
+
123
+ noteGroupMember(
124
+ params.groupMemberNames,
125
+ params.groupHistoryKey,
126
+ sender.e164 ?? undefined,
127
+ sender.name ?? undefined,
128
+ );
129
+
130
+ const baseMentionConfig = {
131
+ ...params.baseMentionConfig,
132
+ allowFrom: inboundPolicy.configuredAllowFrom,
133
+ };
134
+ const mentionConfig = {
135
+ ...buildMentionConfig(params.cfg, params.agentId),
136
+ allowFrom: inboundPolicy.configuredAllowFrom,
137
+ };
138
+ const mentionMsg =
139
+ params.mentionText !== undefined ? { ...params.msg, body: params.mentionText } : params.msg;
140
+ const commandBody = stripMentionsForCommand(
141
+ mentionMsg.body,
142
+ mentionConfig.mentionRegexes,
143
+ self.e164,
144
+ );
145
+ const activationCommand = parseActivationCommand(commandBody);
146
+ const owner = isOwnerSender(baseMentionConfig, params.msg);
147
+ const shouldBypassMention = owner && hasControlCommand(commandBody, params.cfg);
148
+
149
+ if (activationCommand.hasCommand && !owner) {
150
+ return skipGroupMessageAndStoreHistory(
151
+ params,
152
+ `Ignoring /activation from non-owner in group ${params.conversationId}`,
153
+ );
154
+ }
155
+
156
+ const mentionDebug = debugMention(mentionMsg, mentionConfig, params.authDir);
157
+ params.replyLogger.debug(
158
+ {
159
+ conversationId: params.conversationId,
160
+ wasMentioned: mentionDebug.wasMentioned,
161
+ ...mentionDebug.details,
162
+ },
163
+ "group mention debug",
164
+ );
165
+ const wasMentioned = mentionDebug.wasMentioned;
166
+ const activation = await resolveGroupActivationFor({
167
+ cfg: params.cfg,
168
+ accountId: inboundPolicy.account.accountId,
169
+ agentId: params.agentId,
170
+ sessionKey: params.sessionKey,
171
+ conversationId: params.conversationId,
172
+ });
173
+ const requireMention = activation !== "always";
174
+ const replyContext = getReplyContext(params.msg, params.authDir);
175
+ const sharedNumberSelfChat = params.selfChatMode === true;
176
+ // Detect reply-to-bot: compare JIDs, LIDs, and E.164 numbers.
177
+ // WhatsApp may report the quoted message sender as either a phone JID
178
+ // (xxxxx@s.whatsapp.net) or a LID (xxxxx@lid), so we compare both.
179
+ // But in shared-number/selfChatMode setups, replies from the same self number
180
+ // should not count as implicit bot mentions unless the message explicitly
181
+ // mentioned the bot in text.
182
+ const implicitReplyToSelf = sharedNumberSelfChat && identitiesOverlap(self, sender);
183
+ const implicitMentionKinds = implicitMentionKindWhen(
184
+ "quoted_bot",
185
+ !implicitReplyToSelf && identitiesOverlap(self, replyContext?.sender),
186
+ );
187
+ const mentionDecision = resolveInboundMentionDecision({
188
+ facts: {
189
+ canDetectMention: true,
190
+ wasMentioned,
191
+ implicitMentionKinds,
192
+ },
193
+ policy: {
194
+ isGroup: true,
195
+ requireMention,
196
+ allowTextCommands: false,
197
+ hasControlCommand: false,
198
+ commandAuthorized: false,
199
+ },
200
+ });
201
+ const effectiveWasMentioned = mentionDecision.effectiveWasMentioned || shouldBypassMention;
202
+ params.msg.wasMentioned = effectiveWasMentioned;
203
+ if (!shouldBypassMention && requireMention && mentionDecision.shouldSkip) {
204
+ if (params.deferMissingMention === true) {
205
+ params.logVerbose(
206
+ `Deferring group mention skip until audio preflight completes in ${params.conversationId}`,
207
+ );
208
+ return { shouldProcess: false, needsMentionText: true } as const;
209
+ }
210
+ return skipGroupMessageAndStoreHistory(
211
+ params,
212
+ `Group message stored for context (no mention detected) in ${params.conversationId}: ${mentionMsg.body}`,
213
+ params.mentionText,
214
+ );
215
+ }
216
+
217
+ return { shouldProcess: true };
218
+ }
@@ -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,92 @@
1
+ import {
2
+ evaluateSupplementalContextVisibility,
3
+ filterSupplementalContextItems,
4
+ } from "autobot/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
+ }