@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,450 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ACCOUNT_ID,
|
|
4
|
+
pathExists,
|
|
5
|
+
splitSetupEntries,
|
|
6
|
+
createSetupTranslator,
|
|
7
|
+
type DmPolicy,
|
|
8
|
+
type AutoBotConfig,
|
|
9
|
+
} from "autobot/plugin-sdk/setup";
|
|
10
|
+
import type { ChannelSetupWizard } from "autobot/plugin-sdk/setup";
|
|
11
|
+
import { formatCliCommand, formatDocsLink } from "autobot/plugin-sdk/setup-tools";
|
|
12
|
+
import {
|
|
13
|
+
resolveDefaultWhatsAppAccountId,
|
|
14
|
+
resolveWhatsAppAccount,
|
|
15
|
+
resolveWhatsAppAuthDir,
|
|
16
|
+
} from "./accounts.js";
|
|
17
|
+
import {
|
|
18
|
+
normalizeWhatsAppAllowFromEntries,
|
|
19
|
+
normalizeWhatsAppAllowFromEntry,
|
|
20
|
+
} from "./normalize-target.js";
|
|
21
|
+
import { whatsappSetupAdapter } from "./setup-core.js";
|
|
22
|
+
|
|
23
|
+
const t = createSetupTranslator();
|
|
24
|
+
|
|
25
|
+
type SetupPrompter = Parameters<NonNullable<ChannelSetupWizard["finalize"]>>[0]["prompter"];
|
|
26
|
+
type SetupRuntime = Parameters<NonNullable<ChannelSetupWizard["finalize"]>>[0]["runtime"];
|
|
27
|
+
type WhatsAppConfig = NonNullable<NonNullable<AutoBotConfig["channels"]>["whatsapp"]>;
|
|
28
|
+
type WhatsAppAccountConfig = NonNullable<NonNullable<WhatsAppConfig["accounts"]>[string]>;
|
|
29
|
+
|
|
30
|
+
function trimPromptText(value: string | null | undefined): string {
|
|
31
|
+
return value?.trim() ?? "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isDefaultWhatsAppAccountKey(accountId: string): boolean {
|
|
35
|
+
return accountId.trim().toLowerCase() === DEFAULT_ACCOUNT_ID;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function shouldWriteDefaultWhatsAppAccountConfigAtAccountScope(cfg: AutoBotConfig): boolean {
|
|
39
|
+
const accounts = cfg.channels?.whatsapp?.accounts;
|
|
40
|
+
if (!accounts) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (accounts.default) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return Object.keys(accounts).some((accountId) => !isDefaultWhatsAppAccountKey(accountId));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveDefaultWhatsAppAccountWriteKey(cfg: AutoBotConfig): string {
|
|
50
|
+
const accounts = cfg.channels?.whatsapp?.accounts;
|
|
51
|
+
if (!accounts) {
|
|
52
|
+
return DEFAULT_ACCOUNT_ID;
|
|
53
|
+
}
|
|
54
|
+
const match = Object.keys(accounts).find((accountId) => isDefaultWhatsAppAccountKey(accountId));
|
|
55
|
+
return match ?? DEFAULT_ACCOUNT_ID;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveWhatsAppConfigPathPrefix(cfg: AutoBotConfig, accountId: string): string {
|
|
59
|
+
if (
|
|
60
|
+
accountId === DEFAULT_ACCOUNT_ID &&
|
|
61
|
+
shouldWriteDefaultWhatsAppAccountConfigAtAccountScope(cfg)
|
|
62
|
+
) {
|
|
63
|
+
return `channels.whatsapp.accounts.${resolveDefaultWhatsAppAccountWriteKey(cfg)}`;
|
|
64
|
+
}
|
|
65
|
+
return accountId === DEFAULT_ACCOUNT_ID
|
|
66
|
+
? "channels.whatsapp"
|
|
67
|
+
: `channels.whatsapp.accounts.${accountId}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function mergeWhatsAppConfig(
|
|
71
|
+
cfg: AutoBotConfig,
|
|
72
|
+
accountId: string,
|
|
73
|
+
patch: Partial<WhatsAppAccountConfig>,
|
|
74
|
+
options?: { unsetOnUndefined?: string[] },
|
|
75
|
+
): AutoBotConfig {
|
|
76
|
+
const channelConfig: WhatsAppConfig = { ...cfg.channels?.whatsapp };
|
|
77
|
+
const mutableChannelConfig = channelConfig as Record<string, unknown>;
|
|
78
|
+
const targetPathPrefix = resolveWhatsAppConfigPathPrefix(cfg, accountId);
|
|
79
|
+
if (targetPathPrefix === "channels.whatsapp") {
|
|
80
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
81
|
+
if (value === undefined) {
|
|
82
|
+
if (options?.unsetOnUndefined?.includes(key)) {
|
|
83
|
+
delete mutableChannelConfig[key];
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
mutableChannelConfig[key] = value;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
...cfg,
|
|
91
|
+
channels: {
|
|
92
|
+
...cfg.channels,
|
|
93
|
+
whatsapp: channelConfig,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const accounts = {
|
|
98
|
+
...(channelConfig.accounts as Record<string, WhatsAppAccountConfig> | undefined),
|
|
99
|
+
};
|
|
100
|
+
const targetAccountId =
|
|
101
|
+
accountId === DEFAULT_ACCOUNT_ID ? resolveDefaultWhatsAppAccountWriteKey(cfg) : accountId;
|
|
102
|
+
const lowerDefaultAccount =
|
|
103
|
+
accountId === DEFAULT_ACCOUNT_ID && targetAccountId !== DEFAULT_ACCOUNT_ID
|
|
104
|
+
? accounts[DEFAULT_ACCOUNT_ID]
|
|
105
|
+
: undefined;
|
|
106
|
+
const nextAccount: WhatsAppAccountConfig = {
|
|
107
|
+
...accounts[targetAccountId],
|
|
108
|
+
...lowerDefaultAccount,
|
|
109
|
+
};
|
|
110
|
+
const mutableNextAccount = nextAccount as Record<string, unknown>;
|
|
111
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
112
|
+
if (value === undefined) {
|
|
113
|
+
if (options?.unsetOnUndefined?.includes(key)) {
|
|
114
|
+
delete mutableNextAccount[key];
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
mutableNextAccount[key] = value;
|
|
119
|
+
}
|
|
120
|
+
accounts[targetAccountId] = nextAccount;
|
|
121
|
+
if (lowerDefaultAccount) {
|
|
122
|
+
delete accounts[DEFAULT_ACCOUNT_ID];
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
...cfg,
|
|
126
|
+
channels: {
|
|
127
|
+
...cfg.channels,
|
|
128
|
+
whatsapp: {
|
|
129
|
+
...channelConfig,
|
|
130
|
+
accounts,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function setWhatsAppDmPolicy(
|
|
137
|
+
cfg: AutoBotConfig,
|
|
138
|
+
accountId: string,
|
|
139
|
+
dmPolicy: DmPolicy,
|
|
140
|
+
): AutoBotConfig {
|
|
141
|
+
return mergeWhatsAppConfig(cfg, accountId, { dmPolicy });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function setWhatsAppAllowFrom(
|
|
145
|
+
cfg: AutoBotConfig,
|
|
146
|
+
accountId: string,
|
|
147
|
+
allowFrom?: string[],
|
|
148
|
+
): AutoBotConfig {
|
|
149
|
+
return mergeWhatsAppConfig(cfg, accountId, { allowFrom }, { unsetOnUndefined: ["allowFrom"] });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function setWhatsAppSelfChatMode(
|
|
153
|
+
cfg: AutoBotConfig,
|
|
154
|
+
accountId: string,
|
|
155
|
+
selfChatMode: boolean,
|
|
156
|
+
): AutoBotConfig {
|
|
157
|
+
return mergeWhatsAppConfig(cfg, accountId, { selfChatMode });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function detectWhatsAppLinked(cfg: AutoBotConfig, accountId: string): Promise<boolean> {
|
|
161
|
+
const { authDir } = resolveWhatsAppAuthDir({ cfg, accountId });
|
|
162
|
+
const credsPath = path.join(authDir, "creds.json");
|
|
163
|
+
return await pathExists(credsPath);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function promptWhatsAppOwnerAllowFrom(params: {
|
|
167
|
+
existingAllowFrom: string[];
|
|
168
|
+
prompter: SetupPrompter;
|
|
169
|
+
}): Promise<{ normalized: string; allowFrom: string[] }> {
|
|
170
|
+
const { prompter, existingAllowFrom } = params;
|
|
171
|
+
|
|
172
|
+
await prompter.note(t("wizard.whatsapp.ownerNumberNote"), t("wizard.whatsapp.numberTitle"));
|
|
173
|
+
const entry = await prompter.text({
|
|
174
|
+
message: t("wizard.whatsapp.personalNumberPrompt"),
|
|
175
|
+
placeholder: "+15555550123",
|
|
176
|
+
initialValue: existingAllowFrom[0],
|
|
177
|
+
validate: (value) => {
|
|
178
|
+
const raw = trimPromptText(value);
|
|
179
|
+
if (!raw) {
|
|
180
|
+
return t("common.required");
|
|
181
|
+
}
|
|
182
|
+
const normalized = normalizeWhatsAppAllowFromEntry(raw);
|
|
183
|
+
if (!normalized) {
|
|
184
|
+
return `Invalid number: ${raw}`;
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const normalized = normalizeWhatsAppAllowFromEntry(trimPromptText(entry));
|
|
191
|
+
if (!normalized) {
|
|
192
|
+
throw new Error("Invalid WhatsApp owner number (expected E.164 after validation).");
|
|
193
|
+
}
|
|
194
|
+
const allowFrom = normalizeWhatsAppAllowFromEntries([
|
|
195
|
+
...existingAllowFrom.filter((item) => item !== "*"),
|
|
196
|
+
normalized,
|
|
197
|
+
]);
|
|
198
|
+
return { normalized, allowFrom };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function applyWhatsAppOwnerAllowlist(params: {
|
|
202
|
+
cfg: AutoBotConfig;
|
|
203
|
+
accountId: string;
|
|
204
|
+
existingAllowFrom: string[];
|
|
205
|
+
messageLines: string[];
|
|
206
|
+
prompter: SetupPrompter;
|
|
207
|
+
title: string;
|
|
208
|
+
}): Promise<AutoBotConfig> {
|
|
209
|
+
const { normalized, allowFrom } = await promptWhatsAppOwnerAllowFrom({
|
|
210
|
+
prompter: params.prompter,
|
|
211
|
+
existingAllowFrom: params.existingAllowFrom,
|
|
212
|
+
});
|
|
213
|
+
let next = setWhatsAppSelfChatMode(params.cfg, params.accountId, true);
|
|
214
|
+
next = setWhatsAppDmPolicy(next, params.accountId, "allowlist");
|
|
215
|
+
next = setWhatsAppAllowFrom(next, params.accountId, allowFrom);
|
|
216
|
+
await params.prompter.note(
|
|
217
|
+
[...params.messageLines, `- allowFrom includes ${normalized}`].join("\n"),
|
|
218
|
+
params.title,
|
|
219
|
+
);
|
|
220
|
+
return next;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function parseWhatsAppAllowFromEntries(raw: string): { entries: string[]; invalidEntry?: string } {
|
|
224
|
+
const parts = splitSetupEntries(raw);
|
|
225
|
+
if (parts.length === 0) {
|
|
226
|
+
return { entries: [] };
|
|
227
|
+
}
|
|
228
|
+
const entries: string[] = [];
|
|
229
|
+
for (const part of parts) {
|
|
230
|
+
if (part === "*") {
|
|
231
|
+
entries.push("*");
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const normalized = normalizeWhatsAppAllowFromEntry(part);
|
|
235
|
+
if (!normalized) {
|
|
236
|
+
return { entries: [], invalidEntry: part };
|
|
237
|
+
}
|
|
238
|
+
entries.push(normalized);
|
|
239
|
+
}
|
|
240
|
+
return { entries: normalizeWhatsAppAllowFromEntries(entries) };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function promptWhatsAppDmAccess(params: {
|
|
244
|
+
cfg: AutoBotConfig;
|
|
245
|
+
accountId: string;
|
|
246
|
+
forceAllowFrom: boolean;
|
|
247
|
+
prompter: SetupPrompter;
|
|
248
|
+
}): Promise<AutoBotConfig> {
|
|
249
|
+
const accountId = params.accountId.trim() || DEFAULT_ACCOUNT_ID;
|
|
250
|
+
const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId });
|
|
251
|
+
const existingPolicy = account.dmPolicy ?? "pairing";
|
|
252
|
+
const existingAllowFrom = account.allowFrom ?? [];
|
|
253
|
+
const existingLabel = existingAllowFrom.length > 0 ? existingAllowFrom.join(", ") : "unset";
|
|
254
|
+
const configPathPrefix = resolveWhatsAppConfigPathPrefix(params.cfg, accountId);
|
|
255
|
+
const policyKey = `${configPathPrefix}.dmPolicy`;
|
|
256
|
+
const allowFromKey = `${configPathPrefix}.allowFrom`;
|
|
257
|
+
|
|
258
|
+
if (params.forceAllowFrom) {
|
|
259
|
+
return await applyWhatsAppOwnerAllowlist({
|
|
260
|
+
cfg: params.cfg,
|
|
261
|
+
accountId,
|
|
262
|
+
prompter: params.prompter,
|
|
263
|
+
existingAllowFrom,
|
|
264
|
+
title: t("wizard.whatsapp.allowlistTitle"),
|
|
265
|
+
messageLines: [t("wizard.whatsapp.allowlistModeEnabled")],
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
await params.prompter.note(
|
|
270
|
+
[
|
|
271
|
+
`WhatsApp direct chats are gated by \`${policyKey}\` + \`${allowFromKey}\`.`,
|
|
272
|
+
"- pairing (default): unknown senders get a pairing code; owner approves",
|
|
273
|
+
"- allowlist: unknown senders are blocked",
|
|
274
|
+
'- open: public inbound DMs (requires allowFrom to include "*")',
|
|
275
|
+
"- disabled: ignore WhatsApp DMs",
|
|
276
|
+
"",
|
|
277
|
+
`Current: dmPolicy=${existingPolicy}, allowFrom=${existingLabel}`,
|
|
278
|
+
t("wizard.channels.docs", { link: formatDocsLink("/whatsapp", "whatsapp") }),
|
|
279
|
+
].join("\n"),
|
|
280
|
+
t("wizard.whatsapp.dmAccessTitle"),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const phoneMode = await params.prompter.select({
|
|
284
|
+
message: t("wizard.whatsapp.phoneSetupPrompt"),
|
|
285
|
+
options: [
|
|
286
|
+
{ value: "personal", label: t("wizard.whatsapp.personalPhoneLabel") },
|
|
287
|
+
{ value: "separate", label: t("wizard.whatsapp.separatePhoneLabel") },
|
|
288
|
+
],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (phoneMode === "personal") {
|
|
292
|
+
return await applyWhatsAppOwnerAllowlist({
|
|
293
|
+
cfg: params.cfg,
|
|
294
|
+
accountId,
|
|
295
|
+
prompter: params.prompter,
|
|
296
|
+
existingAllowFrom,
|
|
297
|
+
title: t("wizard.whatsapp.personalPhoneTitle"),
|
|
298
|
+
messageLines: [
|
|
299
|
+
t("wizard.whatsapp.personalPhoneModeEnabled"),
|
|
300
|
+
t("wizard.whatsapp.dmPolicySetAllowlist"),
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const policy = (await params.prompter.select({
|
|
306
|
+
message: t("wizard.whatsapp.dmPolicyPrompt"),
|
|
307
|
+
options: [
|
|
308
|
+
{ value: "pairing", label: t("wizard.channels.dmPolicyPairing") },
|
|
309
|
+
{ value: "allowlist", label: t("wizard.whatsapp.dmPolicyAllowlistOnly") },
|
|
310
|
+
{ value: "open", label: t("wizard.channels.dmPolicyOpenOption") },
|
|
311
|
+
{ value: "disabled", label: t("wizard.whatsapp.dmPolicyDisabled") },
|
|
312
|
+
],
|
|
313
|
+
})) as DmPolicy;
|
|
314
|
+
|
|
315
|
+
let next = setWhatsAppSelfChatMode(params.cfg, accountId, false);
|
|
316
|
+
next = setWhatsAppDmPolicy(next, accountId, policy);
|
|
317
|
+
if (policy === "open") {
|
|
318
|
+
const allowFrom = normalizeWhatsAppAllowFromEntries(["*", ...existingAllowFrom]);
|
|
319
|
+
next = setWhatsAppAllowFrom(next, accountId, allowFrom.length > 0 ? allowFrom : ["*"]);
|
|
320
|
+
return next;
|
|
321
|
+
}
|
|
322
|
+
if (policy === "disabled") {
|
|
323
|
+
return next;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const allowOptions =
|
|
327
|
+
existingAllowFrom.length > 0
|
|
328
|
+
? ([
|
|
329
|
+
{ value: "keep", label: t("wizard.whatsapp.keepCurrentAllowFrom") },
|
|
330
|
+
{
|
|
331
|
+
value: "unset",
|
|
332
|
+
label: t("wizard.whatsapp.unsetAllowFromPairing"),
|
|
333
|
+
},
|
|
334
|
+
{ value: "list", label: t("wizard.whatsapp.setAllowFromNumbers") },
|
|
335
|
+
] as const)
|
|
336
|
+
: ([
|
|
337
|
+
{ value: "unset", label: t("wizard.whatsapp.unsetAllowFromDefault") },
|
|
338
|
+
{ value: "list", label: t("wizard.whatsapp.setAllowFromNumbers") },
|
|
339
|
+
] as const);
|
|
340
|
+
|
|
341
|
+
const mode = await params.prompter.select({
|
|
342
|
+
message: t("wizard.whatsapp.allowFromPrompt"),
|
|
343
|
+
options: allowOptions.map((opt) => ({
|
|
344
|
+
value: opt.value,
|
|
345
|
+
label: opt.label,
|
|
346
|
+
})),
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
if (mode === "keep") {
|
|
350
|
+
return next;
|
|
351
|
+
}
|
|
352
|
+
if (mode === "unset") {
|
|
353
|
+
return setWhatsAppAllowFrom(next, accountId, undefined);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const allowRaw = await params.prompter.text({
|
|
357
|
+
message: t("wizard.whatsapp.allowedSenderNumbers"),
|
|
358
|
+
placeholder: "+15555550123, +447700900123",
|
|
359
|
+
validate: (value) => {
|
|
360
|
+
const raw = trimPromptText(value);
|
|
361
|
+
if (!raw) {
|
|
362
|
+
return t("common.required");
|
|
363
|
+
}
|
|
364
|
+
const parsed = parseWhatsAppAllowFromEntries(raw);
|
|
365
|
+
if (parsed.entries.length === 0 && !parsed.invalidEntry) {
|
|
366
|
+
return t("common.required");
|
|
367
|
+
}
|
|
368
|
+
if (parsed.invalidEntry) {
|
|
369
|
+
return `Invalid number: ${parsed.invalidEntry}`;
|
|
370
|
+
}
|
|
371
|
+
return undefined;
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const parsed = parseWhatsAppAllowFromEntries(trimPromptText(allowRaw));
|
|
376
|
+
if (parsed.invalidEntry) {
|
|
377
|
+
throw new Error(`Invalid number: ${parsed.invalidEntry}`);
|
|
378
|
+
}
|
|
379
|
+
if (parsed.entries.length === 0) {
|
|
380
|
+
throw new Error("Invalid WhatsApp allowFrom list (expected at least one E.164 number).");
|
|
381
|
+
}
|
|
382
|
+
return setWhatsAppAllowFrom(next, accountId, parsed.entries);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export async function finalizeWhatsAppSetup(params: {
|
|
386
|
+
cfg: AutoBotConfig;
|
|
387
|
+
accountId: string;
|
|
388
|
+
forceAllowFrom: boolean;
|
|
389
|
+
prompter: SetupPrompter;
|
|
390
|
+
runtime: SetupRuntime;
|
|
391
|
+
}) {
|
|
392
|
+
const accountId = params.accountId.trim() || resolveDefaultWhatsAppAccountId(params.cfg);
|
|
393
|
+
let next =
|
|
394
|
+
accountId === DEFAULT_ACCOUNT_ID
|
|
395
|
+
? params.cfg
|
|
396
|
+
: whatsappSetupAdapter.applyAccountConfig({
|
|
397
|
+
cfg: params.cfg,
|
|
398
|
+
accountId,
|
|
399
|
+
input: {},
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const linked = await detectWhatsAppLinked(next, accountId);
|
|
403
|
+
const { authDir } = resolveWhatsAppAuthDir({
|
|
404
|
+
cfg: next,
|
|
405
|
+
accountId,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
if (!linked) {
|
|
409
|
+
await params.prompter.note(
|
|
410
|
+
[
|
|
411
|
+
t("wizard.whatsapp.scanQr"),
|
|
412
|
+
t("wizard.whatsapp.credentialsStored", { authDir }),
|
|
413
|
+
t("wizard.channels.docs", { link: formatDocsLink("/whatsapp", "whatsapp") }),
|
|
414
|
+
].join("\n"),
|
|
415
|
+
t("wizard.whatsapp.linkingTitle"),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const wantsLink = await params.prompter.confirm({
|
|
420
|
+
message: linked ? t("wizard.whatsapp.relinkPrompt") : t("wizard.whatsapp.linkNowPrompt"),
|
|
421
|
+
initialValue: !linked,
|
|
422
|
+
});
|
|
423
|
+
if (wantsLink) {
|
|
424
|
+
try {
|
|
425
|
+
const { loginWeb } = await import("./login.js");
|
|
426
|
+
await loginWeb(false, undefined, params.runtime, accountId);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
params.runtime.error(`WhatsApp login failed: ${String(error)}`);
|
|
429
|
+
await params.prompter.note(
|
|
430
|
+
t("wizard.channels.docs", { link: formatDocsLink("/whatsapp", "whatsapp") }),
|
|
431
|
+
t("wizard.whatsapp.helpTitle"),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
} else if (!linked) {
|
|
435
|
+
await params.prompter.note(
|
|
436
|
+
t("wizard.whatsapp.linkLater", {
|
|
437
|
+
command: formatCliCommand("autobot channels login"),
|
|
438
|
+
}),
|
|
439
|
+
"WhatsApp",
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
next = await promptWhatsAppDmAccess({
|
|
444
|
+
cfg: next,
|
|
445
|
+
accountId,
|
|
446
|
+
forceAllowFrom: params.forceAllowFrom,
|
|
447
|
+
prompter: params.prompter,
|
|
448
|
+
});
|
|
449
|
+
return { cfg: next };
|
|
450
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ChannelSetupWizard } from "autobot/plugin-sdk/setup";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ACCOUNT_ID,
|
|
4
|
+
setSetupChannelEnabled,
|
|
5
|
+
createSetupTranslator,
|
|
6
|
+
type AutoBotConfig,
|
|
7
|
+
} from "autobot/plugin-sdk/setup";
|
|
8
|
+
import { listWhatsAppAccountIds, resolveWhatsAppAuthDir } from "./accounts.js";
|
|
9
|
+
import { formatWhatsAppWebAuthStatusState, readWebAuthState } from "./auth-store.js";
|
|
10
|
+
|
|
11
|
+
const t = createSetupTranslator();
|
|
12
|
+
|
|
13
|
+
const channel = "whatsapp" as const;
|
|
14
|
+
|
|
15
|
+
type WhatsAppSetupLinkState = "linked" | "not-linked" | "unstable";
|
|
16
|
+
|
|
17
|
+
async function readWhatsAppSetupLinkState(
|
|
18
|
+
cfg: AutoBotConfig,
|
|
19
|
+
accountId: string,
|
|
20
|
+
): Promise<WhatsAppSetupLinkState> {
|
|
21
|
+
const { authDir } = resolveWhatsAppAuthDir({ cfg, accountId });
|
|
22
|
+
return await readWebAuthState(authDir);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const whatsappSetupWizard: ChannelSetupWizard = {
|
|
26
|
+
channel,
|
|
27
|
+
status: {
|
|
28
|
+
configuredLabel: t("wizard.channels.statusLinked"),
|
|
29
|
+
unconfiguredLabel: t("wizard.channels.statusNotLinked"),
|
|
30
|
+
configuredHint: t("wizard.channels.statusLinked"),
|
|
31
|
+
unconfiguredHint: t("wizard.channels.statusNotLinked"),
|
|
32
|
+
configuredScore: 5,
|
|
33
|
+
unconfiguredScore: 4,
|
|
34
|
+
resolveConfigured: async ({ cfg, accountId }) => {
|
|
35
|
+
for (const resolvedAccountId of accountId ? [accountId] : listWhatsAppAccountIds(cfg)) {
|
|
36
|
+
if ((await readWhatsAppSetupLinkState(cfg, resolvedAccountId)) === "linked") {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
},
|
|
42
|
+
resolveStatusLines: async ({ cfg, accountId, configured }) => {
|
|
43
|
+
const linkedAccountId = (
|
|
44
|
+
await Promise.all(
|
|
45
|
+
(accountId ? [accountId] : listWhatsAppAccountIds(cfg)).map(
|
|
46
|
+
async (resolvedAccountId) => ({
|
|
47
|
+
accountId: resolvedAccountId,
|
|
48
|
+
state: await readWhatsAppSetupLinkState(cfg, resolvedAccountId),
|
|
49
|
+
}),
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
).find((entry) => entry.state === "linked" || entry.state === "unstable");
|
|
53
|
+
const labelAccountId = accountId ?? linkedAccountId?.accountId;
|
|
54
|
+
const label = labelAccountId
|
|
55
|
+
? `WhatsApp (${labelAccountId === DEFAULT_ACCOUNT_ID ? "default" : labelAccountId})`
|
|
56
|
+
: "WhatsApp";
|
|
57
|
+
const stateLabel = configured
|
|
58
|
+
? formatWhatsAppWebAuthStatusState("linked")
|
|
59
|
+
: formatWhatsAppWebAuthStatusState(linkedAccountId?.state ?? "not-linked");
|
|
60
|
+
return [`${label}: ${stateLabel}`];
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
resolveShouldPromptAccountIds: ({ shouldPromptAccountIds }) => shouldPromptAccountIds,
|
|
64
|
+
credentials: [],
|
|
65
|
+
finalize: async (params) =>
|
|
66
|
+
await (await import("./setup-finalize.js")).finalizeWhatsAppSetup(params),
|
|
67
|
+
disable: (cfg) => setSetupChannelEnabled(cfg, channel, false),
|
|
68
|
+
onAccountRecorded: (accountId, options) => {
|
|
69
|
+
options?.onAccountId?.(channel, accountId);
|
|
70
|
+
},
|
|
71
|
+
};
|