@lofa199419/waha-v2 2.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/README.md +50 -0
- package/bin/wa +20 -0
- package/bin/wa-adv +9 -0
- package/bin/waha-advanced-entrypoint +12 -0
- package/bin/waha-cli +11 -0
- package/bin/waha_cli.py +549 -0
- package/index.ts +109 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +59 -0
- package/scripts/install-openclaw-extension.mjs +106 -0
- package/skills/waha-v2/SKILL.md +143 -0
- package/skills/waha-v2-onboarding/SKILL.md +146 -0
- package/src/accounts.ts +133 -0
- package/src/channel.ts +823 -0
- package/src/client.ts +585 -0
- package/src/config-schema.ts +342 -0
- package/src/deliver.ts +70 -0
- package/src/gateway.ts +170 -0
- package/src/login.ts +64 -0
- package/src/outbound.ts +84 -0
- package/src/probe.ts +30 -0
- package/src/routes.ts +260 -0
- package/src/runtime.ts +56 -0
- package/src/types.ts +195 -0
- package/src/webhook.ts +841 -0
package/src/channel.ts
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
import type { ChannelMessageActionName, ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { createActionGate, jsonResult } from "openclaw/plugin-sdk";
|
|
3
|
+
import {
|
|
4
|
+
deleteWahaV2Account,
|
|
5
|
+
listWahaV2AccountIds,
|
|
6
|
+
readWahaV2Root,
|
|
7
|
+
resolveWahaV2Account,
|
|
8
|
+
setWahaV2AccountEnabled,
|
|
9
|
+
setWahaV2ChannelConfig,
|
|
10
|
+
} from "./accounts.js";
|
|
11
|
+
import { wahaV2ChannelConfigSchema } from "./config-schema.js";
|
|
12
|
+
import { wahaV2Gateway } from "./gateway.js";
|
|
13
|
+
import { acquireLoginLock, releaseLoginLock, waitForWahaV2Connected } from "./login.js";
|
|
14
|
+
import { wahaV2Outbound } from "./outbound.js";
|
|
15
|
+
import { probeWahaV2Session } from "./probe.js";
|
|
16
|
+
import { getWahaV2Client } from "./runtime.js";
|
|
17
|
+
import {
|
|
18
|
+
WAHA_V2_CHANNEL_ID,
|
|
19
|
+
WAHA_V2_DEFAULT_ACCOUNT_ID,
|
|
20
|
+
type ResolvedWahaV2Account,
|
|
21
|
+
type WahaV2ActionConfig,
|
|
22
|
+
type WahaV2RootConfig,
|
|
23
|
+
} from "./types.js";
|
|
24
|
+
|
|
25
|
+
function sleep(ms: number): Promise<void> {
|
|
26
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function prepareSessionForRequestCode(params: {
|
|
30
|
+
client: NonNullable<ReturnType<typeof getWahaV2Client>>;
|
|
31
|
+
session: string;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
sessionExisted: boolean;
|
|
34
|
+
previousStatus: string | null;
|
|
35
|
+
restarted: boolean;
|
|
36
|
+
}> {
|
|
37
|
+
const sessions = await params.client.listSessions(true).catch(() => []);
|
|
38
|
+
const current = sessions.find((entry) => String(entry?.name ?? "") === params.session);
|
|
39
|
+
const previousStatus = current && typeof current.status === "string" ? current.status : null;
|
|
40
|
+
const sessionExisted = Boolean(current);
|
|
41
|
+
let restarted = false;
|
|
42
|
+
|
|
43
|
+
if (!sessionExisted) {
|
|
44
|
+
await params.client.createSession(params.session);
|
|
45
|
+
await params.client.startSession(params.session);
|
|
46
|
+
return { sessionExisted, previousStatus, restarted };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const normalized = String(previousStatus ?? "").trim().toUpperCase();
|
|
50
|
+
const shouldRestart =
|
|
51
|
+
normalized.length > 0 &&
|
|
52
|
+
!["STOPPED", "FAILED", "NONE", "UNKNOWN", "DISCONNECTED"].includes(normalized);
|
|
53
|
+
|
|
54
|
+
if (shouldRestart) {
|
|
55
|
+
await params.client.stopSession(params.session).catch(() => {});
|
|
56
|
+
restarted = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await params.client.startSession(params.session).catch(async () => {
|
|
60
|
+
await params.client.createSession(params.session);
|
|
61
|
+
await params.client.startSession(params.session);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return { sessionExisted, previousStatus, restarted };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Normalize allow-from entries to WhatsApp JID format. */
|
|
68
|
+
function normalizeAllowEntry(entry: string): string {
|
|
69
|
+
const trimmed = entry.trim();
|
|
70
|
+
if (trimmed.includes("@")) return trimmed;
|
|
71
|
+
return `${trimmed.replace(/\D+/g, "")}@c.us`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const ALL_ACTIONS = [
|
|
75
|
+
// Session management
|
|
76
|
+
"start-session",
|
|
77
|
+
"get-qr",
|
|
78
|
+
"request-code",
|
|
79
|
+
"logout-session",
|
|
80
|
+
// Message actions
|
|
81
|
+
"react",
|
|
82
|
+
"send-seen",
|
|
83
|
+
"send-location",
|
|
84
|
+
"send-contact",
|
|
85
|
+
"forward-message",
|
|
86
|
+
"star-message",
|
|
87
|
+
"unstar-message",
|
|
88
|
+
"edit",
|
|
89
|
+
"delete",
|
|
90
|
+
"pin",
|
|
91
|
+
"unpin",
|
|
92
|
+
// Chats
|
|
93
|
+
"list-chats",
|
|
94
|
+
"get-chat-messages",
|
|
95
|
+
"get-message",
|
|
96
|
+
"archive-chat",
|
|
97
|
+
"unarchive-chat",
|
|
98
|
+
"delete-chat",
|
|
99
|
+
"mark-chat-unread",
|
|
100
|
+
// Contacts
|
|
101
|
+
"list-contacts",
|
|
102
|
+
"get-contact",
|
|
103
|
+
"check-contact",
|
|
104
|
+
"block-contact",
|
|
105
|
+
"unblock-contact",
|
|
106
|
+
// Groups
|
|
107
|
+
"list-groups",
|
|
108
|
+
"get-group",
|
|
109
|
+
"create-group",
|
|
110
|
+
"get-group-participants",
|
|
111
|
+
"add-group-participants",
|
|
112
|
+
"remove-group-participants",
|
|
113
|
+
"promote-group-admin",
|
|
114
|
+
"demote-group-admin",
|
|
115
|
+
"get-group-invite-code",
|
|
116
|
+
"revoke-group-invite-code",
|
|
117
|
+
"renameGroup",
|
|
118
|
+
"update-group-description",
|
|
119
|
+
"leaveGroup",
|
|
120
|
+
"addParticipant",
|
|
121
|
+
"removeParticipant",
|
|
122
|
+
// Status / Stories
|
|
123
|
+
"send-status",
|
|
124
|
+
"delete-status",
|
|
125
|
+
// WA Channels (newsletters)
|
|
126
|
+
"list-wa-channels",
|
|
127
|
+
"get-wa-channel",
|
|
128
|
+
"create-wa-channel",
|
|
129
|
+
"delete-wa-channel",
|
|
130
|
+
"get-wa-channel-messages",
|
|
131
|
+
] as const;
|
|
132
|
+
|
|
133
|
+
export const wahaV2Plugin: ChannelPlugin<ResolvedWahaV2Account> = {
|
|
134
|
+
id: WAHA_V2_CHANNEL_ID,
|
|
135
|
+
|
|
136
|
+
meta: {
|
|
137
|
+
id: WAHA_V2_CHANNEL_ID,
|
|
138
|
+
label: "WAHA (WhatsApp HTTP API)",
|
|
139
|
+
selectionLabel: "WAHA v2 — WhatsApp via HTTP API",
|
|
140
|
+
docsPath: "/channels/whatsapp",
|
|
141
|
+
blurb: "Self-hosted WhatsApp HTTP API; bring your own WAHA instance. No Baileys dependency.",
|
|
142
|
+
order: 13,
|
|
143
|
+
aliases: ["waha-v2", "waha2"],
|
|
144
|
+
quickstartAllowFrom: true,
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
configSchema: wahaV2ChannelConfigSchema,
|
|
148
|
+
|
|
149
|
+
capabilities: {
|
|
150
|
+
chatTypes: ["direct", "group"],
|
|
151
|
+
media: true,
|
|
152
|
+
reactions: true,
|
|
153
|
+
polls: true,
|
|
154
|
+
threads: false,
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
config: {
|
|
158
|
+
listAccountIds: listWahaV2AccountIds,
|
|
159
|
+
resolveAccount: resolveWahaV2Account,
|
|
160
|
+
defaultAccountId: () => WAHA_V2_DEFAULT_ACCOUNT_ID,
|
|
161
|
+
|
|
162
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
163
|
+
setWahaV2AccountEnabled(cfg, accountId, enabled),
|
|
164
|
+
|
|
165
|
+
deleteAccount: ({ cfg, accountId }) => deleteWahaV2Account(cfg, accountId),
|
|
166
|
+
|
|
167
|
+
isEnabled: (account) => account.enabled,
|
|
168
|
+
|
|
169
|
+
isConfigured: (account) => Boolean(account.baseUrl && account.apiKey),
|
|
170
|
+
|
|
171
|
+
unconfiguredReason: () =>
|
|
172
|
+
"Set channels.waha-v2.apiKey via `openclaw config set channels.waha-v2.apiKey <key>`",
|
|
173
|
+
|
|
174
|
+
describeAccount: (account) => ({
|
|
175
|
+
accountId: account.accountId,
|
|
176
|
+
name: account.name,
|
|
177
|
+
enabled: account.enabled,
|
|
178
|
+
configured: Boolean(account.baseUrl && account.apiKey),
|
|
179
|
+
linked: Boolean(account.baseUrl && account.apiKey),
|
|
180
|
+
connected: false, // live value supplied by status.buildAccountSnapshot
|
|
181
|
+
baseUrl: account.baseUrl,
|
|
182
|
+
webhookUrl: account.webhookUrl,
|
|
183
|
+
dmPolicy: account.dmPolicy,
|
|
184
|
+
groupPolicy: account.groupPolicy,
|
|
185
|
+
allowFrom: account.allowFrom,
|
|
186
|
+
allowGroups: account.allowGroups,
|
|
187
|
+
}),
|
|
188
|
+
|
|
189
|
+
resolveAllowFrom: ({ cfg, accountId }) => resolveWahaV2Account(cfg, accountId).allowFrom,
|
|
190
|
+
|
|
191
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
192
|
+
allowFrom.map((e) => normalizeAllowEntry(String(e))).filter(Boolean),
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
setup: {
|
|
196
|
+
applyAccountConfig: ({ cfg, input }) => {
|
|
197
|
+
const root = readWahaV2Root(cfg);
|
|
198
|
+
return setWahaV2ChannelConfig(cfg, {
|
|
199
|
+
...root,
|
|
200
|
+
...(input.token ? { apiKey: input.token } : {}),
|
|
201
|
+
...(input.httpUrl ? { baseUrl: input.httpUrl } : {}),
|
|
202
|
+
...(input.name ? { session: input.name } : {}),
|
|
203
|
+
enabled: true,
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
pairing: {
|
|
209
|
+
idLabel: "WhatsApp number",
|
|
210
|
+
normalizeAllowEntry,
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
messaging: {
|
|
214
|
+
normalizeTarget: (to: string) => {
|
|
215
|
+
const trimmed = to.trim();
|
|
216
|
+
if (trimmed.includes("@")) return trimmed;
|
|
217
|
+
return `${trimmed.replace(/\D+/g, "")}@c.us`;
|
|
218
|
+
},
|
|
219
|
+
targetResolver: {
|
|
220
|
+
looksLikeId: (id: string) => /^\d{7,15}(@[cg]\.us)?$/.test(id.trim()),
|
|
221
|
+
hint: "<E.164 | group JID>",
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
actions: {
|
|
226
|
+
listActions: ({ cfg }) => {
|
|
227
|
+
const root = ((cfg as Record<string, unknown>)?.channels as Record<string, unknown>)?.[
|
|
228
|
+
WAHA_V2_CHANNEL_ID
|
|
229
|
+
] as WahaV2RootConfig | undefined;
|
|
230
|
+
const gate = createActionGate(root?.actions as WahaV2ActionConfig | undefined);
|
|
231
|
+
const result: ChannelMessageActionName[] = [];
|
|
232
|
+
// Session management
|
|
233
|
+
if (gate("sessionManagement")) {
|
|
234
|
+
result.push("start-session", "get-qr", "request-code", "logout-session");
|
|
235
|
+
}
|
|
236
|
+
// Message-level actions
|
|
237
|
+
if (gate("messaging")) {
|
|
238
|
+
result.push(
|
|
239
|
+
"react",
|
|
240
|
+
"send-seen",
|
|
241
|
+
"send-location",
|
|
242
|
+
"send-contact",
|
|
243
|
+
"forward-message",
|
|
244
|
+
"star-message",
|
|
245
|
+
"unstar-message",
|
|
246
|
+
"edit",
|
|
247
|
+
"delete",
|
|
248
|
+
"pin",
|
|
249
|
+
"unpin",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
// Chat browsing / management
|
|
253
|
+
if (gate("chats")) {
|
|
254
|
+
result.push(
|
|
255
|
+
"list-chats",
|
|
256
|
+
"get-chat-messages",
|
|
257
|
+
"get-message",
|
|
258
|
+
"archive-chat",
|
|
259
|
+
"unarchive-chat",
|
|
260
|
+
"delete-chat",
|
|
261
|
+
"mark-chat-unread",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
// Contact lookup / block
|
|
265
|
+
if (gate("contacts")) {
|
|
266
|
+
result.push(
|
|
267
|
+
"list-contacts",
|
|
268
|
+
"get-contact",
|
|
269
|
+
"check-contact",
|
|
270
|
+
"block-contact",
|
|
271
|
+
"unblock-contact",
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
// Group management
|
|
275
|
+
if (gate("groups")) {
|
|
276
|
+
result.push(
|
|
277
|
+
"list-groups",
|
|
278
|
+
"get-group",
|
|
279
|
+
"create-group",
|
|
280
|
+
"get-group-participants",
|
|
281
|
+
"add-group-participants",
|
|
282
|
+
"remove-group-participants",
|
|
283
|
+
"promote-group-admin",
|
|
284
|
+
"demote-group-admin",
|
|
285
|
+
"get-group-invite-code",
|
|
286
|
+
"revoke-group-invite-code",
|
|
287
|
+
"renameGroup",
|
|
288
|
+
"update-group-description",
|
|
289
|
+
"leaveGroup",
|
|
290
|
+
"addParticipant",
|
|
291
|
+
"removeParticipant",
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
// Status / Stories
|
|
295
|
+
if (gate("status")) {
|
|
296
|
+
result.push("send-status", "delete-status");
|
|
297
|
+
}
|
|
298
|
+
// WhatsApp Channels (newsletters)
|
|
299
|
+
if (gate("waChannels")) {
|
|
300
|
+
result.push(
|
|
301
|
+
"list-wa-channels",
|
|
302
|
+
"get-wa-channel",
|
|
303
|
+
"create-wa-channel",
|
|
304
|
+
"delete-wa-channel",
|
|
305
|
+
"get-wa-channel-messages",
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
supportsAction: ({ action }) => (ALL_ACTIONS as readonly string[]).includes(action),
|
|
312
|
+
|
|
313
|
+
handleAction: async ({ action, params, cfg, accountId }) => {
|
|
314
|
+
const p = params as Record<string, unknown>;
|
|
315
|
+
const account = resolveWahaV2Account(cfg, accountId);
|
|
316
|
+
const client = getWahaV2Client(account.accountId);
|
|
317
|
+
if (!client) {
|
|
318
|
+
throw new Error(`waha-v2: no active client for account "${account.accountId}"`);
|
|
319
|
+
}
|
|
320
|
+
const session = account.session;
|
|
321
|
+
|
|
322
|
+
// -----------------------------------------------------------------------
|
|
323
|
+
// Session management
|
|
324
|
+
// -----------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
if (action === "start-session") {
|
|
327
|
+
acquireLoginLock(account.accountId);
|
|
328
|
+
try {
|
|
329
|
+
await client.startSession(session).catch(async () => {
|
|
330
|
+
await client.createSession(session);
|
|
331
|
+
});
|
|
332
|
+
const waitResult = await waitForWahaV2Connected(client, session, 5_000).catch(() => ({
|
|
333
|
+
ok: false,
|
|
334
|
+
}));
|
|
335
|
+
const probe = await probeWahaV2Session(client, session).catch(() => ({ ok: false }));
|
|
336
|
+
return jsonResult({
|
|
337
|
+
ok: true,
|
|
338
|
+
action: "start-session",
|
|
339
|
+
session,
|
|
340
|
+
connected: probe.ok,
|
|
341
|
+
status: (probe as { status?: string }).status ?? null,
|
|
342
|
+
alreadyConnected: waitResult.ok,
|
|
343
|
+
});
|
|
344
|
+
} finally {
|
|
345
|
+
releaseLoginLock(account.accountId);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (action === "get-qr") {
|
|
350
|
+
const qr = await client.getQr(session);
|
|
351
|
+
if (!qr?.data) {
|
|
352
|
+
return jsonResult({
|
|
353
|
+
ok: false,
|
|
354
|
+
action: "get-qr",
|
|
355
|
+
error: "QR not available — session may already be connected or not in QR pairing mode",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return jsonResult({ ok: true, action: "get-qr", session, data: qr.data });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (action === "request-code") {
|
|
362
|
+
const phoneNumber = String(p.phoneNumber ?? "").trim();
|
|
363
|
+
if (!phoneNumber) throw new Error("waha-v2: request-code requires params.phoneNumber");
|
|
364
|
+
acquireLoginLock(account.accountId);
|
|
365
|
+
try {
|
|
366
|
+
const prep = await prepareSessionForRequestCode({ client, session });
|
|
367
|
+
// Give WAHA a short transition window before requesting pairing code.
|
|
368
|
+
await sleep(5_000);
|
|
369
|
+
const result = await client.requestCode(session, phoneNumber);
|
|
370
|
+
return jsonResult({
|
|
371
|
+
ok: true,
|
|
372
|
+
action: "request-code",
|
|
373
|
+
session,
|
|
374
|
+
sessionExisted: prep.sessionExisted,
|
|
375
|
+
previousStatus: prep.previousStatus,
|
|
376
|
+
restarted: prep.restarted,
|
|
377
|
+
waitedMs: 5_000,
|
|
378
|
+
code: result?.code ?? null,
|
|
379
|
+
});
|
|
380
|
+
} finally {
|
|
381
|
+
releaseLoginLock(account.accountId);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (action === "logout-session") {
|
|
386
|
+
await client.logoutSession(session);
|
|
387
|
+
return jsonResult({ ok: true, action: "logout-session", session });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// -----------------------------------------------------------------------
|
|
391
|
+
// Message actions
|
|
392
|
+
// -----------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
if (action === "react") {
|
|
395
|
+
const messageId = String(p.messageId ?? "");
|
|
396
|
+
const emoji = String(p.emoji ?? "");
|
|
397
|
+
await client.addReaction(session, messageId, emoji);
|
|
398
|
+
return jsonResult({ ok: true, action: "react", messageId });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (action === "send-seen") {
|
|
402
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
403
|
+
const messageIds = Array.isArray(p.messageIds) ? (p.messageIds as string[]) : undefined;
|
|
404
|
+
await client.sendSeen(session, chatId, messageIds);
|
|
405
|
+
return jsonResult({ ok: true, action: "send-seen", chatId });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (action === "send-location") {
|
|
409
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
410
|
+
const latitude = Number(p.latitude);
|
|
411
|
+
const longitude = Number(p.longitude);
|
|
412
|
+
const title = p.title ? String(p.title) : undefined;
|
|
413
|
+
if (!chatId || isNaN(latitude) || isNaN(longitude)) {
|
|
414
|
+
throw new Error("waha-v2: send-location requires to, latitude, longitude");
|
|
415
|
+
}
|
|
416
|
+
const result = await client.sendLocation(session, chatId, latitude, longitude, title);
|
|
417
|
+
return jsonResult({ ok: true, action: "send-location", chatId, result });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (action === "send-contact") {
|
|
421
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
422
|
+
const contacts = Array.isArray(p.contacts) ? p.contacts : [p.contact].filter(Boolean);
|
|
423
|
+
if (!chatId || contacts.length === 0) {
|
|
424
|
+
throw new Error("waha-v2: send-contact requires to and contacts array");
|
|
425
|
+
}
|
|
426
|
+
const result = await client.sendContact(session, chatId, contacts);
|
|
427
|
+
return jsonResult({ ok: true, action: "send-contact", chatId, result });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (action === "forward-message") {
|
|
431
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
432
|
+
const messageId = String(p.messageId ?? "");
|
|
433
|
+
if (!chatId || !messageId)
|
|
434
|
+
throw new Error("waha-v2: forward-message requires to and messageId");
|
|
435
|
+
const result = await client.forwardMessage(session, chatId, messageId);
|
|
436
|
+
return jsonResult({ ok: true, action: "forward-message", chatId, result });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (action === "star-message") {
|
|
440
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
441
|
+
const messageId = String(p.messageId ?? "");
|
|
442
|
+
await client.starMessage(session, chatId, messageId, true);
|
|
443
|
+
return jsonResult({ ok: true, action: "star-message", chatId, messageId });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (action === "unstar-message") {
|
|
447
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
448
|
+
const messageId = String(p.messageId ?? "");
|
|
449
|
+
await client.starMessage(session, chatId, messageId, false);
|
|
450
|
+
return jsonResult({ ok: true, action: "unstar-message", chatId, messageId });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (action === "edit") {
|
|
454
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
455
|
+
const messageId = String(p.messageId ?? "");
|
|
456
|
+
const text = String(p.text ?? "");
|
|
457
|
+
if (!chatId || !messageId || !text)
|
|
458
|
+
throw new Error("waha-v2: edit requires to, messageId, text");
|
|
459
|
+
await client.editMessage(session, chatId, messageId, text);
|
|
460
|
+
return jsonResult({ ok: true, action: "edit", chatId, messageId });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (action === "delete") {
|
|
464
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
465
|
+
const messageId = String(p.messageId ?? "");
|
|
466
|
+
if (!chatId || !messageId) throw new Error("waha-v2: delete requires to and messageId");
|
|
467
|
+
await client.deleteMessage(session, chatId, messageId);
|
|
468
|
+
return jsonResult({ ok: true, action: "delete", chatId, messageId });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (action === "pin") {
|
|
472
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
473
|
+
const messageId = String(p.messageId ?? "");
|
|
474
|
+
if (!chatId || !messageId) throw new Error("waha-v2: pin requires to and messageId");
|
|
475
|
+
await client.pinMessage(session, chatId, messageId);
|
|
476
|
+
return jsonResult({ ok: true, action: "pin", chatId, messageId });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (action === "unpin") {
|
|
480
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
481
|
+
const messageId = String(p.messageId ?? "");
|
|
482
|
+
if (!chatId || !messageId) throw new Error("waha-v2: unpin requires to and messageId");
|
|
483
|
+
await client.unpinMessage(session, chatId, messageId);
|
|
484
|
+
return jsonResult({ ok: true, action: "unpin", chatId, messageId });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// -----------------------------------------------------------------------
|
|
488
|
+
// Chats
|
|
489
|
+
// -----------------------------------------------------------------------
|
|
490
|
+
|
|
491
|
+
if (action === "list-chats") {
|
|
492
|
+
const limit = p.limit ? Number(p.limit) : undefined;
|
|
493
|
+
const offset = p.offset ? Number(p.offset) : undefined;
|
|
494
|
+
const chats = await client.listChats(session, limit, offset);
|
|
495
|
+
return jsonResult({ ok: true, action: "list-chats", chats });
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (action === "get-chat-messages") {
|
|
499
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
500
|
+
if (!chatId) throw new Error("waha-v2: get-chat-messages requires to");
|
|
501
|
+
const limit = p.limit ? Number(p.limit) : undefined;
|
|
502
|
+
const messages = await client.getChatMessages(session, chatId, limit);
|
|
503
|
+
return jsonResult({ ok: true, action: "get-chat-messages", chatId, messages });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (action === "get-message") {
|
|
507
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
508
|
+
const messageId = String(p.messageId ?? "");
|
|
509
|
+
if (!chatId || !messageId)
|
|
510
|
+
throw new Error("waha-v2: get-message requires to and messageId");
|
|
511
|
+
const message = await client.getChatMessage(session, chatId, messageId);
|
|
512
|
+
return jsonResult({ ok: true, action: "get-message", chatId, messageId, message });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (action === "archive-chat") {
|
|
516
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
517
|
+
if (!chatId) throw new Error("waha-v2: archive-chat requires to");
|
|
518
|
+
await client.archiveChat(session, chatId);
|
|
519
|
+
return jsonResult({ ok: true, action: "archive-chat", chatId });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (action === "unarchive-chat") {
|
|
523
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
524
|
+
if (!chatId) throw new Error("waha-v2: unarchive-chat requires to");
|
|
525
|
+
await client.unarchiveChat(session, chatId);
|
|
526
|
+
return jsonResult({ ok: true, action: "unarchive-chat", chatId });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (action === "delete-chat") {
|
|
530
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
531
|
+
if (!chatId) throw new Error("waha-v2: delete-chat requires to");
|
|
532
|
+
await client.deleteChat(session, chatId);
|
|
533
|
+
return jsonResult({ ok: true, action: "delete-chat", chatId });
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (action === "mark-chat-unread") {
|
|
537
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
538
|
+
if (!chatId) throw new Error("waha-v2: mark-chat-unread requires to");
|
|
539
|
+
await client.markChatUnread(session, chatId);
|
|
540
|
+
return jsonResult({ ok: true, action: "mark-chat-unread", chatId });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// -----------------------------------------------------------------------
|
|
544
|
+
// Contacts
|
|
545
|
+
// -----------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
if (action === "list-contacts") {
|
|
548
|
+
const limit = p.limit ? Number(p.limit) : undefined;
|
|
549
|
+
const offset = p.offset ? Number(p.offset) : undefined;
|
|
550
|
+
const contacts = await client.listContacts(session, limit, offset);
|
|
551
|
+
return jsonResult({ ok: true, action: "list-contacts", contacts });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (action === "get-contact") {
|
|
555
|
+
const contactId = String(p.contactId ?? p.target ?? p.to ?? "");
|
|
556
|
+
if (!contactId) throw new Error("waha-v2: get-contact requires contactId");
|
|
557
|
+
const contact = await client.getContact(session, contactId);
|
|
558
|
+
return jsonResult({ ok: true, action: "get-contact", contactId, contact });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (action === "check-contact") {
|
|
562
|
+
const phone = String(p.phone ?? p.phoneNumber ?? "").trim();
|
|
563
|
+
if (!phone) throw new Error("waha-v2: check-contact requires phone");
|
|
564
|
+
const result = await client.checkContactExists(session, phone);
|
|
565
|
+
return jsonResult({ ok: true, action: "check-contact", phone, result });
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (action === "block-contact") {
|
|
569
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
570
|
+
if (!chatId) throw new Error("waha-v2: block-contact requires to");
|
|
571
|
+
await client.blockContact(session, chatId);
|
|
572
|
+
return jsonResult({ ok: true, action: "block-contact", chatId });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (action === "unblock-contact") {
|
|
576
|
+
const chatId = String(p.target ?? p.to ?? p.chatId ?? "");
|
|
577
|
+
if (!chatId) throw new Error("waha-v2: unblock-contact requires to");
|
|
578
|
+
await client.unblockContact(session, chatId);
|
|
579
|
+
return jsonResult({ ok: true, action: "unblock-contact", chatId });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// -----------------------------------------------------------------------
|
|
583
|
+
// Groups
|
|
584
|
+
// -----------------------------------------------------------------------
|
|
585
|
+
|
|
586
|
+
if (action === "list-groups") {
|
|
587
|
+
const groups = await client.listGroups(session);
|
|
588
|
+
return jsonResult({ ok: true, action: "list-groups", groups });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (action === "get-group") {
|
|
592
|
+
const groupId = String(p.groupId ?? p.target ?? p.to ?? "");
|
|
593
|
+
if (!groupId) throw new Error("waha-v2: get-group requires groupId");
|
|
594
|
+
const group = await client.getGroup(session, groupId);
|
|
595
|
+
return jsonResult({ ok: true, action: "get-group", groupId, group });
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (action === "create-group") {
|
|
599
|
+
const subject = String(p.subject ?? p.name ?? "");
|
|
600
|
+
if (!subject) throw new Error("waha-v2: create-group requires subject");
|
|
601
|
+
const participants = Array.isArray(p.participants) ? (p.participants as string[]) : [];
|
|
602
|
+
const group = await client.createGroup(session, subject, participants);
|
|
603
|
+
return jsonResult({ ok: true, action: "create-group", group });
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (action === "get-group-participants") {
|
|
607
|
+
const groupId = String(p.groupId ?? p.target ?? p.to ?? "");
|
|
608
|
+
if (!groupId) throw new Error("waha-v2: get-group-participants requires groupId");
|
|
609
|
+
const participants = await client.getGroupParticipants(session, groupId);
|
|
610
|
+
return jsonResult({ ok: true, action: "get-group-participants", groupId, participants });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (action === "add-group-participants" || action === "addParticipant") {
|
|
614
|
+
const groupId = String(p.groupId ?? p.target ?? p.to ?? "");
|
|
615
|
+
const participants = Array.isArray(p.participants)
|
|
616
|
+
? (p.participants as string[])
|
|
617
|
+
: [String(p.participant ?? "")].filter(Boolean);
|
|
618
|
+
if (!groupId || participants.length === 0) {
|
|
619
|
+
throw new Error("waha-v2: add-group-participants requires groupId and participants");
|
|
620
|
+
}
|
|
621
|
+
const result = await client.addGroupParticipants(session, groupId, participants);
|
|
622
|
+
return jsonResult({ ok: true, action: "add-group-participants", groupId, result });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (action === "remove-group-participants" || action === "removeParticipant") {
|
|
626
|
+
const groupId = String(p.groupId ?? p.target ?? p.to ?? "");
|
|
627
|
+
const participants = Array.isArray(p.participants)
|
|
628
|
+
? (p.participants as string[])
|
|
629
|
+
: [String(p.participant ?? "")].filter(Boolean);
|
|
630
|
+
if (!groupId || participants.length === 0) {
|
|
631
|
+
throw new Error("waha-v2: remove-group-participants requires groupId and participants");
|
|
632
|
+
}
|
|
633
|
+
const result = await client.removeGroupParticipants(session, groupId, participants);
|
|
634
|
+
return jsonResult({ ok: true, action: "remove-group-participants", groupId, result });
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (action === "promote-group-admin") {
|
|
638
|
+
const groupId = String(p.groupId ?? p.target ?? p.to ?? "");
|
|
639
|
+
const participants = Array.isArray(p.participants)
|
|
640
|
+
? (p.participants as string[])
|
|
641
|
+
: [String(p.participant ?? "")].filter(Boolean);
|
|
642
|
+
if (!groupId || participants.length === 0)
|
|
643
|
+
throw new Error("waha-v2: promote-group-admin requires groupId and participants");
|
|
644
|
+
const result = await client.promoteGroupAdmin(session, groupId, participants);
|
|
645
|
+
return jsonResult({ ok: true, action: "promote-group-admin", groupId, result });
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (action === "demote-group-admin") {
|
|
649
|
+
const groupId = String(p.groupId ?? p.target ?? p.to ?? "");
|
|
650
|
+
const participants = Array.isArray(p.participants)
|
|
651
|
+
? (p.participants as string[])
|
|
652
|
+
: [String(p.participant ?? "")].filter(Boolean);
|
|
653
|
+
if (!groupId || participants.length === 0)
|
|
654
|
+
throw new Error("waha-v2: demote-group-admin requires groupId and participants");
|
|
655
|
+
const result = await client.demoteGroupAdmin(session, groupId, participants);
|
|
656
|
+
return jsonResult({ ok: true, action: "demote-group-admin", groupId, result });
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (action === "get-group-invite-code") {
|
|
660
|
+
const groupId = String(p.groupId ?? p.target ?? p.to ?? "");
|
|
661
|
+
if (!groupId) throw new Error("waha-v2: get-group-invite-code requires groupId");
|
|
662
|
+
const result = await client.getGroupInviteCode(session, groupId);
|
|
663
|
+
return jsonResult({ ok: true, action: "get-group-invite-code", groupId, result });
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (action === "revoke-group-invite-code") {
|
|
667
|
+
const groupId = String(p.groupId ?? p.target ?? p.to ?? "");
|
|
668
|
+
if (!groupId) throw new Error("waha-v2: revoke-group-invite-code requires groupId");
|
|
669
|
+
const result = await client.revokeGroupInviteCode(session, groupId);
|
|
670
|
+
return jsonResult({ ok: true, action: "revoke-group-invite-code", groupId, result });
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (action === "renameGroup" || action === "update-group-description") {
|
|
674
|
+
const groupId = String(p.groupId ?? p.target ?? p.to ?? "");
|
|
675
|
+
if (action === "renameGroup") {
|
|
676
|
+
const subject = String(p.name ?? p.subject ?? "");
|
|
677
|
+
if (!groupId || !subject)
|
|
678
|
+
throw new Error("waha-v2: renameGroup requires groupId and name");
|
|
679
|
+
await client.updateGroupSubject(session, groupId, subject);
|
|
680
|
+
return jsonResult({ ok: true, action: "renameGroup", groupId });
|
|
681
|
+
}
|
|
682
|
+
const description = String(p.description ?? "");
|
|
683
|
+
if (!groupId || !description)
|
|
684
|
+
throw new Error("waha-v2: update-group-description requires groupId and description");
|
|
685
|
+
await client.updateGroupDescription(session, groupId, description);
|
|
686
|
+
return jsonResult({ ok: true, action: "update-group-description", groupId });
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (action === "leaveGroup") {
|
|
690
|
+
const groupId = String(p.groupId ?? p.target ?? p.to ?? "");
|
|
691
|
+
if (!groupId) throw new Error("waha-v2: leaveGroup requires groupId");
|
|
692
|
+
await client.leaveGroup(session, groupId);
|
|
693
|
+
return jsonResult({ ok: true, action: "leaveGroup", groupId });
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// -----------------------------------------------------------------------
|
|
697
|
+
// Status / Stories
|
|
698
|
+
// -----------------------------------------------------------------------
|
|
699
|
+
|
|
700
|
+
if (action === "send-status") {
|
|
701
|
+
const text = String(p.text ?? "").trim();
|
|
702
|
+
if (!text) throw new Error("waha-v2: send-status requires text (text status only for now)");
|
|
703
|
+
const result = await client.sendStatusText(session, text);
|
|
704
|
+
return jsonResult({ ok: true, action: "send-status", result });
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (action === "delete-status") {
|
|
708
|
+
const messageId = String(p.messageId ?? "");
|
|
709
|
+
if (!messageId) throw new Error("waha-v2: delete-status requires messageId");
|
|
710
|
+
await client.deleteStatus(session, messageId);
|
|
711
|
+
return jsonResult({ ok: true, action: "delete-status", messageId });
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// -----------------------------------------------------------------------
|
|
715
|
+
// WhatsApp Channels (Newsletters)
|
|
716
|
+
// -----------------------------------------------------------------------
|
|
717
|
+
|
|
718
|
+
if (action === "list-wa-channels") {
|
|
719
|
+
const channels = await client.listWaChannels(session);
|
|
720
|
+
return jsonResult({ ok: true, action: "list-wa-channels", channels });
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (action === "get-wa-channel") {
|
|
724
|
+
const channelId = String(p.channelId ?? "");
|
|
725
|
+
if (!channelId) throw new Error("waha-v2: get-wa-channel requires channelId");
|
|
726
|
+
const channel = await client.getWaChannel(session, channelId);
|
|
727
|
+
return jsonResult({ ok: true, action: "get-wa-channel", channelId, channel });
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (action === "create-wa-channel") {
|
|
731
|
+
const name = String(p.name ?? "");
|
|
732
|
+
if (!name) throw new Error("waha-v2: create-wa-channel requires name");
|
|
733
|
+
const description = p.description ? String(p.description) : undefined;
|
|
734
|
+
const channel = await client.createWaChannel(session, name, description);
|
|
735
|
+
return jsonResult({ ok: true, action: "create-wa-channel", channel });
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (action === "delete-wa-channel") {
|
|
739
|
+
const channelId = String(p.channelId ?? "");
|
|
740
|
+
if (!channelId) throw new Error("waha-v2: delete-wa-channel requires channelId");
|
|
741
|
+
await client.deleteWaChannel(session, channelId);
|
|
742
|
+
return jsonResult({ ok: true, action: "delete-wa-channel", channelId });
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (action === "get-wa-channel-messages") {
|
|
746
|
+
const channelId = String(p.channelId ?? "");
|
|
747
|
+
if (!channelId) throw new Error("waha-v2: get-wa-channel-messages requires channelId");
|
|
748
|
+
const limit = p.limit ? Number(p.limit) : undefined;
|
|
749
|
+
const messages = await client.getWaChannelMessages(session, channelId, limit);
|
|
750
|
+
return jsonResult({ ok: true, action: "get-wa-channel-messages", channelId, messages });
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
throw new Error(`waha-v2: action "${action}" is not supported`);
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
|
|
757
|
+
status: {
|
|
758
|
+
defaultRuntime: {
|
|
759
|
+
accountId: WAHA_V2_DEFAULT_ACCOUNT_ID,
|
|
760
|
+
running: false,
|
|
761
|
+
connected: false,
|
|
762
|
+
lastError: null,
|
|
763
|
+
},
|
|
764
|
+
|
|
765
|
+
probeAccount: async ({ account }) => {
|
|
766
|
+
const client = getWahaV2Client(account.accountId);
|
|
767
|
+
if (!client) return { ok: false, error: "gateway not running" };
|
|
768
|
+
return probeWahaV2Session(client, account.session);
|
|
769
|
+
},
|
|
770
|
+
|
|
771
|
+
buildChannelSummary: async ({ account, snapshot }) => ({
|
|
772
|
+
configured: Boolean(account.baseUrl && account.apiKey),
|
|
773
|
+
linked: Boolean(account.baseUrl && account.apiKey),
|
|
774
|
+
running: snapshot.running ?? false,
|
|
775
|
+
connected: snapshot.connected ?? false,
|
|
776
|
+
lastError: snapshot.lastError ?? null,
|
|
777
|
+
transport: "waha-v2",
|
|
778
|
+
baseUrl: account.baseUrl ?? null,
|
|
779
|
+
session: account.session ?? "default",
|
|
780
|
+
}),
|
|
781
|
+
|
|
782
|
+
buildAccountSnapshot: async ({ account, runtime, probe }) => {
|
|
783
|
+
const probeOk = (probe as { ok?: boolean } | undefined)?.ok === true;
|
|
784
|
+
const probeErr = (probe as { error?: string } | undefined)?.error ?? null;
|
|
785
|
+
const connected = probeOk || (runtime?.connected ?? false);
|
|
786
|
+
return {
|
|
787
|
+
accountId: account.accountId,
|
|
788
|
+
name: account.name,
|
|
789
|
+
enabled: account.enabled,
|
|
790
|
+
configured: Boolean(account.baseUrl && account.apiKey),
|
|
791
|
+
linked: Boolean(account.baseUrl && account.apiKey),
|
|
792
|
+
running: runtime?.running ?? false,
|
|
793
|
+
connected,
|
|
794
|
+
// Clear stale errors once the session is confirmed connected.
|
|
795
|
+
lastError: connected ? null : (runtime?.lastError ?? probeErr),
|
|
796
|
+
baseUrl: account.baseUrl,
|
|
797
|
+
dmPolicy: account.dmPolicy,
|
|
798
|
+
groupPolicy: account.groupPolicy,
|
|
799
|
+
allowFrom: account.allowFrom,
|
|
800
|
+
allowGroups: account.allowGroups,
|
|
801
|
+
probe,
|
|
802
|
+
};
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
|
|
806
|
+
outbound: wahaV2Outbound,
|
|
807
|
+
gateway: wahaV2Gateway,
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// ---------------------------------------------------------------------------
|
|
811
|
+
// Config mutation helpers used by setup/config adapters
|
|
812
|
+
// ---------------------------------------------------------------------------
|
|
813
|
+
|
|
814
|
+
export function _setWahaV2ChannelRootConfig(
|
|
815
|
+
cfg: OpenClawConfig,
|
|
816
|
+
patch: Record<string, unknown>,
|
|
817
|
+
): OpenClawConfig {
|
|
818
|
+
const base = cfg as Record<string, unknown>;
|
|
819
|
+
const channels = { ...((base.channels as Record<string, unknown>) ?? {}) };
|
|
820
|
+
const current = (channels[WAHA_V2_CHANNEL_ID] ?? {}) as Record<string, unknown>;
|
|
821
|
+
channels[WAHA_V2_CHANNEL_ID] = { ...current, ...patch };
|
|
822
|
+
return { ...base, channels } as OpenClawConfig;
|
|
823
|
+
}
|