@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.
- package/action-runtime-api.ts +1 -0
- package/action-runtime.runtime.ts +1 -0
- package/api.ts +67 -0
- package/auth-presence.ts +80 -0
- package/autobot.plugin.json +23 -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/doctor-contract-api.ts +8 -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/outbound-payload-test-api.ts +1 -0
- package/package.json +76 -0
- package/runtime-api.ts +84 -0
- package/secret-contract-api.ts +4 -0
- package/security-contract-api.ts +4 -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 +17 -0
- package/src/account-types.ts +5 -0
- package/src/accounts.ts +176 -0
- package/src/action-runtime-target-auth.ts +27 -0
- package/src/action-runtime.ts +76 -0
- package/src/active-listener.ts +17 -0
- package/src/agent-tools-login.ts +113 -0
- package/src/approval-auth.ts +27 -0
- package/src/auth-store.runtime.ts +1 -0
- package/src/auth-store.ts +494 -0
- package/src/auto-reply/config.runtime.ts +16 -0
- package/src/auto-reply/constants.ts +1 -0
- package/src/auto-reply/deliver-reply.ts +332 -0
- package/src/auto-reply/loggers.ts +6 -0
- package/src/auto-reply/mentions.ts +131 -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.ts +73 -0
- package/src/auto-reply/monitor/group-gating.runtime.ts +8 -0
- package/src/auto-reply/monitor/group-gating.ts +218 -0
- package/src/auto-reply/monitor/group-members.ts +65 -0
- package/src/auto-reply/monitor/inbound-context.ts +92 -0
- package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +22 -0
- package/src/auto-reply/monitor/inbound-dispatch.ts +749 -0
- package/src/auto-reply/monitor/last-route.ts +61 -0
- package/src/auto-reply/monitor/listener-log.ts +28 -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.ts +333 -0
- package/src/auto-reply/monitor/peer.ts +17 -0
- package/src/auto-reply/monitor/process-message.ts +584 -0
- package/src/auto-reply/monitor/runtime-api.ts +36 -0
- package/src/auto-reply/monitor/status-reaction.ts +108 -0
- package/src/auto-reply/monitor-state.ts +114 -0
- package/src/auto-reply/monitor.ts +720 -0
- package/src/auto-reply/reply-resolver.runtime.ts +1 -0
- package/src/auto-reply/types.ts +48 -0
- package/src/auto-reply/util.ts +62 -0
- package/src/auto-reply.impl.ts +6 -0
- package/src/auto-reply.ts +1 -0
- package/src/channel-actions.runtime.ts +7 -0
- package/src/channel-actions.ts +85 -0
- package/src/channel-outbound.ts +87 -0
- package/src/channel-react-action.runtime.ts +10 -0
- package/src/channel-react-action.ts +247 -0
- package/src/channel.runtime.ts +117 -0
- package/src/channel.setup.ts +32 -0
- package/src/channel.ts +356 -0
- package/src/command-policy.ts +7 -0
- package/src/config-accessors.ts +22 -0
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +24 -0
- package/src/connection-controller-registry.ts +49 -0
- package/src/connection-controller.ts +680 -0
- package/src/creds-files.ts +19 -0
- package/src/creds-persistence.ts +71 -0
- package/src/directory-config.ts +40 -0
- package/src/doctor-contract.ts +11 -0
- package/src/doctor.ts +56 -0
- package/src/document-filename.ts +17 -0
- package/src/group-intro.ts +15 -0
- package/src/group-policy.ts +40 -0
- package/src/group-session-contract.ts +20 -0
- package/src/group-session-key.ts +42 -0
- package/src/heartbeat.ts +34 -0
- package/src/identity.ts +164 -0
- package/src/inbound/access-control.ts +187 -0
- package/src/inbound/dedupe.ts +132 -0
- package/src/inbound/extract.ts +484 -0
- package/src/inbound/lifecycle.ts +39 -0
- package/src/inbound/media.ts +128 -0
- package/src/inbound/monitor.ts +1042 -0
- package/src/inbound/outbound-mentions.ts +260 -0
- package/src/inbound/runtime-api.ts +7 -0
- package/src/inbound/save-media.runtime.ts +1 -0
- package/src/inbound/send-api.ts +203 -0
- package/src/inbound/send-result.ts +109 -0
- package/src/inbound/types.ts +107 -0
- package/src/inbound-policy.ts +215 -0
- package/src/inbound.ts +9 -0
- package/src/login-qr.ts +542 -0
- package/src/login.ts +83 -0
- package/src/media.ts +10 -0
- package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +417 -0
- package/src/monitor-inbox.append-upsert.test-support.ts +133 -0
- package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +418 -0
- package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +308 -0
- package/src/monitor-inbox.streams-inbound-messages.test-support.ts +824 -0
- package/src/normalize-target.ts +148 -0
- package/src/normalize.ts +8 -0
- package/src/outbound-adapter.ts +36 -0
- package/src/outbound-base.ts +256 -0
- package/src/outbound-media-contract.ts +307 -0
- package/src/outbound-media.runtime.ts +41 -0
- package/src/outbound-send-deps.ts +1 -0
- package/src/outbound-test-support.ts +16 -0
- package/src/qa-driver.runtime.ts +189 -0
- package/src/qr-image.ts +1 -0
- package/src/qr-terminal.ts +1 -0
- package/src/quoted-message.ts +184 -0
- package/src/reaction-level.ts +24 -0
- package/src/reconnect.ts +55 -0
- package/src/resolve-outbound-target.ts +58 -0
- package/src/runtime-api.ts +59 -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.ts +342 -0
- package/src/session-contract.ts +43 -0
- package/src/session-errors.ts +125 -0
- package/src/session-route.ts +32 -0
- package/src/session.runtime.ts +8 -0
- package/src/session.ts +327 -0
- package/src/setup-core.ts +52 -0
- package/src/setup-finalize.ts +450 -0
- package/src/setup-surface.ts +71 -0
- package/src/setup-test-helpers.ts +217 -0
- package/src/shared.ts +291 -0
- package/src/socket-timing.ts +38 -0
- package/src/state-migrations.ts +55 -0
- package/src/status-issues.ts +185 -0
- package/src/system-prompt.ts +31 -0
- package/src/targets-runtime.ts +221 -0
- package/src/text-runtime.ts +18 -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,217 @@
|
|
|
1
|
+
import { expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
type WhatsAppSetupConfig = {
|
|
4
|
+
channels?: {
|
|
5
|
+
whatsapp?: {
|
|
6
|
+
selfChatMode?: boolean;
|
|
7
|
+
dmPolicy?: string;
|
|
8
|
+
allowFrom?: string[];
|
|
9
|
+
accounts?: Record<string, { dmPolicy?: string; allowFrom?: string[]; authDir?: string }>;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type WizardPromptHarness = {
|
|
15
|
+
text: (...args: unknown[]) => unknown;
|
|
16
|
+
select: (...args: unknown[]) => unknown;
|
|
17
|
+
note: (...args: unknown[]) => unknown;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type QueuedWizardPrompterFactory<T extends WizardPromptHarness> = (params: {
|
|
21
|
+
confirmValues?: boolean[];
|
|
22
|
+
selectValues?: string[];
|
|
23
|
+
textValues?: string[];
|
|
24
|
+
}) => T;
|
|
25
|
+
|
|
26
|
+
const WHATSAPP_OWNER_NUMBER_INPUT = "+1 (555) 555-0123";
|
|
27
|
+
const WHATSAPP_OWNER_NUMBER_E164 = "+15555550123";
|
|
28
|
+
const WHATSAPP_OWNER_NUMBER = "15555550123";
|
|
29
|
+
const WHATSAPP_PERSONAL_NUMBER_INPUT = "+1 (555) 111-2222";
|
|
30
|
+
const WHATSAPP_PERSONAL_NUMBER = "15551112222";
|
|
31
|
+
const WHATSAPP_ACCESS_NOTE_TITLE = "WhatsApp DM access";
|
|
32
|
+
const WHATSAPP_LOGIN_NOTE_TITLE = "WhatsApp";
|
|
33
|
+
|
|
34
|
+
export function createWhatsAppRootAllowFromConfig(): WhatsAppSetupConfig {
|
|
35
|
+
return {
|
|
36
|
+
channels: {
|
|
37
|
+
whatsapp: {
|
|
38
|
+
allowFrom: [WHATSAPP_OWNER_NUMBER_E164],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createWhatsAppOwnerAllowlistHarness<T extends WizardPromptHarness>(
|
|
45
|
+
createPrompter: QueuedWizardPrompterFactory<T>,
|
|
46
|
+
): T {
|
|
47
|
+
return createPrompter({
|
|
48
|
+
confirmValues: [false],
|
|
49
|
+
textValues: [WHATSAPP_OWNER_NUMBER_INPUT],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createWhatsAppPersonalPhoneHarness<T extends WizardPromptHarness>(
|
|
54
|
+
createPrompter: QueuedWizardPrompterFactory<T>,
|
|
55
|
+
): T {
|
|
56
|
+
return createPrompter({
|
|
57
|
+
confirmValues: [false],
|
|
58
|
+
selectValues: ["personal"],
|
|
59
|
+
textValues: [WHATSAPP_PERSONAL_NUMBER_INPUT],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createWhatsAppLinkingHarness<T extends WizardPromptHarness>(
|
|
64
|
+
createPrompter: QueuedWizardPrompterFactory<T>,
|
|
65
|
+
): T {
|
|
66
|
+
return createPrompter({
|
|
67
|
+
confirmValues: [true],
|
|
68
|
+
selectValues: ["separate", "disabled"],
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createWhatsAppWorkAccountConfig(
|
|
73
|
+
params: {
|
|
74
|
+
defaultAccount?: string;
|
|
75
|
+
} = {},
|
|
76
|
+
): WhatsAppSetupConfig {
|
|
77
|
+
return {
|
|
78
|
+
channels: {
|
|
79
|
+
whatsapp: {
|
|
80
|
+
...(params.defaultAccount ? { defaultAccount: params.defaultAccount } : {}),
|
|
81
|
+
dmPolicy: "disabled",
|
|
82
|
+
allowFrom: [WHATSAPP_OWNER_NUMBER_E164],
|
|
83
|
+
accounts: {
|
|
84
|
+
work: {
|
|
85
|
+
authDir: "/tmp/work",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createWhatsAppAllowlistModeInput(): {
|
|
94
|
+
selectValues: string[];
|
|
95
|
+
textValues: string[];
|
|
96
|
+
} {
|
|
97
|
+
return {
|
|
98
|
+
selectValues: ["separate", "allowlist", "list"],
|
|
99
|
+
textValues: [`${WHATSAPP_OWNER_NUMBER_INPUT}, ${WHATSAPP_OWNER_NUMBER}, *`],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function expectWhatsAppDmAccess(
|
|
104
|
+
cfg: WhatsAppSetupConfig,
|
|
105
|
+
expected: {
|
|
106
|
+
selfChatMode: boolean;
|
|
107
|
+
dmPolicy: string;
|
|
108
|
+
allowFrom?: string[];
|
|
109
|
+
},
|
|
110
|
+
): void {
|
|
111
|
+
expect(cfg.channels?.whatsapp?.selfChatMode).toBe(expected.selfChatMode);
|
|
112
|
+
expect(cfg.channels?.whatsapp?.dmPolicy).toBe(expected.dmPolicy);
|
|
113
|
+
if ("allowFrom" in expected) {
|
|
114
|
+
expect(cfg.channels?.whatsapp?.allowFrom).toEqual(expected.allowFrom);
|
|
115
|
+
} else {
|
|
116
|
+
expect(cfg.channels?.whatsapp?.allowFrom).toBeUndefined();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function expectWhatsAppWorkAccountOpenAccess(cfg: WhatsAppSetupConfig): void {
|
|
121
|
+
expect(cfg.channels?.whatsapp?.dmPolicy).toBe("disabled");
|
|
122
|
+
expect(cfg.channels?.whatsapp?.allowFrom).toEqual([WHATSAPP_OWNER_NUMBER_E164]);
|
|
123
|
+
expect(cfg.channels?.whatsapp?.accounts?.work?.dmPolicy).toBe("open");
|
|
124
|
+
expect(cfg.channels?.whatsapp?.accounts?.work?.allowFrom).toEqual(["*", WHATSAPP_OWNER_NUMBER]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function expectWhatsAppOwnerNumberPrompt(harness: WizardPromptHarness): void {
|
|
128
|
+
expect(harness.text).toHaveBeenCalledWith(
|
|
129
|
+
expect.objectContaining({
|
|
130
|
+
message: "Your personal WhatsApp number (the phone you will message from)",
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function expectWhatsAppOwnerAllowlistSetup(
|
|
136
|
+
cfg: WhatsAppSetupConfig,
|
|
137
|
+
harness: WizardPromptHarness,
|
|
138
|
+
): void {
|
|
139
|
+
expectWhatsAppDmAccess(cfg, {
|
|
140
|
+
selfChatMode: true,
|
|
141
|
+
dmPolicy: "allowlist",
|
|
142
|
+
allowFrom: [WHATSAPP_OWNER_NUMBER],
|
|
143
|
+
});
|
|
144
|
+
expectWhatsAppOwnerNumberPrompt(harness);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function expectWhatsAppSeparatePhoneDisabledSetup(
|
|
148
|
+
cfg: WhatsAppSetupConfig,
|
|
149
|
+
harness: WizardPromptHarness,
|
|
150
|
+
): void {
|
|
151
|
+
expectWhatsAppDmAccess(cfg, {
|
|
152
|
+
selfChatMode: false,
|
|
153
|
+
dmPolicy: "disabled",
|
|
154
|
+
});
|
|
155
|
+
expect(harness.text).not.toHaveBeenCalled();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function expectWhatsAppAllowlistModeSetup(cfg: WhatsAppSetupConfig): void {
|
|
159
|
+
expectWhatsAppDmAccess(cfg, {
|
|
160
|
+
selfChatMode: false,
|
|
161
|
+
dmPolicy: "allowlist",
|
|
162
|
+
allowFrom: [WHATSAPP_OWNER_NUMBER, "*"],
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function expectWhatsAppPersonalPhoneSetup(cfg: WhatsAppSetupConfig): void {
|
|
167
|
+
expectWhatsAppDmAccess(cfg, {
|
|
168
|
+
selfChatMode: true,
|
|
169
|
+
dmPolicy: "allowlist",
|
|
170
|
+
allowFrom: [WHATSAPP_PERSONAL_NUMBER],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function expectWhatsAppOpenPolicySetup(
|
|
175
|
+
cfg: WhatsAppSetupConfig,
|
|
176
|
+
harness: WizardPromptHarness,
|
|
177
|
+
): void {
|
|
178
|
+
expectWhatsAppDmAccess(cfg, {
|
|
179
|
+
selfChatMode: false,
|
|
180
|
+
dmPolicy: "open",
|
|
181
|
+
allowFrom: ["*", WHATSAPP_OWNER_NUMBER],
|
|
182
|
+
});
|
|
183
|
+
expect(harness.select).toHaveBeenCalledTimes(2);
|
|
184
|
+
expect(harness.text).not.toHaveBeenCalled();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function expectNoWhatsAppLoginFollowup(harness: WizardPromptHarness): void {
|
|
188
|
+
expect(harness.note).not.toHaveBeenCalledWith(
|
|
189
|
+
expect.stringContaining("autobot channels login"),
|
|
190
|
+
WHATSAPP_LOGIN_NOTE_TITLE,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function expectWhatsAppLoginFollowup(harness: WizardPromptHarness): void {
|
|
195
|
+
expect(harness.note).toHaveBeenCalledWith(
|
|
196
|
+
expect.stringContaining("autobot channels login"),
|
|
197
|
+
WHATSAPP_LOGIN_NOTE_TITLE,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function expectWhatsAppWorkAccountAccessNote(harness: WizardPromptHarness): void {
|
|
202
|
+
expect(harness.note).toHaveBeenCalledWith(
|
|
203
|
+
expect.stringContaining(
|
|
204
|
+
"`channels.whatsapp.accounts.work.dmPolicy` + `channels.whatsapp.accounts.work.allowFrom`",
|
|
205
|
+
),
|
|
206
|
+
WHATSAPP_ACCESS_NOTE_TITLE,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function expectWhatsAppDefaultAccountAccessNote(harness: WizardPromptHarness): void {
|
|
211
|
+
expect(harness.note).toHaveBeenCalledWith(
|
|
212
|
+
expect.stringContaining(
|
|
213
|
+
"`channels.whatsapp.accounts.default.dmPolicy` + `channels.whatsapp.accounts.default.allowFrom`",
|
|
214
|
+
),
|
|
215
|
+
WHATSAPP_ACCESS_NOTE_TITLE,
|
|
216
|
+
);
|
|
217
|
+
}
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID } from "autobot/plugin-sdk/account-core";
|
|
2
|
+
import { describeAccountSnapshot } from "autobot/plugin-sdk/account-helpers";
|
|
3
|
+
import { normalizeE164 } from "autobot/plugin-sdk/account-resolution";
|
|
4
|
+
import {
|
|
5
|
+
adaptScopedAccountAccessor,
|
|
6
|
+
createScopedChannelConfigAdapter,
|
|
7
|
+
createScopedDmSecurityResolver,
|
|
8
|
+
} from "autobot/plugin-sdk/channel-config-helpers";
|
|
9
|
+
import {
|
|
10
|
+
collectOpenGroupPolicyRouteAllowlistWarnings,
|
|
11
|
+
createAllowlistProviderGroupPolicyWarningCollector,
|
|
12
|
+
} from "autobot/plugin-sdk/channel-policy";
|
|
13
|
+
import type { ChannelPlugin } from "autobot/plugin-sdk/core";
|
|
14
|
+
import { createChannelPluginBase, getChatChannelMeta } from "autobot/plugin-sdk/core";
|
|
15
|
+
import {
|
|
16
|
+
createDelegatedSetupWizardProxy,
|
|
17
|
+
type ChannelSetupWizard,
|
|
18
|
+
} from "autobot/plugin-sdk/setup-runtime";
|
|
19
|
+
import {
|
|
20
|
+
hasAnyWhatsAppAuth,
|
|
21
|
+
listWhatsAppAccountIds,
|
|
22
|
+
resolveDefaultWhatsAppAccountId,
|
|
23
|
+
resolveWhatsAppAccount,
|
|
24
|
+
type ResolvedWhatsAppAccount,
|
|
25
|
+
} from "./accounts.js";
|
|
26
|
+
import { formatWhatsAppConfigAllowFromEntries } from "./config-accessors.js";
|
|
27
|
+
import { WhatsAppChannelConfigSchema } from "./config-schema.js";
|
|
28
|
+
import { whatsappDoctor } from "./doctor.js";
|
|
29
|
+
import { resolveLegacyGroupSessionKey } from "./group-session-contract.js";
|
|
30
|
+
import {
|
|
31
|
+
collectUnsupportedSecretRefConfigCandidates,
|
|
32
|
+
unsupportedSecretRefSurfacePatterns,
|
|
33
|
+
} from "./security-contract.js";
|
|
34
|
+
import { applyWhatsAppSecurityConfigFixes } from "./security-fix.js";
|
|
35
|
+
import {
|
|
36
|
+
canonicalizeLegacySessionKey,
|
|
37
|
+
deriveLegacySessionChatType,
|
|
38
|
+
isLegacyGroupSessionKey,
|
|
39
|
+
} from "./session-contract.js";
|
|
40
|
+
|
|
41
|
+
const WHATSAPP_CHANNEL = "whatsapp" as const;
|
|
42
|
+
|
|
43
|
+
const WHATSAPP_GROUP_SCOPE_FIELDS = ["groupPolicy", "groupAllowFrom", "groups"] as const;
|
|
44
|
+
|
|
45
|
+
type WhatsAppGroupScopeField = (typeof WHATSAPP_GROUP_SCOPE_FIELDS)[number];
|
|
46
|
+
|
|
47
|
+
function resolveWhatsAppAccountKey(
|
|
48
|
+
accounts: Record<string, unknown> | undefined,
|
|
49
|
+
accountId: string,
|
|
50
|
+
): string | undefined {
|
|
51
|
+
if (!accounts) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if (Object.hasOwn(accounts, accountId)) {
|
|
55
|
+
return accountId;
|
|
56
|
+
}
|
|
57
|
+
const normalizedAccountId = accountId.trim().toLowerCase();
|
|
58
|
+
return Object.keys(accounts).find((key) => key.trim().toLowerCase() === normalizedAccountId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveWhatsAppGroupScopeBasePath(params: {
|
|
62
|
+
cfg: Parameters<typeof resolveWhatsAppAccount>[0]["cfg"];
|
|
63
|
+
accountId?: string | null;
|
|
64
|
+
}): string {
|
|
65
|
+
const accountId =
|
|
66
|
+
typeof params.accountId === "string"
|
|
67
|
+
? params.accountId.trim() || DEFAULT_ACCOUNT_ID
|
|
68
|
+
: DEFAULT_ACCOUNT_ID;
|
|
69
|
+
const accounts = params.cfg.channels?.whatsapp?.accounts;
|
|
70
|
+
const accountKey = resolveWhatsAppAccountKey(accounts, accountId);
|
|
71
|
+
const defaultAccountKey = resolveWhatsAppAccountKey(accounts, DEFAULT_ACCOUNT_ID);
|
|
72
|
+
const accountConfig = accountKey ? accounts?.[accountKey] : undefined;
|
|
73
|
+
const defaultAccountConfig = defaultAccountKey ? accounts?.[defaultAccountKey] : undefined;
|
|
74
|
+
const matchesAnyGroupScopeField = (config: Record<string, unknown> | undefined): boolean =>
|
|
75
|
+
WHATSAPP_GROUP_SCOPE_FIELDS.some((field) => config?.[field] !== undefined);
|
|
76
|
+
if (matchesAnyGroupScopeField(accountConfig)) {
|
|
77
|
+
return `channels.whatsapp.accounts.${accountKey}`;
|
|
78
|
+
}
|
|
79
|
+
if (accountId !== DEFAULT_ACCOUNT_ID && matchesAnyGroupScopeField(defaultAccountConfig)) {
|
|
80
|
+
return `channels.whatsapp.accounts.${defaultAccountKey}`;
|
|
81
|
+
}
|
|
82
|
+
return "channels.whatsapp";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveWhatsAppConfigPath(params: {
|
|
86
|
+
cfg: Parameters<typeof resolveWhatsAppAccount>[0]["cfg"];
|
|
87
|
+
accountId?: string | null;
|
|
88
|
+
field: WhatsAppGroupScopeField;
|
|
89
|
+
}): string {
|
|
90
|
+
return `${resolveWhatsAppGroupScopeBasePath(params)}.${params.field}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function loadWhatsAppChannelRuntime() {
|
|
94
|
+
return await import("./channel.runtime.js");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function loadWhatsAppSetupSurface() {
|
|
98
|
+
return await import("./setup-surface.js");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const whatsappSetupWizardProxy = createWhatsAppSetupWizardProxy(
|
|
102
|
+
async () => (await loadWhatsAppSetupSurface()).whatsappSetupWizard,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const whatsappConfigAdapter = createScopedChannelConfigAdapter<ResolvedWhatsAppAccount>({
|
|
106
|
+
sectionKey: WHATSAPP_CHANNEL,
|
|
107
|
+
listAccountIds: listWhatsAppAccountIds,
|
|
108
|
+
resolveAccount: adaptScopedAccountAccessor(resolveWhatsAppAccount),
|
|
109
|
+
defaultAccountId: resolveDefaultWhatsAppAccountId,
|
|
110
|
+
clearBaseFields: [],
|
|
111
|
+
allowTopLevel: false,
|
|
112
|
+
resolveAllowFrom: (account) => account.allowFrom,
|
|
113
|
+
formatAllowFrom: (allowFrom) => formatWhatsAppConfigAllowFromEntries(allowFrom),
|
|
114
|
+
resolveDefaultTo: (account) => account.defaultTo,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const whatsappResolveDmPolicy = createScopedDmSecurityResolver<ResolvedWhatsAppAccount>({
|
|
118
|
+
channelKey: WHATSAPP_CHANNEL,
|
|
119
|
+
resolvePolicy: (account) => account.dmPolicy,
|
|
120
|
+
resolveAllowFrom: (account) => account.allowFrom,
|
|
121
|
+
policyPathSuffix: "dmPolicy",
|
|
122
|
+
normalizeEntry: (raw) => normalizeE164(raw),
|
|
123
|
+
inheritSharedDefaultsFromDefaultAccount: true,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function createWhatsAppSetupWizardProxy(
|
|
127
|
+
loadWizard: () => Promise<ChannelSetupWizard>,
|
|
128
|
+
): ChannelSetupWizard {
|
|
129
|
+
return createDelegatedSetupWizardProxy({
|
|
130
|
+
channel: WHATSAPP_CHANNEL,
|
|
131
|
+
loadWizard,
|
|
132
|
+
status: {
|
|
133
|
+
configuredLabel: "linked",
|
|
134
|
+
unconfiguredLabel: "not linked",
|
|
135
|
+
configuredHint: "linked",
|
|
136
|
+
unconfiguredHint: "not linked",
|
|
137
|
+
configuredScore: 5,
|
|
138
|
+
unconfiguredScore: 4,
|
|
139
|
+
},
|
|
140
|
+
resolveShouldPromptAccountIds: (params) => params.shouldPromptAccountIds,
|
|
141
|
+
credentials: [],
|
|
142
|
+
delegateFinalize: true,
|
|
143
|
+
disable: (cfg) => ({
|
|
144
|
+
...cfg,
|
|
145
|
+
channels: {
|
|
146
|
+
...cfg.channels,
|
|
147
|
+
whatsapp: {
|
|
148
|
+
...cfg.channels?.whatsapp,
|
|
149
|
+
enabled: false,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
onAccountRecorded: (accountId, options) => {
|
|
154
|
+
options?.onAccountId?.(WHATSAPP_CHANNEL, accountId);
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function createWhatsAppPluginBase(params: {
|
|
160
|
+
groups: NonNullable<ChannelPlugin<ResolvedWhatsAppAccount>["groups"]>;
|
|
161
|
+
setupWizard: NonNullable<ChannelPlugin<ResolvedWhatsAppAccount>["setupWizard"]>;
|
|
162
|
+
setup: NonNullable<ChannelPlugin<ResolvedWhatsAppAccount>["setup"]>;
|
|
163
|
+
isConfigured: NonNullable<ChannelPlugin<ResolvedWhatsAppAccount>["config"]>["isConfigured"];
|
|
164
|
+
}) {
|
|
165
|
+
const collectWhatsAppSecurityWarnings = createAllowlistProviderGroupPolicyWarningCollector<{
|
|
166
|
+
account: ResolvedWhatsAppAccount;
|
|
167
|
+
cfg: Parameters<typeof resolveWhatsAppAccount>[0]["cfg"];
|
|
168
|
+
accountId?: string | null;
|
|
169
|
+
}>({
|
|
170
|
+
providerConfigPresent: (cfg) => cfg.channels?.whatsapp !== undefined,
|
|
171
|
+
resolveGroupPolicy: ({ account }) => account.groupPolicy,
|
|
172
|
+
collect: ({ account, accountId, cfg, groupPolicy }) =>
|
|
173
|
+
collectOpenGroupPolicyRouteAllowlistWarnings({
|
|
174
|
+
groupPolicy,
|
|
175
|
+
routeAllowlistConfigured:
|
|
176
|
+
Boolean(account.groups) && Object.keys(account.groups ?? {}).length > 0,
|
|
177
|
+
restrictSenders: {
|
|
178
|
+
surface: "WhatsApp groups",
|
|
179
|
+
openScope: "any member in allowed groups",
|
|
180
|
+
groupPolicyPath: resolveWhatsAppConfigPath({ cfg, accountId, field: "groupPolicy" }),
|
|
181
|
+
groupAllowFromPath: resolveWhatsAppConfigPath({
|
|
182
|
+
cfg,
|
|
183
|
+
accountId,
|
|
184
|
+
field: "groupAllowFrom",
|
|
185
|
+
}),
|
|
186
|
+
},
|
|
187
|
+
noRouteAllowlist: {
|
|
188
|
+
surface: "WhatsApp groups",
|
|
189
|
+
routeAllowlistPath: resolveWhatsAppConfigPath({ cfg, accountId, field: "groups" }),
|
|
190
|
+
routeScope: "group",
|
|
191
|
+
groupPolicyPath: resolveWhatsAppConfigPath({ cfg, accountId, field: "groupPolicy" }),
|
|
192
|
+
groupAllowFromPath: resolveWhatsAppConfigPath({
|
|
193
|
+
cfg,
|
|
194
|
+
accountId,
|
|
195
|
+
field: "groupAllowFrom",
|
|
196
|
+
}),
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
199
|
+
});
|
|
200
|
+
const base = createChannelPluginBase({
|
|
201
|
+
id: WHATSAPP_CHANNEL,
|
|
202
|
+
meta: {
|
|
203
|
+
...getChatChannelMeta(WHATSAPP_CHANNEL),
|
|
204
|
+
showConfigured: false,
|
|
205
|
+
quickstartAllowFrom: true,
|
|
206
|
+
forceAccountBinding: true,
|
|
207
|
+
preferSessionLookupForAnnounceTarget: true,
|
|
208
|
+
},
|
|
209
|
+
setupWizard: params.setupWizard,
|
|
210
|
+
capabilities: {
|
|
211
|
+
chatTypes: ["direct", "group", "channel"],
|
|
212
|
+
polls: true,
|
|
213
|
+
reactions: true,
|
|
214
|
+
media: true,
|
|
215
|
+
tts: {
|
|
216
|
+
voice: {
|
|
217
|
+
synthesisTarget: "voice-note",
|
|
218
|
+
transcodesAudio: true,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
reload: { configPrefixes: ["web"], noopPrefixes: ["channels.whatsapp"] },
|
|
223
|
+
gatewayMethodDescriptors: [{ name: "web.login.start" }, { name: "web.login.wait" }],
|
|
224
|
+
configSchema: WhatsAppChannelConfigSchema,
|
|
225
|
+
config: {
|
|
226
|
+
...whatsappConfigAdapter,
|
|
227
|
+
isEnabled: (account, cfg) => account.enabled && cfg.web?.enabled !== false,
|
|
228
|
+
disabledReason: () => "disabled",
|
|
229
|
+
isConfigured: params.isConfigured,
|
|
230
|
+
hasPersistedAuthState: ({ cfg }) => hasAnyWhatsAppAuth(cfg),
|
|
231
|
+
unconfiguredReason: () => "not linked",
|
|
232
|
+
describeAccount: (account) =>
|
|
233
|
+
describeAccountSnapshot({
|
|
234
|
+
account,
|
|
235
|
+
configured: Boolean(account.authDir),
|
|
236
|
+
extra: {
|
|
237
|
+
linked: Boolean(account.authDir),
|
|
238
|
+
dmPolicy: account.dmPolicy,
|
|
239
|
+
allowFrom: account.allowFrom,
|
|
240
|
+
},
|
|
241
|
+
}),
|
|
242
|
+
},
|
|
243
|
+
security: {
|
|
244
|
+
applyConfigFixes: applyWhatsAppSecurityConfigFixes,
|
|
245
|
+
resolveDmPolicy: whatsappResolveDmPolicy,
|
|
246
|
+
collectWarnings: collectWhatsAppSecurityWarnings,
|
|
247
|
+
},
|
|
248
|
+
doctor: whatsappDoctor,
|
|
249
|
+
setup: params.setup,
|
|
250
|
+
groups: params.groups,
|
|
251
|
+
});
|
|
252
|
+
return {
|
|
253
|
+
...base,
|
|
254
|
+
setupWizard: base.setupWizard!,
|
|
255
|
+
capabilities: base.capabilities!,
|
|
256
|
+
reload: base.reload!,
|
|
257
|
+
gatewayMethodDescriptors: base.gatewayMethodDescriptors!,
|
|
258
|
+
configSchema: base.configSchema!,
|
|
259
|
+
config: base.config!,
|
|
260
|
+
messaging: {
|
|
261
|
+
defaultMarkdownTableMode: "bullets",
|
|
262
|
+
deriveLegacySessionChatType,
|
|
263
|
+
resolveLegacyGroupSessionKey,
|
|
264
|
+
isLegacyGroupSessionKey,
|
|
265
|
+
canonicalizeLegacySessionKey: (params) =>
|
|
266
|
+
canonicalizeLegacySessionKey({ key: params.key, agentId: params.agentId }),
|
|
267
|
+
},
|
|
268
|
+
secrets: {
|
|
269
|
+
unsupportedSecretRefSurfacePatterns,
|
|
270
|
+
collectUnsupportedSecretRefConfigCandidates,
|
|
271
|
+
},
|
|
272
|
+
security: base.security!,
|
|
273
|
+
groups: base.groups!,
|
|
274
|
+
} satisfies Pick<
|
|
275
|
+
ChannelPlugin<ResolvedWhatsAppAccount>,
|
|
276
|
+
| "id"
|
|
277
|
+
| "meta"
|
|
278
|
+
| "setupWizard"
|
|
279
|
+
| "capabilities"
|
|
280
|
+
| "reload"
|
|
281
|
+
| "gatewayMethodDescriptors"
|
|
282
|
+
| "configSchema"
|
|
283
|
+
| "config"
|
|
284
|
+
| "messaging"
|
|
285
|
+
| "secrets"
|
|
286
|
+
| "security"
|
|
287
|
+
| "doctor"
|
|
288
|
+
| "setup"
|
|
289
|
+
| "groups"
|
|
290
|
+
>;
|
|
291
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
2
|
+
|
|
3
|
+
export type WhatsAppSocketTimingOptions = {
|
|
4
|
+
keepAliveIntervalMs?: number;
|
|
5
|
+
connectTimeoutMs?: number;
|
|
6
|
+
defaultQueryTimeoutMs?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_WHATSAPP_SOCKET_TIMING: Required<WhatsAppSocketTimingOptions> = {
|
|
10
|
+
keepAliveIntervalMs: 25_000,
|
|
11
|
+
connectTimeoutMs: 60_000,
|
|
12
|
+
defaultQueryTimeoutMs: 60_000,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function positiveInteger(value: number | undefined): number | undefined {
|
|
16
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveWhatsAppSocketTiming(
|
|
20
|
+
cfg: AutoBotConfig,
|
|
21
|
+
overrides?: WhatsAppSocketTimingOptions,
|
|
22
|
+
): Required<WhatsAppSocketTimingOptions> {
|
|
23
|
+
const configured = cfg.web?.whatsapp;
|
|
24
|
+
return {
|
|
25
|
+
keepAliveIntervalMs:
|
|
26
|
+
positiveInteger(overrides?.keepAliveIntervalMs) ??
|
|
27
|
+
positiveInteger(configured?.keepAliveIntervalMs) ??
|
|
28
|
+
DEFAULT_WHATSAPP_SOCKET_TIMING.keepAliveIntervalMs,
|
|
29
|
+
connectTimeoutMs:
|
|
30
|
+
positiveInteger(overrides?.connectTimeoutMs) ??
|
|
31
|
+
positiveInteger(configured?.connectTimeoutMs) ??
|
|
32
|
+
DEFAULT_WHATSAPP_SOCKET_TIMING.connectTimeoutMs,
|
|
33
|
+
defaultQueryTimeoutMs:
|
|
34
|
+
positiveInteger(overrides?.defaultQueryTimeoutMs) ??
|
|
35
|
+
positiveInteger(configured?.defaultQueryTimeoutMs) ??
|
|
36
|
+
DEFAULT_WHATSAPP_SOCKET_TIMING.defaultQueryTimeoutMs,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DEFAULT_ACCOUNT_ID } from "autobot/plugin-sdk/account-id";
|
|
4
|
+
import type { ChannelLegacyStateMigrationPlan } from "autobot/plugin-sdk/channel-contract";
|
|
5
|
+
import { statRegularFileSync } from "autobot/plugin-sdk/security-runtime";
|
|
6
|
+
|
|
7
|
+
function fileExists(pathValue: string): boolean {
|
|
8
|
+
try {
|
|
9
|
+
return !statRegularFileSync(pathValue).missing;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isLegacyWhatsAppAuthFile(name: string): boolean {
|
|
16
|
+
if (name === "creds.json" || name === "creds.json.bak") {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (!name.endsWith(".json")) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return /^(app-state-sync|session|sender-key|pre-key)-/.test(name);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function detectWhatsAppLegacyStateMigrations(params: {
|
|
26
|
+
oauthDir: string;
|
|
27
|
+
}): ChannelLegacyStateMigrationPlan[] {
|
|
28
|
+
const targetDir = path.join(params.oauthDir, "whatsapp", DEFAULT_ACCOUNT_ID);
|
|
29
|
+
const entries = (() => {
|
|
30
|
+
try {
|
|
31
|
+
return fs.readdirSync(params.oauthDir, { withFileTypes: true });
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
|
|
37
|
+
return entries.flatMap((entry) => {
|
|
38
|
+
if (!entry.isFile() || entry.name === "oauth.json" || !isLegacyWhatsAppAuthFile(entry.name)) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const sourcePath = path.join(params.oauthDir, entry.name);
|
|
42
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
43
|
+
if (fileExists(targetPath)) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
kind: "move" as const,
|
|
49
|
+
label: `WhatsApp auth ${entry.name}`,
|
|
50
|
+
sourcePath,
|
|
51
|
+
targetPath,
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
});
|
|
55
|
+
}
|