@elizaos/plugin-whatsapp 2.0.0-alpha.9 → 2.0.3-beta.5
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/LICENSE +21 -0
- package/README.md +176 -0
- package/auto-enable.ts +21 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/accounts.d.ts +205 -0
- package/dist/accounts.d.ts.map +1 -0
- package/dist/api/whatsapp-routes.d.ts +53 -0
- package/dist/api/whatsapp-routes.d.ts.map +1 -0
- package/dist/baileys/auth.d.ts +10 -0
- package/dist/baileys/auth.d.ts.map +1 -0
- package/dist/baileys/connection.d.ts +19 -0
- package/dist/baileys/connection.d.ts.map +1 -0
- package/dist/baileys/message-adapter.d.ts +14 -0
- package/dist/baileys/message-adapter.d.ts.map +1 -0
- package/dist/baileys/qr-code.d.ts +6 -0
- package/dist/baileys/qr-code.d.ts.map +1 -0
- package/dist/client.d.ts +99 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/clients/baileys-client.d.ts +18 -0
- package/dist/clients/baileys-client.d.ts.map +1 -0
- package/dist/clients/factory.d.ts +6 -0
- package/dist/clients/factory.d.ts.map +1 -0
- package/dist/clients/interface.d.ts +10 -0
- package/dist/clients/interface.d.ts.map +1 -0
- package/dist/config.d.ts +135 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/connector-account-provider.d.ts +19 -0
- package/dist/connector-account-provider.d.ts.map +1 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2526 -854
- package/dist/index.js.map +23 -19
- package/dist/media.d.ts +2 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/normalize.d.ts +69 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/pairing-service.d.ts +41 -0
- package/dist/pairing-service.d.ts.map +1 -0
- package/dist/qrcode-terminal-types.d.ts +6 -0
- package/dist/qrcode-terminal-types.d.ts.map +1 -0
- package/dist/runtime-service.d.ts +125 -0
- package/dist/runtime-service.d.ts.map +1 -0
- package/dist/services/whatsapp-pairing.d.ts +41 -0
- package/dist/services/whatsapp-pairing.d.ts.map +1 -0
- package/dist/setup-routes.d.ts +26 -0
- package/dist/setup-routes.d.ts.map +1 -0
- package/dist/types.d.ts +370 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/config-detector.d.ts +3 -0
- package/dist/utils/config-detector.d.ts.map +1 -0
- package/dist/webhook-auth.d.ts +11 -0
- package/dist/webhook-auth.d.ts.map +1 -0
- package/dist/workflow-credential-provider.d.ts +21 -0
- package/dist/workflow-credential-provider.d.ts.map +1 -0
- package/package.json +162 -131
- package/registry-entry.json +128 -0
package/dist/index.js
CHANGED
|
@@ -1,385 +1,378 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// src/actions/sendMessage.ts
|
|
5
|
-
import {
|
|
6
|
-
composePromptFromState,
|
|
7
|
-
ModelType,
|
|
8
|
-
parseJSONObjectFromText
|
|
9
|
-
} from "@elizaos/core";
|
|
10
|
-
var WHATSAPP_SEND_MESSAGE_ACTION = "WHATSAPP_SEND_MESSAGE";
|
|
11
|
-
var SEND_MESSAGE_TEMPLATE = `
|
|
12
|
-
You are extracting WhatsApp message parameters from a conversation.
|
|
13
|
-
|
|
14
|
-
The user wants to send a WhatsApp message. Extract the following:
|
|
15
|
-
1. to: The phone number to send to (E.164 format, e.g., +14155552671)
|
|
16
|
-
2. text: The message text to send
|
|
17
|
-
|
|
18
|
-
{{recentMessages}}
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
3
|
|
|
20
|
-
|
|
4
|
+
// src/index.ts
|
|
5
|
+
import { getConnectorAccountManager, logger as logger2 } from "@elizaos/core";
|
|
21
6
|
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
7
|
+
// src/accounts.ts
|
|
8
|
+
import { checkPairingAllowed, isInAllowlist } from "@elizaos/core";
|
|
9
|
+
var DEFAULT_ACCOUNT_ID = "default";
|
|
10
|
+
function normalizeAccountId(accountId) {
|
|
11
|
+
if (!accountId || typeof accountId !== "string") {
|
|
12
|
+
return DEFAULT_ACCOUNT_ID;
|
|
13
|
+
}
|
|
14
|
+
const trimmed = accountId.trim().toLowerCase();
|
|
15
|
+
if (!trimmed || trimmed === "default") {
|
|
16
|
+
return DEFAULT_ACCOUNT_ID;
|
|
17
|
+
}
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
20
|
+
function getMultiAccountConfig(runtime) {
|
|
21
|
+
const characterWhatsApp = runtime.character.settings?.whatsapp;
|
|
22
|
+
return {
|
|
23
|
+
enabled: characterWhatsApp?.enabled,
|
|
24
|
+
transport: characterWhatsApp?.transport,
|
|
25
|
+
authDir: characterWhatsApp?.authDir,
|
|
26
|
+
accessToken: characterWhatsApp?.accessToken,
|
|
27
|
+
phoneNumberId: characterWhatsApp?.phoneNumberId,
|
|
28
|
+
businessAccountId: characterWhatsApp?.businessAccountId,
|
|
29
|
+
webhookVerifyToken: characterWhatsApp?.webhookVerifyToken,
|
|
30
|
+
apiVersion: characterWhatsApp?.apiVersion,
|
|
31
|
+
dmPolicy: characterWhatsApp?.dmPolicy,
|
|
32
|
+
groupPolicy: characterWhatsApp?.groupPolicy,
|
|
33
|
+
mediaMaxMb: characterWhatsApp?.mediaMaxMb,
|
|
34
|
+
textChunkLimit: characterWhatsApp?.textChunkLimit,
|
|
35
|
+
accounts: characterWhatsApp?.accounts,
|
|
36
|
+
groups: characterWhatsApp?.groups
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function listWhatsAppAccountIds(runtime) {
|
|
40
|
+
const config = getMultiAccountConfig(runtime);
|
|
41
|
+
const accounts = config.accounts;
|
|
42
|
+
const ids = new Set;
|
|
43
|
+
const envToken = runtime.getSetting("WHATSAPP_ACCESS_TOKEN");
|
|
44
|
+
const envPhoneId = runtime.getSetting("WHATSAPP_PHONE_NUMBER_ID");
|
|
45
|
+
const envAuthDir = runtime.getSetting("WHATSAPP_AUTH_DIR") ?? runtime.getSetting("WHATSAPP_SESSION_PATH");
|
|
46
|
+
const baseConfigured = Boolean(config.accessToken?.trim() && config.phoneNumberId?.trim());
|
|
47
|
+
const envConfigured = Boolean(envToken?.trim() && envPhoneId?.trim());
|
|
48
|
+
const baileysConfigured = Boolean(config.authDir?.trim() || envAuthDir?.trim());
|
|
49
|
+
if (baseConfigured || envConfigured || baileysConfigured) {
|
|
50
|
+
ids.add(DEFAULT_ACCOUNT_ID);
|
|
51
|
+
}
|
|
52
|
+
if (accounts && typeof accounts === "object") {
|
|
53
|
+
for (const id of Object.keys(accounts)) {
|
|
54
|
+
if (id) {
|
|
55
|
+
ids.add(normalizeAccountId(id));
|
|
71
56
|
}
|
|
72
|
-
return { success: false, error: "WhatsApp not configured" };
|
|
73
57
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
return { success: false, error: "Empty message text" };
|
|
113
|
-
}
|
|
114
|
-
params = parsed;
|
|
115
|
-
}
|
|
116
|
-
} catch {
|
|
117
|
-
if (callback) {
|
|
118
|
-
await callback({
|
|
119
|
-
text: "Failed to parse message parameters"
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
return { success: false, error: "Failed to parse message parameters" };
|
|
58
|
+
}
|
|
59
|
+
const result = Array.from(ids);
|
|
60
|
+
if (result.length === 0) {
|
|
61
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
62
|
+
}
|
|
63
|
+
return result.slice().sort((a, b) => a.localeCompare(b));
|
|
64
|
+
}
|
|
65
|
+
function resolveDefaultWhatsAppAccountId(runtime) {
|
|
66
|
+
const ids = listWhatsAppAccountIds(runtime);
|
|
67
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
68
|
+
return DEFAULT_ACCOUNT_ID;
|
|
69
|
+
}
|
|
70
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
71
|
+
}
|
|
72
|
+
function getAccountConfig(runtime, accountId) {
|
|
73
|
+
const config = getMultiAccountConfig(runtime);
|
|
74
|
+
const accounts = config.accounts;
|
|
75
|
+
if (!accounts || typeof accounts !== "object") {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const direct = accounts[accountId];
|
|
79
|
+
if (direct) {
|
|
80
|
+
return direct;
|
|
81
|
+
}
|
|
82
|
+
const normalized = normalizeAccountId(accountId);
|
|
83
|
+
const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized);
|
|
84
|
+
return matchKey ? accounts[matchKey] : undefined;
|
|
85
|
+
}
|
|
86
|
+
function resolveWhatsAppToken(runtime, accountId) {
|
|
87
|
+
const multiConfig = getMultiAccountConfig(runtime);
|
|
88
|
+
const accountConfig = getAccountConfig(runtime, accountId);
|
|
89
|
+
if (accountConfig?.accessToken?.trim()) {
|
|
90
|
+
return { token: accountConfig.accessToken.trim(), source: "config" };
|
|
91
|
+
}
|
|
92
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
93
|
+
if (multiConfig.accessToken?.trim()) {
|
|
94
|
+
return { token: multiConfig.accessToken.trim(), source: "config" };
|
|
123
95
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
method: "POST",
|
|
128
|
-
headers: {
|
|
129
|
-
Authorization: `Bearer ${accessToken}`,
|
|
130
|
-
"Content-Type": "application/json"
|
|
131
|
-
},
|
|
132
|
-
body: JSON.stringify({
|
|
133
|
-
messaging_product: "whatsapp",
|
|
134
|
-
recipient_type: "individual",
|
|
135
|
-
to: params.to,
|
|
136
|
-
type: "text",
|
|
137
|
-
text: {
|
|
138
|
-
preview_url: false,
|
|
139
|
-
body: params.text
|
|
140
|
-
}
|
|
141
|
-
})
|
|
142
|
-
});
|
|
143
|
-
if (!response.ok) {
|
|
144
|
-
const errorData = await response.json();
|
|
145
|
-
throw new Error(errorData.error?.message || `HTTP ${response.status}`);
|
|
146
|
-
}
|
|
147
|
-
const data = await response.json();
|
|
148
|
-
const messageId = data.messages?.[0]?.id;
|
|
149
|
-
if (callback) {
|
|
150
|
-
await callback({
|
|
151
|
-
text: `Message sent to ${params.to}`,
|
|
152
|
-
action: WHATSAPP_SEND_MESSAGE_ACTION
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
return {
|
|
156
|
-
success: true,
|
|
157
|
-
data: {
|
|
158
|
-
action: WHATSAPP_SEND_MESSAGE_ACTION,
|
|
159
|
-
to: params.to,
|
|
160
|
-
messageId
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
} catch (error) {
|
|
164
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
165
|
-
if (callback) {
|
|
166
|
-
await callback({
|
|
167
|
-
text: `Failed to send WhatsApp message: ${errorMessage}`
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
return { success: false, error: errorMessage };
|
|
96
|
+
const envToken = runtime.getSetting("WHATSAPP_ACCESS_TOKEN");
|
|
97
|
+
if (envToken?.trim()) {
|
|
98
|
+
return { token: envToken.trim(), source: "env" };
|
|
171
99
|
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
100
|
+
}
|
|
101
|
+
return { token: "", source: "none" };
|
|
102
|
+
}
|
|
103
|
+
function filterDefined(obj) {
|
|
104
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
105
|
+
}
|
|
106
|
+
function mergeWhatsAppAccountConfig(runtime, accountId) {
|
|
107
|
+
const multiConfig = getMultiAccountConfig(runtime);
|
|
108
|
+
const { accounts: _ignored, ...baseConfig } = multiConfig;
|
|
109
|
+
const accountConfig = getAccountConfig(runtime, accountId) ?? {};
|
|
110
|
+
const envToken = runtime.getSetting("WHATSAPP_ACCESS_TOKEN");
|
|
111
|
+
const envPhoneId = runtime.getSetting("WHATSAPP_PHONE_NUMBER_ID");
|
|
112
|
+
const envBusinessId = runtime.getSetting("WHATSAPP_BUSINESS_ACCOUNT_ID");
|
|
113
|
+
const envWebhookToken = runtime.getSetting("WHATSAPP_WEBHOOK_VERIFY_TOKEN");
|
|
114
|
+
const envDmPolicy = runtime.getSetting("WHATSAPP_DM_POLICY");
|
|
115
|
+
const envGroupPolicy = runtime.getSetting("WHATSAPP_GROUP_POLICY");
|
|
116
|
+
const envAuthDir = runtime.getSetting("WHATSAPP_AUTH_DIR") ?? runtime.getSetting("WHATSAPP_SESSION_PATH");
|
|
117
|
+
const envTransport = runtime.getSetting("WHATSAPP_AUTH_METHOD");
|
|
118
|
+
const envConfig = {
|
|
119
|
+
transport: envTransport,
|
|
120
|
+
authDir: envAuthDir || undefined,
|
|
121
|
+
accessToken: envToken || undefined,
|
|
122
|
+
phoneNumberId: envPhoneId || undefined,
|
|
123
|
+
businessAccountId: envBusinessId || undefined,
|
|
124
|
+
webhookVerifyToken: envWebhookToken || undefined,
|
|
125
|
+
dmPolicy: envDmPolicy,
|
|
126
|
+
groupPolicy: envGroupPolicy
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
...filterDefined(envConfig),
|
|
130
|
+
...filterDefined(baseConfig),
|
|
131
|
+
...filterDefined(accountConfig)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function resolveWhatsAppAccount(runtime, accountId) {
|
|
135
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
136
|
+
const multiConfig = getMultiAccountConfig(runtime);
|
|
137
|
+
const baseEnabled = multiConfig.enabled !== false;
|
|
138
|
+
const merged = mergeWhatsAppAccountConfig(runtime, normalizedAccountId);
|
|
139
|
+
const accountEnabled = merged.enabled !== false;
|
|
140
|
+
const enabled = baseEnabled && accountEnabled;
|
|
141
|
+
const { token, source: tokenSource } = resolveWhatsAppToken(runtime, normalizedAccountId);
|
|
142
|
+
const phoneNumberId = merged.phoneNumberId?.trim() || "";
|
|
143
|
+
const configured = Boolean(token && phoneNumberId);
|
|
144
|
+
return {
|
|
145
|
+
accountId: normalizedAccountId,
|
|
146
|
+
enabled,
|
|
147
|
+
name: merged.name?.trim() || undefined,
|
|
148
|
+
accessToken: token,
|
|
149
|
+
phoneNumberId,
|
|
150
|
+
businessAccountId: merged.businessAccountId?.trim() || undefined,
|
|
151
|
+
tokenSource,
|
|
152
|
+
configured,
|
|
153
|
+
config: merged
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function listEnabledWhatsAppAccounts(runtime) {
|
|
157
|
+
return listWhatsAppAccountIds(runtime).map((accountId) => resolveWhatsAppAccount(runtime, accountId)).filter((account) => account.enabled && account.configured);
|
|
158
|
+
}
|
|
159
|
+
function isMultiAccountEnabled(runtime) {
|
|
160
|
+
const accounts = listEnabledWhatsAppAccounts(runtime);
|
|
161
|
+
return accounts.length > 1;
|
|
162
|
+
}
|
|
163
|
+
function resolveWhatsAppGroupConfig(runtime, accountId, groupId) {
|
|
164
|
+
const multiConfig = getMultiAccountConfig(runtime);
|
|
165
|
+
const accountConfig = getAccountConfig(runtime, accountId);
|
|
166
|
+
const accountGroup = accountConfig?.groups?.[groupId];
|
|
167
|
+
if (accountGroup) {
|
|
168
|
+
return accountGroup;
|
|
169
|
+
}
|
|
170
|
+
return multiConfig.groups?.[groupId];
|
|
171
|
+
}
|
|
172
|
+
function isWhatsAppUserAllowed(params) {
|
|
173
|
+
const { identifier, accountConfig, isGroup, groupConfig } = params;
|
|
174
|
+
if (isGroup) {
|
|
175
|
+
const policy2 = accountConfig.groupPolicy ?? "allowlist";
|
|
176
|
+
if (policy2 === "disabled") {
|
|
232
177
|
return false;
|
|
233
178
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return source === "whatsapp";
|
|
237
|
-
};
|
|
238
|
-
try {
|
|
239
|
-
return Boolean(await __avLegacyValidate(runtime, message, state, options));
|
|
240
|
-
} catch {
|
|
241
|
-
return false;
|
|
179
|
+
if (policy2 === "open") {
|
|
180
|
+
return true;
|
|
242
181
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const accessToken = runtime.getSetting("WHATSAPP_ACCESS_TOKEN");
|
|
246
|
-
const phoneNumberId = runtime.getSetting("WHATSAPP_PHONE_NUMBER_ID");
|
|
247
|
-
const apiVersion = runtime.getSetting("WHATSAPP_API_VERSION") || "v24.0";
|
|
248
|
-
if (!accessToken || !phoneNumberId) {
|
|
249
|
-
if (callback) {
|
|
250
|
-
await callback({
|
|
251
|
-
text: "WhatsApp is not configured. Missing access token or phone number ID."
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
return { success: false, error: "WhatsApp not configured" };
|
|
182
|
+
if (groupConfig?.allowFrom?.length) {
|
|
183
|
+
return groupConfig.allowFrom.some((allowed) => String(allowed) === identifier);
|
|
255
184
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
state: currentState,
|
|
259
|
-
template: REACTION_TEMPLATE
|
|
260
|
-
});
|
|
261
|
-
let params;
|
|
262
|
-
try {
|
|
263
|
-
const response = await runtime.useModel(ModelType2.TEXT_SMALL, {
|
|
264
|
-
prompt
|
|
265
|
-
});
|
|
266
|
-
const parsed = parseJSONObjectFromText2(response);
|
|
267
|
-
if (!parsed || !parsed.messageId || !parsed.emoji) {
|
|
268
|
-
const messageId = message.content?.messageId;
|
|
269
|
-
if (!messageId) {
|
|
270
|
-
if (callback) {
|
|
271
|
-
await callback({
|
|
272
|
-
text: "Could not determine which message to react to"
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
return { success: false, error: "Missing message ID" };
|
|
276
|
-
}
|
|
277
|
-
params = { messageId, emoji: "\uD83D\uDC4D" };
|
|
278
|
-
} else {
|
|
279
|
-
params = parsed;
|
|
280
|
-
}
|
|
281
|
-
} catch {
|
|
282
|
-
if (callback) {
|
|
283
|
-
await callback({
|
|
284
|
-
text: "Failed to parse reaction parameters"
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
return { success: false, error: "Failed to parse reaction parameters" };
|
|
185
|
+
if (accountConfig.groupAllowFrom?.length) {
|
|
186
|
+
return accountConfig.groupAllowFrom.some((allowed) => String(allowed) === identifier);
|
|
288
187
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
188
|
+
return policy2 !== "allowlist";
|
|
189
|
+
}
|
|
190
|
+
const policy = accountConfig.dmPolicy ?? "pairing";
|
|
191
|
+
if (policy === "disabled") {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (policy === "open") {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
if (policy === "pairing") {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
if (accountConfig.allowFrom?.length) {
|
|
201
|
+
return accountConfig.allowFrom.some((allowed) => String(allowed) === identifier);
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
function isWhatsAppMentionRequired(params) {
|
|
206
|
+
const { groupConfig } = params;
|
|
207
|
+
return groupConfig?.requireMention ?? false;
|
|
208
|
+
}
|
|
209
|
+
async function checkWhatsAppUserAccess(params) {
|
|
210
|
+
const { runtime, identifier, accountConfig, isGroup, groupConfig, metadata } = params;
|
|
211
|
+
if (isGroup) {
|
|
212
|
+
const policy2 = accountConfig.groupPolicy ?? "allowlist";
|
|
213
|
+
if (policy2 === "disabled") {
|
|
214
|
+
return { allowed: false };
|
|
297
215
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const response = await fetch(url, {
|
|
301
|
-
method: "POST",
|
|
302
|
-
headers: {
|
|
303
|
-
Authorization: `Bearer ${accessToken}`,
|
|
304
|
-
"Content-Type": "application/json"
|
|
305
|
-
},
|
|
306
|
-
body: JSON.stringify({
|
|
307
|
-
messaging_product: "whatsapp",
|
|
308
|
-
recipient_type: "individual",
|
|
309
|
-
to,
|
|
310
|
-
type: "reaction",
|
|
311
|
-
reaction: {
|
|
312
|
-
message_id: params.messageId,
|
|
313
|
-
emoji: params.emoji
|
|
314
|
-
}
|
|
315
|
-
})
|
|
316
|
-
});
|
|
317
|
-
if (!response.ok) {
|
|
318
|
-
const errorData = await response.json();
|
|
319
|
-
throw new Error(errorData.error?.message || `HTTP ${response.status}`);
|
|
320
|
-
}
|
|
321
|
-
if (callback) {
|
|
322
|
-
await callback({
|
|
323
|
-
text: `Reacted with ${params.emoji}`,
|
|
324
|
-
action: WHATSAPP_SEND_REACTION_ACTION
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
return {
|
|
328
|
-
success: true,
|
|
329
|
-
data: {
|
|
330
|
-
action: WHATSAPP_SEND_REACTION_ACTION,
|
|
331
|
-
messageId: params.messageId,
|
|
332
|
-
emoji: params.emoji
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
} catch (error) {
|
|
336
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
337
|
-
if (callback) {
|
|
338
|
-
await callback({
|
|
339
|
-
text: `Failed to send reaction: ${errorMessage}`
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
return { success: false, error: errorMessage };
|
|
216
|
+
if (policy2 === "open") {
|
|
217
|
+
return { allowed: true };
|
|
343
218
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
219
|
+
if (groupConfig?.allowFrom?.length) {
|
|
220
|
+
const allowed = groupConfig.allowFrom.some((a) => String(a) === identifier);
|
|
221
|
+
return { allowed };
|
|
222
|
+
}
|
|
223
|
+
if (accountConfig.groupAllowFrom?.length) {
|
|
224
|
+
const allowed = accountConfig.groupAllowFrom.some((a) => String(a) === identifier);
|
|
225
|
+
return { allowed };
|
|
226
|
+
}
|
|
227
|
+
return { allowed: policy2 !== "allowlist" };
|
|
228
|
+
}
|
|
229
|
+
const policy = accountConfig.dmPolicy ?? "pairing";
|
|
230
|
+
if (policy === "disabled") {
|
|
231
|
+
return { allowed: false };
|
|
232
|
+
}
|
|
233
|
+
if (policy === "open") {
|
|
234
|
+
return { allowed: true };
|
|
235
|
+
}
|
|
236
|
+
if (policy === "pairing") {
|
|
237
|
+
const result = await checkPairingAllowed(runtime, {
|
|
238
|
+
channel: "whatsapp",
|
|
239
|
+
senderId: identifier,
|
|
240
|
+
metadata
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
allowed: result.allowed,
|
|
244
|
+
pairingCode: result.pairingCode,
|
|
245
|
+
newPairingRequest: result.newRequest,
|
|
246
|
+
replyMessage: result.replyMessage
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (accountConfig.allowFrom?.length) {
|
|
250
|
+
const allowed = accountConfig.allowFrom.some((a) => String(a) === identifier);
|
|
251
|
+
if (allowed) {
|
|
252
|
+
return { allowed: true };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const inDynamicAllowlist = await isInAllowlist(runtime, "whatsapp", identifier);
|
|
256
|
+
return { allowed: inDynamicAllowlist };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/connector-account-provider.ts
|
|
260
|
+
var WHATSAPP_PROVIDER_ID = "whatsapp";
|
|
261
|
+
function purposeForAccount(_account) {
|
|
262
|
+
return ["messaging"];
|
|
263
|
+
}
|
|
264
|
+
function accessGateForAccount(account) {
|
|
265
|
+
const dmPolicy = account.config.dmPolicy;
|
|
266
|
+
if (dmPolicy === "disabled")
|
|
267
|
+
return "disabled";
|
|
268
|
+
if (dmPolicy === "pairing")
|
|
269
|
+
return "pairing";
|
|
270
|
+
return "open";
|
|
271
|
+
}
|
|
272
|
+
function roleForAccount(_account) {
|
|
273
|
+
return "AGENT";
|
|
274
|
+
}
|
|
275
|
+
function toConnectorAccount(account) {
|
|
276
|
+
const now = Date.now();
|
|
277
|
+
return {
|
|
278
|
+
id: normalizeAccountId(account.accountId),
|
|
279
|
+
provider: WHATSAPP_PROVIDER_ID,
|
|
280
|
+
label: account.name ?? account.accountId,
|
|
281
|
+
role: roleForAccount(account),
|
|
282
|
+
purpose: purposeForAccount(account),
|
|
283
|
+
accessGate: accessGateForAccount(account),
|
|
284
|
+
status: account.enabled && account.configured ? "connected" : "disabled",
|
|
285
|
+
externalId: account.phoneNumberId || undefined,
|
|
286
|
+
displayHandle: account.phoneNumberId || undefined,
|
|
287
|
+
createdAt: now,
|
|
288
|
+
updatedAt: now,
|
|
289
|
+
metadata: {
|
|
290
|
+
tokenSource: account.tokenSource,
|
|
291
|
+
phoneNumberId: account.phoneNumberId,
|
|
292
|
+
businessAccountId: account.businessAccountId ?? null,
|
|
293
|
+
dmPolicy: account.config.dmPolicy ?? "pairing",
|
|
294
|
+
groupPolicy: account.config.groupPolicy ?? "allowlist"
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function createWhatsAppConnectorAccountProvider(runtime) {
|
|
299
|
+
return {
|
|
300
|
+
provider: WHATSAPP_PROVIDER_ID,
|
|
301
|
+
label: "WhatsApp",
|
|
302
|
+
listAccounts: async (_manager) => {
|
|
303
|
+
const enabled = listEnabledWhatsAppAccounts(runtime);
|
|
304
|
+
if (enabled.length > 0) {
|
|
305
|
+
return enabled.map(toConnectorAccount);
|
|
359
306
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
307
|
+
const fallback = resolveWhatsAppAccount(runtime, DEFAULT_ACCOUNT_ID);
|
|
308
|
+
return [toConnectorAccount(fallback)];
|
|
309
|
+
},
|
|
310
|
+
createAccount: async (input, _manager) => {
|
|
311
|
+
return {
|
|
312
|
+
...input,
|
|
313
|
+
provider: WHATSAPP_PROVIDER_ID,
|
|
314
|
+
role: input.role ?? "AGENT",
|
|
315
|
+
purpose: input.purpose ?? ["messaging"],
|
|
316
|
+
accessGate: input.accessGate ?? "open",
|
|
317
|
+
status: input.status ?? "pending"
|
|
318
|
+
};
|
|
319
|
+
},
|
|
320
|
+
patchAccount: async (_accountId, patch, _manager) => {
|
|
321
|
+
return { ...patch, provider: WHATSAPP_PROVIDER_ID };
|
|
322
|
+
},
|
|
323
|
+
deleteAccount: async (_accountId, _manager) => {}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/runtime-service.ts
|
|
328
|
+
import {
|
|
329
|
+
ChannelType,
|
|
330
|
+
createUniqueUuid,
|
|
331
|
+
lifeOpsPassiveConnectorsEnabled,
|
|
332
|
+
Service
|
|
333
|
+
} from "@elizaos/core";
|
|
334
|
+
|
|
363
335
|
// src/client.ts
|
|
364
336
|
import { EventEmitter } from "node:events";
|
|
365
|
-
|
|
337
|
+
|
|
338
|
+
// src/media.ts
|
|
339
|
+
var ALLOWED_MEDIA_PROTOCOLS = new Set(["http:", "https:"]);
|
|
340
|
+
function mediaLinkError(kind) {
|
|
341
|
+
return new Error(`${kind} message requires a valid http(s) media link`);
|
|
342
|
+
}
|
|
343
|
+
function assertValidWhatsAppMediaLink(link, kind) {
|
|
344
|
+
if (typeof link !== "string" || !link.trim()) {
|
|
345
|
+
throw mediaLinkError(kind);
|
|
346
|
+
}
|
|
347
|
+
let url;
|
|
348
|
+
try {
|
|
349
|
+
url = new URL(link.trim());
|
|
350
|
+
} catch {
|
|
351
|
+
throw mediaLinkError(kind);
|
|
352
|
+
}
|
|
353
|
+
if (!ALLOWED_MEDIA_PROTOCOLS.has(url.protocol) || url.username || url.password || !url.hostname) {
|
|
354
|
+
throw mediaLinkError(kind);
|
|
355
|
+
}
|
|
356
|
+
return url.toString();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/client.ts
|
|
366
360
|
var DEFAULT_API_VERSION = "v24.0";
|
|
367
361
|
|
|
368
362
|
class WhatsAppClient extends EventEmitter {
|
|
369
|
-
|
|
363
|
+
baseUrl;
|
|
364
|
+
headers;
|
|
370
365
|
config;
|
|
371
366
|
connectionStatus = "close";
|
|
372
367
|
constructor(config) {
|
|
373
368
|
super();
|
|
374
369
|
this.config = config;
|
|
375
370
|
const apiVersion = config.apiVersion || DEFAULT_API_VERSION;
|
|
376
|
-
this.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
});
|
|
371
|
+
this.baseUrl = `https://graph.facebook.com/${apiVersion}`;
|
|
372
|
+
this.headers = {
|
|
373
|
+
Authorization: `Bearer ${config.accessToken}`,
|
|
374
|
+
"Content-Type": "application/json"
|
|
375
|
+
};
|
|
383
376
|
}
|
|
384
377
|
async start() {
|
|
385
378
|
this.connectionStatus = "open";
|
|
@@ -399,7 +392,7 @@ class WhatsAppClient extends EventEmitter {
|
|
|
399
392
|
async sendMessage(message) {
|
|
400
393
|
const endpoint = `/${this.config.phoneNumberId}/messages`;
|
|
401
394
|
const payload = this.buildMessagePayload(message);
|
|
402
|
-
return this.
|
|
395
|
+
return this.post(endpoint, payload);
|
|
403
396
|
}
|
|
404
397
|
async sendTextMessage(to, text, _previewUrl = false) {
|
|
405
398
|
return this.sendMessage({
|
|
@@ -421,10 +414,10 @@ class WhatsAppClient extends EventEmitter {
|
|
|
421
414
|
}
|
|
422
415
|
};
|
|
423
416
|
try {
|
|
424
|
-
const response = await this.
|
|
417
|
+
const response = await this.post(endpoint, payload);
|
|
425
418
|
return {
|
|
426
419
|
success: true,
|
|
427
|
-
messageId: response.data.messages
|
|
420
|
+
messageId: response.data.messages[0]?.id
|
|
428
421
|
};
|
|
429
422
|
} catch (error) {
|
|
430
423
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -545,7 +538,7 @@ class WhatsAppClient extends EventEmitter {
|
|
|
545
538
|
message_id: messageId
|
|
546
539
|
};
|
|
547
540
|
try {
|
|
548
|
-
await this.
|
|
541
|
+
await this.post(endpoint, payload);
|
|
549
542
|
return true;
|
|
550
543
|
} catch {
|
|
551
544
|
return false;
|
|
@@ -553,7 +546,7 @@ class WhatsAppClient extends EventEmitter {
|
|
|
553
546
|
}
|
|
554
547
|
async getMediaUrl(mediaId) {
|
|
555
548
|
try {
|
|
556
|
-
const response = await this.
|
|
549
|
+
const response = await this.get(`/${mediaId}`);
|
|
557
550
|
return response.data.url || null;
|
|
558
551
|
} catch {
|
|
559
552
|
return null;
|
|
@@ -562,6 +555,44 @@ class WhatsAppClient extends EventEmitter {
|
|
|
562
555
|
async verifyWebhook(token) {
|
|
563
556
|
return token === this.config.webhookVerifyToken;
|
|
564
557
|
}
|
|
558
|
+
get(endpoint) {
|
|
559
|
+
return this.request(endpoint, { method: "GET" });
|
|
560
|
+
}
|
|
561
|
+
post(endpoint, payload) {
|
|
562
|
+
return this.request(endpoint, {
|
|
563
|
+
method: "POST",
|
|
564
|
+
body: JSON.stringify(payload)
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
async request(endpoint, init) {
|
|
568
|
+
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
|
|
569
|
+
const response = await fetch(`${this.baseUrl}/${normalizedEndpoint}`, {
|
|
570
|
+
...init,
|
|
571
|
+
headers: {
|
|
572
|
+
...this.headers,
|
|
573
|
+
...init.headers
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
const text = await response.text();
|
|
577
|
+
const data = text ? this.parseResponseBody(text) : undefined;
|
|
578
|
+
if (!response.ok) {
|
|
579
|
+
const detail = typeof data === "string" ? data : data ? JSON.stringify(data) : response.statusText;
|
|
580
|
+
throw new Error(`WhatsApp Cloud API request failed (${response.status} ${response.statusText}): ${detail}`);
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
data,
|
|
584
|
+
status: response.status,
|
|
585
|
+
statusText: response.statusText,
|
|
586
|
+
headers: response.headers
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
parseResponseBody(text) {
|
|
590
|
+
try {
|
|
591
|
+
return JSON.parse(text);
|
|
592
|
+
} catch {
|
|
593
|
+
return text;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
565
596
|
buildMessagePayload(message) {
|
|
566
597
|
const basePayload = {
|
|
567
598
|
messaging_product: "whatsapp",
|
|
@@ -591,7 +622,7 @@ class WhatsAppClient extends EventEmitter {
|
|
|
591
622
|
...basePayload,
|
|
592
623
|
...contextPayload,
|
|
593
624
|
image: {
|
|
594
|
-
link: imageContent.link,
|
|
625
|
+
link: assertValidWhatsAppMediaLink(imageContent.link, "image"),
|
|
595
626
|
caption: imageContent.caption
|
|
596
627
|
}
|
|
597
628
|
};
|
|
@@ -602,7 +633,7 @@ class WhatsAppClient extends EventEmitter {
|
|
|
602
633
|
...basePayload,
|
|
603
634
|
...contextPayload,
|
|
604
635
|
video: {
|
|
605
|
-
link: videoContent.link,
|
|
636
|
+
link: assertValidWhatsAppMediaLink(videoContent.link, "video"),
|
|
606
637
|
caption: videoContent.caption
|
|
607
638
|
}
|
|
608
639
|
};
|
|
@@ -613,7 +644,7 @@ class WhatsAppClient extends EventEmitter {
|
|
|
613
644
|
...basePayload,
|
|
614
645
|
...contextPayload,
|
|
615
646
|
audio: {
|
|
616
|
-
link: audioContent.link
|
|
647
|
+
link: assertValidWhatsAppMediaLink(audioContent.link, "audio")
|
|
617
648
|
}
|
|
618
649
|
};
|
|
619
650
|
}
|
|
@@ -623,7 +654,7 @@ class WhatsAppClient extends EventEmitter {
|
|
|
623
654
|
...basePayload,
|
|
624
655
|
...contextPayload,
|
|
625
656
|
document: {
|
|
626
|
-
link: docContent.link,
|
|
657
|
+
link: assertValidWhatsAppMediaLink(docContent.link, "document"),
|
|
627
658
|
filename: docContent.filename,
|
|
628
659
|
caption: docContent.caption
|
|
629
660
|
}
|
|
@@ -666,24 +697,6 @@ class WhatsAppClient extends EventEmitter {
|
|
|
666
697
|
}
|
|
667
698
|
}
|
|
668
699
|
|
|
669
|
-
// src/utils/config-detector.ts
|
|
670
|
-
function detectAuthMethod(config) {
|
|
671
|
-
const explicitMethod = config.authMethod;
|
|
672
|
-
if (explicitMethod !== undefined) {
|
|
673
|
-
if (explicitMethod === "baileys" || explicitMethod === "cloudapi") {
|
|
674
|
-
return explicitMethod;
|
|
675
|
-
}
|
|
676
|
-
throw new Error(`Invalid authMethod: "${String(explicitMethod)}". Must be either "baileys" or "cloudapi".`);
|
|
677
|
-
}
|
|
678
|
-
if ("authDir" in config && config.authDir) {
|
|
679
|
-
return "baileys";
|
|
680
|
-
}
|
|
681
|
-
if ("accessToken" in config && "phoneNumberId" in config) {
|
|
682
|
-
return "cloudapi";
|
|
683
|
-
}
|
|
684
|
-
throw new Error("Cannot detect auth method. Provide either authDir (Baileys) or accessToken + phoneNumberId (Cloud API).");
|
|
685
|
-
}
|
|
686
|
-
|
|
687
700
|
// src/clients/baileys-client.ts
|
|
688
701
|
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
689
702
|
|
|
@@ -712,9 +725,7 @@ class BaileysAuthManager {
|
|
|
712
725
|
|
|
713
726
|
// src/baileys/connection.ts
|
|
714
727
|
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
715
|
-
import makeWASocket, {
|
|
716
|
-
DisconnectReason
|
|
717
|
-
} from "@whiskeysockets/baileys";
|
|
728
|
+
import makeWASocket, { DisconnectReason } from "@whiskeysockets/baileys";
|
|
718
729
|
import pino from "pino";
|
|
719
730
|
|
|
720
731
|
class BaileysConnection extends EventEmitter2 {
|
|
@@ -807,8 +818,9 @@ class BaileysConnection extends EventEmitter2 {
|
|
|
807
818
|
if (!this.socket) {
|
|
808
819
|
return;
|
|
809
820
|
}
|
|
810
|
-
this.socket
|
|
811
|
-
|
|
821
|
+
const socket = this.socket;
|
|
822
|
+
socket.ev.removeAllListeners();
|
|
823
|
+
socket.ws.close();
|
|
812
824
|
this.socket = undefined;
|
|
813
825
|
this.connectionStatus = "close";
|
|
814
826
|
this.emit("connection", "close");
|
|
@@ -817,13 +829,18 @@ class BaileysConnection extends EventEmitter2 {
|
|
|
817
829
|
|
|
818
830
|
// src/baileys/message-adapter.ts
|
|
819
831
|
class MessageAdapter {
|
|
820
|
-
|
|
832
|
+
toNormalized(msg) {
|
|
833
|
+
const chatId = msg.key?.remoteJid ?? "";
|
|
834
|
+
const senderId = msg.key?.participant ?? chatId;
|
|
821
835
|
return {
|
|
822
836
|
id: msg.key?.id ?? "",
|
|
823
|
-
from:
|
|
837
|
+
from: chatId,
|
|
824
838
|
timestamp: Number(msg.messageTimestamp ?? 0),
|
|
825
839
|
type: this.detectType(msg),
|
|
826
|
-
content: this.extractContent(msg)
|
|
840
|
+
content: this.extractContent(msg),
|
|
841
|
+
chatId,
|
|
842
|
+
senderId,
|
|
843
|
+
replyToId: this.extractReplyToId(msg)
|
|
827
844
|
};
|
|
828
845
|
}
|
|
829
846
|
toBaileys(msg) {
|
|
@@ -841,30 +858,24 @@ class MessageAdapter {
|
|
|
841
858
|
case "template":
|
|
842
859
|
return { text: this.renderTemplate(msg.content) };
|
|
843
860
|
default:
|
|
844
|
-
throw new Error(`Message type ${msg.type} is
|
|
861
|
+
throw new Error(`Message type ${msg.type} is outside the Baileys adapter contract`);
|
|
845
862
|
}
|
|
846
863
|
}
|
|
847
864
|
mediaWithCaption(key, media) {
|
|
848
|
-
|
|
849
|
-
throw new Error(`${key} message requires a media link`);
|
|
850
|
-
}
|
|
865
|
+
const link = assertValidWhatsAppMediaLink(media.link, key);
|
|
851
866
|
return {
|
|
852
|
-
[key]: { url:
|
|
867
|
+
[key]: { url: link },
|
|
853
868
|
...media.caption ? { caption: media.caption } : {}
|
|
854
869
|
};
|
|
855
870
|
}
|
|
856
871
|
mediaNoCaption(key, media) {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
}
|
|
860
|
-
return { [key]: { url: media.link } };
|
|
872
|
+
const link = assertValidWhatsAppMediaLink(media.link, key);
|
|
873
|
+
return { [key]: { url: link } };
|
|
861
874
|
}
|
|
862
875
|
mediaWithFilename(media) {
|
|
863
|
-
|
|
864
|
-
throw new Error("document message requires a media link");
|
|
865
|
-
}
|
|
876
|
+
const link = assertValidWhatsAppMediaLink(media.link, "document");
|
|
866
877
|
return {
|
|
867
|
-
document: { url:
|
|
878
|
+
document: { url: link },
|
|
868
879
|
...media.filename ? { fileName: media.filename } : {},
|
|
869
880
|
...media.caption ? { caption: media.caption } : {}
|
|
870
881
|
};
|
|
@@ -888,7 +899,11 @@ class MessageAdapter {
|
|
|
888
899
|
return "text";
|
|
889
900
|
}
|
|
890
901
|
extractContent(msg) {
|
|
891
|
-
return msg.message?.conversation ?? msg.message?.extendedTextMessage?.text ?? "";
|
|
902
|
+
return msg.message?.conversation ?? msg.message?.extendedTextMessage?.text ?? msg.message?.imageMessage?.caption ?? msg.message?.videoMessage?.caption ?? msg.message?.documentMessage?.caption ?? "";
|
|
903
|
+
}
|
|
904
|
+
extractReplyToId(msg) {
|
|
905
|
+
const contextInfo = msg.message?.extendedTextMessage?.contextInfo ?? msg.message?.imageMessage?.contextInfo ?? msg.message?.videoMessage?.contextInfo ?? msg.message?.documentMessage?.contextInfo;
|
|
906
|
+
return typeof contextInfo?.stanzaId === "string" ? contextInfo.stanzaId : undefined;
|
|
892
907
|
}
|
|
893
908
|
renderTemplate(template) {
|
|
894
909
|
const params = template.components?.flatMap((component) => component.parameters.map((parameter) => parameter.text).filter(Boolean));
|
|
@@ -898,7 +913,7 @@ class MessageAdapter {
|
|
|
898
913
|
|
|
899
914
|
// src/baileys/qr-code.ts
|
|
900
915
|
import QRCode from "qrcode";
|
|
901
|
-
import QRCodeTerminal from "qrcode-terminal";
|
|
916
|
+
import * as QRCodeTerminal from "qrcode-terminal";
|
|
902
917
|
|
|
903
918
|
class QRCodeGenerator {
|
|
904
919
|
async generate(qrString) {
|
|
@@ -958,7 +973,7 @@ class BaileysClient extends EventEmitter3 {
|
|
|
958
973
|
for (const message of messages) {
|
|
959
974
|
const maybe = message;
|
|
960
975
|
if (!maybe.key?.fromMe && maybe.message) {
|
|
961
|
-
this.emit("message", this.adapter.
|
|
976
|
+
this.emit("message", this.adapter.toNormalized(message));
|
|
962
977
|
}
|
|
963
978
|
}
|
|
964
979
|
});
|
|
@@ -989,514 +1004,2143 @@ class BaileysClient extends EventEmitter3 {
|
|
|
989
1004
|
getConnectionStatus() {
|
|
990
1005
|
return this.connection.getStatus();
|
|
991
1006
|
}
|
|
1007
|
+
getPhoneNumber() {
|
|
1008
|
+
return this.connection.getSocket()?.user?.id?.split(":")[0] ?? null;
|
|
1009
|
+
}
|
|
992
1010
|
}
|
|
993
1011
|
|
|
994
|
-
// src/
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1012
|
+
// src/normalize.ts
|
|
1013
|
+
var WHATSAPP_TEXT_CHUNK_LIMIT = 4096;
|
|
1014
|
+
var WHATSAPP_USER_JID_RE = /^(\d+)(?::\d+)?@s\.whatsapp\.net$/i;
|
|
1015
|
+
var WHATSAPP_LID_RE = /^(\d+)@lid$/i;
|
|
1016
|
+
function stripWhatsAppTargetPrefixes(value) {
|
|
1017
|
+
let candidate = value.trim();
|
|
1018
|
+
for (;; ) {
|
|
1019
|
+
const before = candidate;
|
|
1020
|
+
candidate = candidate.replace(/^whatsapp:/i, "").trim();
|
|
1021
|
+
if (candidate === before) {
|
|
1022
|
+
return candidate;
|
|
1000
1023
|
}
|
|
1001
|
-
return new WhatsAppClient(config);
|
|
1002
1024
|
}
|
|
1003
1025
|
}
|
|
1026
|
+
function normalizeE164(input) {
|
|
1027
|
+
const stripped = input.replace(/[\s\-().]+/g, "");
|
|
1028
|
+
const digitsOnly = stripped.replace(/[^\d+]/g, "");
|
|
1029
|
+
if (!digitsOnly) {
|
|
1030
|
+
return "";
|
|
1031
|
+
}
|
|
1032
|
+
if (digitsOnly.startsWith("+")) {
|
|
1033
|
+
return digitsOnly;
|
|
1034
|
+
}
|
|
1035
|
+
if (digitsOnly.startsWith("00")) {
|
|
1036
|
+
return `+${digitsOnly.slice(2)}`;
|
|
1037
|
+
}
|
|
1038
|
+
if (digitsOnly.length >= 10) {
|
|
1039
|
+
return `+${digitsOnly}`;
|
|
1040
|
+
}
|
|
1041
|
+
return digitsOnly;
|
|
1042
|
+
}
|
|
1043
|
+
function isWhatsAppGroupJid(value) {
|
|
1044
|
+
const candidate = stripWhatsAppTargetPrefixes(value);
|
|
1045
|
+
const lower = candidate.toLowerCase();
|
|
1046
|
+
if (!lower.endsWith("@g.us")) {
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
const localPart = candidate.slice(0, candidate.length - "@g.us".length);
|
|
1050
|
+
if (!localPart || localPart.includes("@")) {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
return /^[0-9]+(-[0-9]+)*$/.test(localPart);
|
|
1054
|
+
}
|
|
1055
|
+
function isWhatsAppUserTarget(value) {
|
|
1056
|
+
const candidate = stripWhatsAppTargetPrefixes(value);
|
|
1057
|
+
return WHATSAPP_USER_JID_RE.test(candidate) || WHATSAPP_LID_RE.test(candidate);
|
|
1058
|
+
}
|
|
1059
|
+
function extractUserJidPhone(jid) {
|
|
1060
|
+
const userMatch = jid.match(WHATSAPP_USER_JID_RE);
|
|
1061
|
+
if (userMatch) {
|
|
1062
|
+
return userMatch[1];
|
|
1063
|
+
}
|
|
1064
|
+
const lidMatch = jid.match(WHATSAPP_LID_RE);
|
|
1065
|
+
if (lidMatch) {
|
|
1066
|
+
return lidMatch[1];
|
|
1067
|
+
}
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
function normalizeWhatsAppTarget(value) {
|
|
1071
|
+
const candidate = stripWhatsAppTargetPrefixes(value);
|
|
1072
|
+
if (!candidate) {
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
if (isWhatsAppGroupJid(candidate)) {
|
|
1076
|
+
const localPart = candidate.slice(0, candidate.length - "@g.us".length);
|
|
1077
|
+
return `${localPart}@g.us`;
|
|
1078
|
+
}
|
|
1079
|
+
if (isWhatsAppUserTarget(candidate)) {
|
|
1080
|
+
const phone = extractUserJidPhone(candidate);
|
|
1081
|
+
if (!phone) {
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
const normalized2 = normalizeE164(phone);
|
|
1085
|
+
return normalized2.length > 1 ? normalized2 : null;
|
|
1086
|
+
}
|
|
1087
|
+
if (candidate.includes("@")) {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
const normalized = normalizeE164(candidate);
|
|
1091
|
+
return normalized.length > 1 ? normalized : null;
|
|
1092
|
+
}
|
|
1093
|
+
function formatWhatsAppId(id) {
|
|
1094
|
+
if (isWhatsAppGroupJid(id)) {
|
|
1095
|
+
return `group:${id}`;
|
|
1096
|
+
}
|
|
1097
|
+
const normalized = normalizeWhatsAppTarget(id);
|
|
1098
|
+
return normalized || id;
|
|
1099
|
+
}
|
|
1100
|
+
function isWhatsAppGroup(id) {
|
|
1101
|
+
return isWhatsAppGroupJid(id);
|
|
1102
|
+
}
|
|
1103
|
+
function getWhatsAppChatType(id) {
|
|
1104
|
+
return isWhatsAppGroupJid(id) ? "group" : "user";
|
|
1105
|
+
}
|
|
1106
|
+
function buildWhatsAppUserJid(phoneNumber) {
|
|
1107
|
+
const normalized = normalizeE164(phoneNumber);
|
|
1108
|
+
const digits = normalized.replace(/^\+/, "");
|
|
1109
|
+
return `${digits}@s.whatsapp.net`;
|
|
1110
|
+
}
|
|
1111
|
+
function splitAtBreakPoint(text, limit) {
|
|
1112
|
+
if (text.length <= limit) {
|
|
1113
|
+
return { chunk: text, remainder: "" };
|
|
1114
|
+
}
|
|
1115
|
+
const searchArea = text.slice(0, limit);
|
|
1116
|
+
const doubleNewline = searchArea.lastIndexOf(`
|
|
1117
|
+
|
|
1118
|
+
`);
|
|
1119
|
+
if (doubleNewline > limit * 0.5) {
|
|
1120
|
+
return {
|
|
1121
|
+
chunk: text.slice(0, doubleNewline).trimEnd(),
|
|
1122
|
+
remainder: text.slice(doubleNewline + 2).trimStart()
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
const singleNewline = searchArea.lastIndexOf(`
|
|
1126
|
+
`);
|
|
1127
|
+
if (singleNewline > limit * 0.5) {
|
|
1128
|
+
return {
|
|
1129
|
+
chunk: text.slice(0, singleNewline).trimEnd(),
|
|
1130
|
+
remainder: text.slice(singleNewline + 1).trimStart()
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
const sentenceEnd = Math.max(searchArea.lastIndexOf(". "), searchArea.lastIndexOf("! "), searchArea.lastIndexOf("? "));
|
|
1134
|
+
if (sentenceEnd > limit * 0.5) {
|
|
1135
|
+
return {
|
|
1136
|
+
chunk: text.slice(0, sentenceEnd + 1).trimEnd(),
|
|
1137
|
+
remainder: text.slice(sentenceEnd + 2).trimStart()
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
const space = searchArea.lastIndexOf(" ");
|
|
1141
|
+
if (space > limit * 0.5) {
|
|
1142
|
+
return {
|
|
1143
|
+
chunk: text.slice(0, space).trimEnd(),
|
|
1144
|
+
remainder: text.slice(space + 1).trimStart()
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
chunk: text.slice(0, limit),
|
|
1149
|
+
remainder: text.slice(limit)
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
function chunkWhatsAppText(text, opts = {}) {
|
|
1153
|
+
const limit = opts.limit ?? WHATSAPP_TEXT_CHUNK_LIMIT;
|
|
1154
|
+
if (!text.trim()) {
|
|
1155
|
+
return [];
|
|
1156
|
+
}
|
|
1157
|
+
const normalizedText = text.trim();
|
|
1158
|
+
if (normalizedText.length <= limit) {
|
|
1159
|
+
return [normalizedText];
|
|
1160
|
+
}
|
|
1161
|
+
const chunks = [];
|
|
1162
|
+
let remaining = normalizedText;
|
|
1163
|
+
while (remaining.length > 0) {
|
|
1164
|
+
const { chunk, remainder } = splitAtBreakPoint(remaining, limit);
|
|
1165
|
+
if (chunk) {
|
|
1166
|
+
chunks.push(chunk);
|
|
1167
|
+
}
|
|
1168
|
+
remaining = remainder;
|
|
1169
|
+
}
|
|
1170
|
+
return chunks.filter((c) => c.length > 0);
|
|
1171
|
+
}
|
|
1172
|
+
function truncateText(text, maxLength) {
|
|
1173
|
+
if (text.length <= maxLength) {
|
|
1174
|
+
return text;
|
|
1175
|
+
}
|
|
1176
|
+
if (maxLength <= 3) {
|
|
1177
|
+
return "...".slice(0, maxLength);
|
|
1178
|
+
}
|
|
1179
|
+
return `${text.slice(0, maxLength - 3)}...`;
|
|
1180
|
+
}
|
|
1181
|
+
function resolveWhatsAppSystemLocation(params) {
|
|
1182
|
+
const { chatType, chatId, chatName } = params;
|
|
1183
|
+
const name = chatName || chatId.slice(0, 8);
|
|
1184
|
+
return `WhatsApp ${chatType}:${name}`;
|
|
1185
|
+
}
|
|
1186
|
+
function isValidWhatsAppNumber(value) {
|
|
1187
|
+
const normalized = normalizeWhatsAppTarget(value);
|
|
1188
|
+
if (!normalized) {
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
if (!normalized.startsWith("+")) {
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
const digits = normalized.replace(/^\+/, "");
|
|
1195
|
+
return /^\d{10,15}$/.test(digits);
|
|
1196
|
+
}
|
|
1197
|
+
function formatWhatsAppPhoneNumber(phoneNumber) {
|
|
1198
|
+
const normalized = normalizeE164(phoneNumber);
|
|
1199
|
+
if (!normalized) {
|
|
1200
|
+
return phoneNumber;
|
|
1201
|
+
}
|
|
1202
|
+
const digits = normalized.replace(/^\+/, "");
|
|
1203
|
+
if (digits.length <= 10) {
|
|
1204
|
+
return normalized;
|
|
1205
|
+
}
|
|
1206
|
+
const countryCode = digits.slice(0, digits.length - 10);
|
|
1207
|
+
const rest = digits.slice(-10);
|
|
1208
|
+
return `+${countryCode} ${rest.slice(0, 3)} ${rest.slice(3, 6)} ${rest.slice(6)}`;
|
|
1209
|
+
}
|
|
1004
1210
|
|
|
1005
|
-
// src/
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1211
|
+
// src/runtime-service.ts
|
|
1212
|
+
function readStringSetting(runtime, key) {
|
|
1213
|
+
const value = runtime.getSetting(key);
|
|
1214
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1215
|
+
return value.trim();
|
|
1010
1216
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (response && typeof response === "object" && "data" in response) {
|
|
1015
|
-
return response.data;
|
|
1016
|
-
}
|
|
1017
|
-
return response;
|
|
1018
|
-
} catch (error) {
|
|
1019
|
-
if (error instanceof Error) {
|
|
1020
|
-
throw new Error(`Failed to send WhatsApp message: ${error.message}`);
|
|
1021
|
-
}
|
|
1022
|
-
throw new Error("Failed to send WhatsApp message");
|
|
1023
|
-
}
|
|
1217
|
+
const envValue = process.env[key];
|
|
1218
|
+
if (typeof envValue === "string" && envValue.trim().length > 0) {
|
|
1219
|
+
return envValue.trim();
|
|
1024
1220
|
}
|
|
1221
|
+
return;
|
|
1025
1222
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
if (event.entry?.[0]?.changes?.[0]?.value?.messages) {
|
|
1031
|
-
const messages = event.entry[0].changes[0].value.messages;
|
|
1032
|
-
for (const message of messages) {
|
|
1033
|
-
await this.handleMessage(message);
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
if (event.entry?.[0]?.changes?.[0]?.value?.statuses) {
|
|
1037
|
-
const statuses = event.entry[0].changes[0].value.statuses;
|
|
1038
|
-
for (const status of statuses) {
|
|
1039
|
-
await this.handleStatus(status);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
} catch (error) {
|
|
1043
|
-
if (error instanceof Error) {
|
|
1044
|
-
throw new Error(`Failed to send WhatsApp message: ${error.message}`);
|
|
1045
|
-
}
|
|
1046
|
-
throw new Error("Failed to send WhatsApp message");
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
async handleMessage(message) {
|
|
1050
|
-
console.log("Received message:", message);
|
|
1051
|
-
}
|
|
1052
|
-
async handleStatus(status) {
|
|
1053
|
-
console.log("Received status update:", status);
|
|
1223
|
+
function readCsvSetting(runtime, key) {
|
|
1224
|
+
const value = readStringSetting(runtime, key);
|
|
1225
|
+
if (!value) {
|
|
1226
|
+
return [];
|
|
1054
1227
|
}
|
|
1228
|
+
return value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
1055
1229
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1230
|
+
function resolveRuntimeConfig(runtime) {
|
|
1231
|
+
const dmPolicy = readStringSetting(runtime, "WHATSAPP_DM_POLICY");
|
|
1232
|
+
const groupPolicy = readStringSetting(runtime, "WHATSAPP_GROUP_POLICY");
|
|
1233
|
+
const allowFrom = readCsvSetting(runtime, "WHATSAPP_ALLOW_FROM");
|
|
1234
|
+
const groupAllowFrom = readCsvSetting(runtime, "WHATSAPP_GROUP_ALLOW_FROM");
|
|
1235
|
+
const authDir = readStringSetting(runtime, "WHATSAPP_AUTH_DIR") ?? readStringSetting(runtime, "WHATSAPP_SESSION_PATH");
|
|
1236
|
+
if (authDir) {
|
|
1237
|
+
return {
|
|
1238
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
1239
|
+
transport: "baileys",
|
|
1240
|
+
authDir,
|
|
1241
|
+
dmPolicy,
|
|
1242
|
+
groupPolicy,
|
|
1243
|
+
allowFrom,
|
|
1244
|
+
groupAllowFrom
|
|
1245
|
+
};
|
|
1065
1246
|
}
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1247
|
+
const accessToken = readStringSetting(runtime, "WHATSAPP_ACCESS_TOKEN");
|
|
1248
|
+
const phoneNumberId = readStringSetting(runtime, "WHATSAPP_PHONE_NUMBER_ID");
|
|
1249
|
+
if (accessToken && phoneNumberId) {
|
|
1250
|
+
return {
|
|
1251
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
1252
|
+
transport: "cloudapi",
|
|
1253
|
+
accessToken,
|
|
1254
|
+
phoneNumberId,
|
|
1255
|
+
webhookVerifyToken: readStringSetting(runtime, "WHATSAPP_WEBHOOK_VERIFY_TOKEN"),
|
|
1256
|
+
apiVersion: readStringSetting(runtime, "WHATSAPP_API_VERSION"),
|
|
1257
|
+
dmPolicy,
|
|
1258
|
+
groupPolicy,
|
|
1259
|
+
allowFrom,
|
|
1260
|
+
groupAllowFrom
|
|
1261
|
+
};
|
|
1069
1262
|
}
|
|
1070
|
-
return
|
|
1263
|
+
return null;
|
|
1071
1264
|
}
|
|
1072
|
-
function
|
|
1073
|
-
const
|
|
1265
|
+
function configuredAccountForId(config, accountId) {
|
|
1266
|
+
const normalized = normalizeAccountId(accountId);
|
|
1267
|
+
const accountConfig = config.accounts?.[accountId] ?? Object.entries(config.accounts ?? {}).find(([key]) => normalizeAccountId(key) === normalized)?.[1] ?? {};
|
|
1074
1268
|
return {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
webhookVerifyToken: characterWhatsApp?.webhookVerifyToken,
|
|
1080
|
-
apiVersion: characterWhatsApp?.apiVersion,
|
|
1081
|
-
dmPolicy: characterWhatsApp?.dmPolicy,
|
|
1082
|
-
groupPolicy: characterWhatsApp?.groupPolicy,
|
|
1083
|
-
mediaMaxMb: characterWhatsApp?.mediaMaxMb,
|
|
1084
|
-
textChunkLimit: characterWhatsApp?.textChunkLimit,
|
|
1085
|
-
accounts: characterWhatsApp?.accounts,
|
|
1086
|
-
groups: characterWhatsApp?.groups
|
|
1269
|
+
...config,
|
|
1270
|
+
accounts: undefined,
|
|
1271
|
+
groups: undefined,
|
|
1272
|
+
...accountConfig
|
|
1087
1273
|
};
|
|
1088
1274
|
}
|
|
1089
|
-
function
|
|
1090
|
-
const
|
|
1091
|
-
const
|
|
1092
|
-
const
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1275
|
+
function resolveRuntimeConfigs(runtime) {
|
|
1276
|
+
const multiConfig = getMultiAccountConfig(runtime);
|
|
1277
|
+
const accountIds = listWhatsAppAccountIds(runtime);
|
|
1278
|
+
const configs = [];
|
|
1279
|
+
for (const accountId of accountIds) {
|
|
1280
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
1281
|
+
const accountConfig = configuredAccountForId(multiConfig, normalizedAccountId);
|
|
1282
|
+
const authDir = accountConfig.authDir?.trim();
|
|
1283
|
+
const transport = accountConfig.transport ?? (authDir ? "baileys" : "cloudapi");
|
|
1284
|
+
if (transport === "baileys" && authDir) {
|
|
1285
|
+
configs.push({
|
|
1286
|
+
accountId: normalizedAccountId,
|
|
1287
|
+
name: accountConfig.name?.trim() || undefined,
|
|
1288
|
+
transport: "baileys",
|
|
1289
|
+
authDir,
|
|
1290
|
+
dmPolicy: accountConfig.dmPolicy,
|
|
1291
|
+
groupPolicy: accountConfig.groupPolicy,
|
|
1292
|
+
allowFrom: accountConfig.allowFrom?.map(String),
|
|
1293
|
+
groupAllowFrom: accountConfig.groupAllowFrom?.map(String)
|
|
1294
|
+
});
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
const cloud = resolveWhatsAppAccount(runtime, normalizedAccountId);
|
|
1298
|
+
if (cloud.enabled && cloud.configured) {
|
|
1299
|
+
configs.push({
|
|
1300
|
+
accountId: normalizedAccountId,
|
|
1301
|
+
name: cloud.name,
|
|
1302
|
+
transport: "cloudapi",
|
|
1303
|
+
accessToken: cloud.accessToken,
|
|
1304
|
+
phoneNumberId: cloud.phoneNumberId,
|
|
1305
|
+
businessAccountId: cloud.businessAccountId,
|
|
1306
|
+
webhookVerifyToken: cloud.config.webhookVerifyToken,
|
|
1307
|
+
apiVersion: cloud.config.apiVersion,
|
|
1308
|
+
dmPolicy: cloud.config.dmPolicy,
|
|
1309
|
+
groupPolicy: cloud.config.groupPolicy,
|
|
1310
|
+
allowFrom: cloud.config.allowFrom?.map(String),
|
|
1311
|
+
groupAllowFrom: cloud.config.groupAllowFrom?.map(String)
|
|
1312
|
+
});
|
|
1105
1313
|
}
|
|
1106
1314
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
return [DEFAULT_ACCOUNT_ID];
|
|
1315
|
+
if (configs.length > 0) {
|
|
1316
|
+
return configs;
|
|
1110
1317
|
}
|
|
1111
|
-
|
|
1318
|
+
const legacy = resolveRuntimeConfig(runtime);
|
|
1319
|
+
return legacy ? [legacy] : [];
|
|
1112
1320
|
}
|
|
1113
|
-
function
|
|
1114
|
-
const
|
|
1115
|
-
if (
|
|
1116
|
-
return
|
|
1321
|
+
function toTimestampMs(value) {
|
|
1322
|
+
const parsed = Number(value);
|
|
1323
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1324
|
+
return Date.now();
|
|
1117
1325
|
}
|
|
1118
|
-
return
|
|
1326
|
+
return parsed >= 1000000000000 ? parsed : parsed * 1000;
|
|
1119
1327
|
}
|
|
1120
|
-
function
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1328
|
+
function toMemoryId(runtime, chatId, messageId) {
|
|
1329
|
+
return createUniqueUuid(runtime, `whatsapp:${chatId}:${messageId}`);
|
|
1330
|
+
}
|
|
1331
|
+
function readTargetAccountId(target) {
|
|
1332
|
+
return target?.accountId;
|
|
1333
|
+
}
|
|
1334
|
+
function readContextAccountId(context) {
|
|
1335
|
+
return context?.accountId;
|
|
1336
|
+
}
|
|
1337
|
+
function targetWithAccount(target, accountId) {
|
|
1338
|
+
return { ...target, accountId };
|
|
1339
|
+
}
|
|
1340
|
+
function registerMessageConnectorIfAvailable(runtime, registration) {
|
|
1341
|
+
const withRegistry = runtime;
|
|
1342
|
+
if (typeof withRegistry.registerMessageConnector === "function") {
|
|
1343
|
+
withRegistry.registerMessageConnector(registration);
|
|
1124
1344
|
return;
|
|
1125
1345
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
return direct;
|
|
1346
|
+
if (registration.sendHandler) {
|
|
1347
|
+
runtime.registerSendHandler(registration.source, registration.sendHandler);
|
|
1129
1348
|
}
|
|
1130
|
-
const normalized = normalizeAccountId(accountId);
|
|
1131
|
-
const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized);
|
|
1132
|
-
return matchKey ? accounts[matchKey] : undefined;
|
|
1133
1349
|
}
|
|
1134
|
-
function
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
if (accountConfig?.accessToken?.trim()) {
|
|
1138
|
-
return { token: accountConfig.accessToken.trim(), source: "config" };
|
|
1350
|
+
function normalizeBaileysSendTarget(target) {
|
|
1351
|
+
if (isWhatsAppGroupJid(target) || isWhatsAppUserTarget(target)) {
|
|
1352
|
+
return target;
|
|
1139
1353
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1354
|
+
const normalized = normalizeWhatsAppTarget(target);
|
|
1355
|
+
return normalized ? buildWhatsAppUserJid(normalized) : target;
|
|
1356
|
+
}
|
|
1357
|
+
function normalizeWhatsAppConnectorTarget(value) {
|
|
1358
|
+
const trimmed = value.trim().replace(/^whatsapp:/i, "").trim();
|
|
1359
|
+
if (!trimmed)
|
|
1360
|
+
return "";
|
|
1361
|
+
if (isWhatsAppGroupJid(trimmed) || isWhatsAppUserTarget(trimmed)) {
|
|
1362
|
+
return trimmed;
|
|
1148
1363
|
}
|
|
1149
|
-
return
|
|
1364
|
+
return normalizeWhatsAppTarget(trimmed) ?? trimmed;
|
|
1150
1365
|
}
|
|
1151
|
-
function
|
|
1152
|
-
return
|
|
1366
|
+
function isWhatsAppAddress(value) {
|
|
1367
|
+
return isWhatsAppGroupJid(value) || isWhatsAppUserTarget(value) || normalizeWhatsAppTarget(value) !== null;
|
|
1153
1368
|
}
|
|
1154
|
-
function
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1369
|
+
function normalizedSearchText(value) {
|
|
1370
|
+
return (value ?? "").toLowerCase().replace(/[^a-z0-9@+._-]+/g, " ").trim();
|
|
1371
|
+
}
|
|
1372
|
+
function matchesQuery(query, ...values) {
|
|
1373
|
+
const normalizedQuery = normalizedSearchText(query);
|
|
1374
|
+
if (!normalizedQuery)
|
|
1375
|
+
return true;
|
|
1376
|
+
const normalizedTargetQuery = normalizedSearchText(normalizeWhatsAppConnectorTarget(query));
|
|
1377
|
+
return values.some((value) => {
|
|
1378
|
+
const normalizedValue = normalizedSearchText(value);
|
|
1379
|
+
return normalizedValue.includes(normalizedQuery) || normalizedTargetQuery.length > 0 && normalizedValue.includes(normalizedTargetQuery);
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
function whatsappTargetKind(value) {
|
|
1383
|
+
if (isWhatsAppGroupJid(value))
|
|
1384
|
+
return "group";
|
|
1385
|
+
if (/^\+?\d{7,}$/.test(value) || isWhatsAppUserTarget(value))
|
|
1386
|
+
return "phone";
|
|
1387
|
+
return "contact";
|
|
1388
|
+
}
|
|
1389
|
+
function knownWhatsAppTargetToConnectorTarget(known, score = 0.72) {
|
|
1390
|
+
const accountId = known.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
1172
1391
|
return {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1392
|
+
target: targetWithAccount({
|
|
1393
|
+
source: "whatsapp",
|
|
1394
|
+
channelId: known.chatId,
|
|
1395
|
+
entityId: known.senderId,
|
|
1396
|
+
roomId: known.roomId
|
|
1397
|
+
}, accountId),
|
|
1398
|
+
label: known.label,
|
|
1399
|
+
kind: known.isGroup ? "group" : whatsappTargetKind(known.senderId),
|
|
1400
|
+
description: known.isGroup ? "WhatsApp group chat" : "WhatsApp contact",
|
|
1401
|
+
score,
|
|
1402
|
+
metadata: {
|
|
1403
|
+
accountId,
|
|
1404
|
+
chatId: known.chatId,
|
|
1405
|
+
senderId: known.senderId,
|
|
1406
|
+
lastMessageAt: known.lastMessageAt
|
|
1407
|
+
}
|
|
1176
1408
|
};
|
|
1177
1409
|
}
|
|
1178
|
-
function
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
const merged = mergeWhatsAppAccountConfig(runtime, normalizedAccountId);
|
|
1183
|
-
const accountEnabled = merged.enabled !== false;
|
|
1184
|
-
const enabled = baseEnabled && accountEnabled;
|
|
1185
|
-
const { token, source: tokenSource } = resolveWhatsAppToken(runtime, normalizedAccountId);
|
|
1186
|
-
const phoneNumberId = merged.phoneNumberId?.trim() || "";
|
|
1187
|
-
const configured = Boolean(token && phoneNumberId);
|
|
1410
|
+
function directWhatsAppTarget(value, accountId = DEFAULT_ACCOUNT_ID, score = 0.68) {
|
|
1411
|
+
const normalized = normalizeWhatsAppConnectorTarget(value);
|
|
1412
|
+
if (!normalized || !isWhatsAppAddress(normalized))
|
|
1413
|
+
return null;
|
|
1188
1414
|
return {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1415
|
+
target: targetWithAccount({
|
|
1416
|
+
source: "whatsapp",
|
|
1417
|
+
channelId: normalized,
|
|
1418
|
+
entityId: normalized
|
|
1419
|
+
}, accountId),
|
|
1420
|
+
label: normalized,
|
|
1421
|
+
kind: whatsappTargetKind(normalized),
|
|
1422
|
+
score,
|
|
1423
|
+
metadata: {
|
|
1424
|
+
accountId,
|
|
1425
|
+
normalizedTarget: normalized
|
|
1426
|
+
}
|
|
1198
1427
|
};
|
|
1199
1428
|
}
|
|
1200
|
-
function
|
|
1201
|
-
|
|
1429
|
+
async function resolveWhatsAppSendTarget(runtime, service, target, fallbackAccountId) {
|
|
1430
|
+
const targetAccountId = typeof service.resolveAccountId === "function" ? service.resolveAccountId(readTargetAccountId(target) ?? fallbackAccountId) : normalizeAccountId(readTargetAccountId(target) ?? fallbackAccountId);
|
|
1431
|
+
if (target.channelId?.trim()) {
|
|
1432
|
+
const normalized = normalizeWhatsAppConnectorTarget(target.channelId);
|
|
1433
|
+
const known = service.getKnownTarget(normalized, targetAccountId) ?? service.findKnownChatByParticipant(normalized, targetAccountId);
|
|
1434
|
+
if (known) {
|
|
1435
|
+
return { accountId: known.accountId ?? targetAccountId, chatId: known.chatId };
|
|
1436
|
+
}
|
|
1437
|
+
return isWhatsAppAddress(normalized) ? { accountId: targetAccountId, chatId: normalized } : null;
|
|
1438
|
+
}
|
|
1439
|
+
if (target.entityId?.trim()) {
|
|
1440
|
+
const normalized = normalizeWhatsAppConnectorTarget(target.entityId);
|
|
1441
|
+
const known = service.findKnownChatByParticipant(normalized, targetAccountId);
|
|
1442
|
+
if (known) {
|
|
1443
|
+
return { accountId: known.accountId ?? targetAccountId, chatId: known.chatId };
|
|
1444
|
+
}
|
|
1445
|
+
return isWhatsAppAddress(normalized) ? { accountId: targetAccountId, chatId: normalized } : null;
|
|
1446
|
+
}
|
|
1447
|
+
if (target.roomId) {
|
|
1448
|
+
const room = await runtime.getRoom(target.roomId);
|
|
1449
|
+
if (room?.channelId) {
|
|
1450
|
+
const normalized = normalizeWhatsAppConnectorTarget(room.channelId);
|
|
1451
|
+
const known = service.getKnownTarget(normalized, targetAccountId) ?? service.findKnownChatByParticipant(normalized, targetAccountId);
|
|
1452
|
+
if (known) {
|
|
1453
|
+
return { accountId: known.accountId ?? targetAccountId, chatId: known.chatId };
|
|
1454
|
+
}
|
|
1455
|
+
return isWhatsAppAddress(normalized) ? { accountId: targetAccountId, chatId: normalized } : null;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return null;
|
|
1202
1459
|
}
|
|
1203
|
-
function
|
|
1204
|
-
|
|
1205
|
-
|
|
1460
|
+
function extractWebhookText(message) {
|
|
1461
|
+
if (typeof message.text?.body === "string" && message.text.body.trim()) {
|
|
1462
|
+
return message.text.body.trim();
|
|
1463
|
+
}
|
|
1464
|
+
if (typeof message.interactive?.button_reply?.title === "string" && message.interactive.button_reply.title.trim()) {
|
|
1465
|
+
return message.interactive.button_reply.title.trim();
|
|
1466
|
+
}
|
|
1467
|
+
if (typeof message.interactive?.list_reply?.title === "string" && message.interactive.list_reply.title.trim()) {
|
|
1468
|
+
return message.interactive.list_reply.title.trim();
|
|
1469
|
+
}
|
|
1470
|
+
if (typeof message.interactive?.nfm_reply?.body === "string" && message.interactive.nfm_reply.body.trim()) {
|
|
1471
|
+
return message.interactive.nfm_reply.body.trim();
|
|
1472
|
+
}
|
|
1473
|
+
if (typeof message.image?.caption === "string" && message.image.caption.trim()) {
|
|
1474
|
+
return message.image.caption.trim();
|
|
1475
|
+
}
|
|
1476
|
+
if (typeof message.video?.caption === "string" && message.video.caption.trim()) {
|
|
1477
|
+
return message.video.caption.trim();
|
|
1478
|
+
}
|
|
1479
|
+
if (typeof message.document?.caption === "string" && message.document.caption.trim()) {
|
|
1480
|
+
return message.document.caption.trim();
|
|
1481
|
+
}
|
|
1482
|
+
if (message.reaction?.emoji) {
|
|
1483
|
+
return `Reaction: ${message.reaction.emoji}`;
|
|
1484
|
+
}
|
|
1485
|
+
if (message.location) {
|
|
1486
|
+
const { latitude, longitude } = message.location;
|
|
1487
|
+
return `Location: ${latitude}, ${longitude}`;
|
|
1488
|
+
}
|
|
1489
|
+
return "";
|
|
1206
1490
|
}
|
|
1207
|
-
function
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1491
|
+
function isRecord(value) {
|
|
1492
|
+
return typeof value === "object" && value !== null;
|
|
1493
|
+
}
|
|
1494
|
+
function asRecordArray(value) {
|
|
1495
|
+
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
1496
|
+
}
|
|
1497
|
+
function isWebhookMessage(value) {
|
|
1498
|
+
if (!isRecord(value)) {
|
|
1499
|
+
return false;
|
|
1500
|
+
}
|
|
1501
|
+
return Boolean(typeof value.from === "string" && value.from.trim() && typeof value.id === "string");
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
class WhatsAppConnectorService extends Service {
|
|
1505
|
+
static serviceType = "whatsapp";
|
|
1506
|
+
capabilityDescription = "The agent is able to send and receive messages on whatsapp";
|
|
1507
|
+
connected = false;
|
|
1508
|
+
phoneNumber = null;
|
|
1509
|
+
defaultAccountId = DEFAULT_ACCOUNT_ID;
|
|
1510
|
+
clients = new Map;
|
|
1511
|
+
configs = new Map;
|
|
1512
|
+
phoneNumbers = new Map;
|
|
1513
|
+
client = null;
|
|
1514
|
+
config = undefined;
|
|
1515
|
+
knownTargets = new Map;
|
|
1516
|
+
constructor(runtime) {
|
|
1517
|
+
super(runtime);
|
|
1518
|
+
if (runtime) {
|
|
1519
|
+
this.runtime = runtime;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
resolveAccountId(accountId) {
|
|
1523
|
+
return normalizeAccountId(accountId ?? this.defaultAccountId);
|
|
1524
|
+
}
|
|
1525
|
+
getClientForAccount(accountId) {
|
|
1526
|
+
const normalizedAccountId = this.resolveAccountId(accountId);
|
|
1527
|
+
return this.clients.get(normalizedAccountId) ?? (normalizedAccountId === this.defaultAccountId ? this.client : null);
|
|
1528
|
+
}
|
|
1529
|
+
getConfigForAccount(accountId) {
|
|
1530
|
+
const normalizedAccountId = this.resolveAccountId(accountId);
|
|
1531
|
+
return this.configs.get(normalizedAccountId) ?? (normalizedAccountId === this.defaultAccountId ? this.config ?? null : null);
|
|
1532
|
+
}
|
|
1533
|
+
getConnectorAccountIds() {
|
|
1534
|
+
const ids = Array.from(this.configs.keys());
|
|
1535
|
+
return ids.length > 0 ? ids : [this.defaultAccountId];
|
|
1536
|
+
}
|
|
1537
|
+
targetKey(chatId, accountId) {
|
|
1538
|
+
return `${this.resolveAccountId(accountId)}:${normalizeWhatsAppConnectorTarget(chatId)}`;
|
|
1539
|
+
}
|
|
1540
|
+
roomIdFor(chatId, accountId) {
|
|
1541
|
+
const normalizedAccountId = this.resolveAccountId(accountId);
|
|
1542
|
+
return createUniqueUuid(this.runtime, normalizedAccountId === DEFAULT_ACCOUNT_ID ? `whatsapp-room:${chatId}` : `whatsapp-room:${normalizedAccountId}:${chatId}`);
|
|
1543
|
+
}
|
|
1544
|
+
entityIdFor(senderId, accountId) {
|
|
1545
|
+
const normalizedAccountId = this.resolveAccountId(accountId);
|
|
1546
|
+
return createUniqueUuid(this.runtime, normalizedAccountId === DEFAULT_ACCOUNT_ID ? `whatsapp-entity:${senderId}` : `whatsapp-entity:${normalizedAccountId}:${senderId}`);
|
|
1547
|
+
}
|
|
1548
|
+
worldIdFor(chatId, accountId) {
|
|
1549
|
+
const normalizedAccountId = this.resolveAccountId(accountId);
|
|
1550
|
+
return createUniqueUuid(this.runtime, normalizedAccountId === DEFAULT_ACCOUNT_ID ? `whatsapp-world:${chatId}` : `whatsapp-world:${normalizedAccountId}:${chatId}`);
|
|
1551
|
+
}
|
|
1552
|
+
metadataMatchesAccount(memory, accountId) {
|
|
1553
|
+
const metadata = memory.metadata;
|
|
1554
|
+
const memoryAccountId = typeof metadata?.accountId === "string" && metadata.accountId.trim() ? this.resolveAccountId(metadata.accountId) : undefined;
|
|
1555
|
+
return memoryAccountId ? memoryAccountId === accountId : accountId === DEFAULT_ACCOUNT_ID;
|
|
1556
|
+
}
|
|
1557
|
+
static async start(runtime) {
|
|
1558
|
+
const service = new WhatsAppConnectorService(runtime);
|
|
1559
|
+
await service.initialize();
|
|
1560
|
+
return service;
|
|
1561
|
+
}
|
|
1562
|
+
static registerSendHandlers(runtime, service) {
|
|
1563
|
+
const resolveServiceAccountId = (accountId) => typeof service.resolveAccountId === "function" ? service.resolveAccountId(accountId) : normalizeAccountId(accountId);
|
|
1564
|
+
const getServiceConfigForAccount = (accountId) => typeof service.getConfigForAccount === "function" ? service.getConfigForAccount(accountId) : service.config ?? null;
|
|
1565
|
+
const accountIds = typeof service.getConnectorAccountIds === "function" ? service.getConnectorAccountIds() : [DEFAULT_ACCOUNT_ID];
|
|
1566
|
+
const registrationAccountIds = accountIds.length > 1 ? accountIds : [undefined];
|
|
1567
|
+
for (const registrationAccountId of registrationAccountIds) {
|
|
1568
|
+
const connectorAccountId = resolveServiceAccountId(registrationAccountId);
|
|
1569
|
+
const config = getServiceConfigForAccount(connectorAccountId);
|
|
1570
|
+
registerMessageConnectorIfAvailable(runtime, {
|
|
1571
|
+
source: "whatsapp",
|
|
1572
|
+
...registrationAccountId ? { accountId: connectorAccountId } : {},
|
|
1573
|
+
label: registrationAccountId && connectorAccountId !== DEFAULT_ACCOUNT_ID ? `WhatsApp (${connectorAccountId})` : "WhatsApp",
|
|
1574
|
+
capabilities: [
|
|
1575
|
+
"send_message",
|
|
1576
|
+
"read_messages",
|
|
1577
|
+
"search_messages",
|
|
1578
|
+
"send_reaction",
|
|
1579
|
+
"contact_resolution",
|
|
1580
|
+
"chat_context",
|
|
1581
|
+
"get_user"
|
|
1582
|
+
],
|
|
1583
|
+
supportedTargetKinds: ["phone", "contact", "user", "group", "room"],
|
|
1584
|
+
contexts: ["phone", "social", "connectors"],
|
|
1585
|
+
description: "Send, read, search, and react in WhatsApp conversations through Cloud API or Baileys using phone numbers, JIDs, known contacts, or group ids.",
|
|
1586
|
+
metadata: {
|
|
1587
|
+
aliases: ["whatsapp", "wa"],
|
|
1588
|
+
accountId: connectorAccountId,
|
|
1589
|
+
transport: config?.transport ?? service.config?.transport ?? "unconfigured",
|
|
1590
|
+
connected: service.connected
|
|
1591
|
+
},
|
|
1592
|
+
sendHandler: async (_runtime, target, content) => {
|
|
1593
|
+
const text = typeof content.text === "string" ? content.text.trim() : "";
|
|
1594
|
+
const attachments = Array.isArray(content.attachments) ? content.attachments.filter((media) => typeof media?.url === "string" && media.url.trim().length > 0) : [];
|
|
1595
|
+
if (!text && attachments.length === 0) {
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const resolved = await resolveWhatsAppSendTarget(runtime, service, target, connectorAccountId);
|
|
1599
|
+
if (!resolved) {
|
|
1600
|
+
throw new Error("WhatsApp target is missing a phone number, JID, or chat id");
|
|
1601
|
+
}
|
|
1602
|
+
let replyToMessageId;
|
|
1603
|
+
if (typeof content.inReplyTo === "string" && content.inReplyTo.trim()) {
|
|
1604
|
+
const repliedToMemory = await runtime.getMemoryById(content.inReplyTo);
|
|
1605
|
+
const metadata = repliedToMemory?.metadata;
|
|
1606
|
+
const externalMessageId = metadata?.messageIdFull ?? metadata?.externalMessageId ?? metadata?.whatsappMessageId;
|
|
1607
|
+
if (typeof externalMessageId === "string" && externalMessageId.trim()) {
|
|
1608
|
+
replyToMessageId = externalMessageId.trim();
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
if (text) {
|
|
1612
|
+
for (const chunk of chunkWhatsAppText(text)) {
|
|
1613
|
+
await service.sendMessage({
|
|
1614
|
+
accountId: resolved.accountId,
|
|
1615
|
+
type: "text",
|
|
1616
|
+
to: resolved.chatId,
|
|
1617
|
+
content: chunk,
|
|
1618
|
+
replyToMessageId
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
for (const media of attachments) {
|
|
1623
|
+
try {
|
|
1624
|
+
await service.sendMediaMessage(resolved.accountId, resolved.chatId, media);
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
runtime.logger.warn({
|
|
1627
|
+
src: "plugin:whatsapp",
|
|
1628
|
+
agentId: runtime.agentId,
|
|
1629
|
+
url: media.url,
|
|
1630
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1631
|
+
}, "Failed to send WhatsApp outbound attachment; skipping");
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
},
|
|
1635
|
+
resolveTargets: async (query) => {
|
|
1636
|
+
const candidates = [];
|
|
1637
|
+
for (const known of service.listKnownTargets(connectorAccountId)) {
|
|
1638
|
+
if (matchesQuery(query, known.label, known.chatId, known.senderId)) {
|
|
1639
|
+
candidates.push(knownWhatsAppTargetToConnectorTarget(known, 0.82));
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const direct = directWhatsAppTarget(query, connectorAccountId, 0.74);
|
|
1643
|
+
if (direct)
|
|
1644
|
+
candidates.push(direct);
|
|
1645
|
+
return candidates;
|
|
1646
|
+
},
|
|
1647
|
+
listRecentTargets: () => service.listKnownTargets(connectorAccountId).map((known) => knownWhatsAppTargetToConnectorTarget(known, 0.66)),
|
|
1648
|
+
listRooms: () => service.listKnownTargets(connectorAccountId).filter((known) => known.isGroup).map((known) => knownWhatsAppTargetToConnectorTarget(known, 0.7)),
|
|
1649
|
+
fetchMessages: service.fetchConnectorMessages.bind(service),
|
|
1650
|
+
searchMessages: service.searchConnectorMessages.bind(service),
|
|
1651
|
+
reactHandler: service.reactConnectorMessage.bind(service),
|
|
1652
|
+
getUser: service.getConnectorUser.bind(service),
|
|
1653
|
+
getChatContext: async (target, context) => {
|
|
1654
|
+
const resolved = await resolveWhatsAppSendTarget(context.runtime, service, target, readContextAccountId(context) ?? connectorAccountId);
|
|
1655
|
+
if (!resolved)
|
|
1656
|
+
return null;
|
|
1657
|
+
const known = service.getKnownTarget(resolved.chatId, resolved.accountId) ?? service.findKnownChatByParticipant(resolved.chatId, resolved.accountId);
|
|
1658
|
+
const resolvedConfig = getServiceConfigForAccount(resolved.accountId);
|
|
1659
|
+
return {
|
|
1660
|
+
target: targetWithAccount({ ...target, channelId: resolved.chatId }, resolved.accountId),
|
|
1661
|
+
label: known?.label ?? resolved.chatId,
|
|
1662
|
+
summary: known?.isGroup ? "WhatsApp group chat." : "WhatsApp direct chat.",
|
|
1663
|
+
metadata: {
|
|
1664
|
+
accountId: resolved.accountId,
|
|
1665
|
+
chatId: resolved.chatId,
|
|
1666
|
+
senderId: known?.senderId,
|
|
1667
|
+
lastMessageAt: known?.lastMessageAt,
|
|
1668
|
+
connected: service.connected,
|
|
1669
|
+
transport: resolvedConfig?.transport
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
},
|
|
1673
|
+
getUserContext: async (entityId) => {
|
|
1674
|
+
const handle = normalizeWhatsAppConnectorTarget(String(entityId));
|
|
1675
|
+
if (!handle)
|
|
1676
|
+
return null;
|
|
1677
|
+
const known = service.findKnownChatByParticipant(handle, connectorAccountId);
|
|
1678
|
+
return {
|
|
1679
|
+
entityId,
|
|
1680
|
+
label: known?.label ?? handle,
|
|
1681
|
+
aliases: known ? [known.label, known.senderId, known.chatId] : [handle],
|
|
1682
|
+
handles: {
|
|
1683
|
+
whatsapp: known?.chatId ?? handle,
|
|
1684
|
+
phone: normalizeWhatsAppTarget(handle) ?? handle
|
|
1685
|
+
},
|
|
1686
|
+
metadata: {
|
|
1687
|
+
accountId: known?.accountId ?? connectorAccountId,
|
|
1688
|
+
normalizedHandle: handle,
|
|
1689
|
+
chatId: known?.chatId
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
async initialize() {
|
|
1697
|
+
this.defaultAccountId = resolveDefaultWhatsAppAccountId(this.runtime);
|
|
1698
|
+
const configs = resolveRuntimeConfigs(this.runtime);
|
|
1699
|
+
if (configs.length === 0) {
|
|
1700
|
+
this.runtime.logger.warn({ src: "plugin:whatsapp", agentId: this.runtime.agentId }, "WhatsApp connector is not configured");
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
for (const config of configs) {
|
|
1704
|
+
const client = config.transport === "baileys" ? new BaileysClient({
|
|
1705
|
+
authMethod: "baileys",
|
|
1706
|
+
authDir: config.authDir,
|
|
1707
|
+
printQRInTerminal: false
|
|
1708
|
+
}) : new WhatsAppClient({
|
|
1709
|
+
accessToken: config.accessToken,
|
|
1710
|
+
phoneNumberId: config.phoneNumberId,
|
|
1711
|
+
webhookVerifyToken: config.webhookVerifyToken,
|
|
1712
|
+
apiVersion: config.apiVersion
|
|
1713
|
+
});
|
|
1714
|
+
this.configs.set(config.accountId, config);
|
|
1715
|
+
this.clients.set(config.accountId, client);
|
|
1716
|
+
if (config.accountId === this.defaultAccountId || !this.client) {
|
|
1717
|
+
this.config = config;
|
|
1718
|
+
this.client = client;
|
|
1719
|
+
}
|
|
1720
|
+
this.bindClientEvents(client, config.accountId);
|
|
1721
|
+
await client.start();
|
|
1722
|
+
if (config.transport === "cloudapi") {
|
|
1723
|
+
this.connected = true;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
async stop() {
|
|
1728
|
+
for (const client of this.clients.values()) {
|
|
1729
|
+
await client.stop();
|
|
1730
|
+
}
|
|
1731
|
+
this.clients.clear();
|
|
1732
|
+
this.configs.clear();
|
|
1733
|
+
this.phoneNumbers.clear();
|
|
1734
|
+
this.client = null;
|
|
1735
|
+
this.config = undefined;
|
|
1736
|
+
this.connected = false;
|
|
1737
|
+
this.phoneNumber = null;
|
|
1738
|
+
}
|
|
1739
|
+
async handleWebhook(event) {
|
|
1740
|
+
for (const entry of asRecordArray(event?.entry)) {
|
|
1741
|
+
for (const change of asRecordArray(entry.changes)) {
|
|
1742
|
+
if (!isRecord(change.value)) {
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
const value = change.value;
|
|
1746
|
+
const metadata = isRecord(value.metadata) ? value.metadata : {};
|
|
1747
|
+
const phoneNumberId = typeof metadata.phone_number_id === "string" ? metadata.phone_number_id : undefined;
|
|
1748
|
+
const accountId = this.resolveWebhookAccountId(phoneNumberId);
|
|
1749
|
+
if (typeof metadata.display_phone_number === "string") {
|
|
1750
|
+
this.phoneNumbers.set(accountId, metadata.display_phone_number);
|
|
1751
|
+
if (accountId === this.defaultAccountId) {
|
|
1752
|
+
this.phoneNumber = metadata.display_phone_number;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
for (const message of asRecordArray(value.messages)) {
|
|
1756
|
+
if (!isWebhookMessage(message)) {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
await this.handleIncomingWebhookMessage(message, accountId);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
verifyWebhook(mode, token, challenge, accountId) {
|
|
1765
|
+
const configs = accountId ? [this.getConfigForAccount(accountId)].filter((config) => Boolean(config)) : Array.from(this.configs.values());
|
|
1766
|
+
const expectedTokens = configs.length > 0 ? configs.filter((config) => config.transport === "cloudapi").map((config) => config.webhookVerifyToken) : [
|
|
1767
|
+
this.config?.transport === "cloudapi" ? this.config.webhookVerifyToken : readStringSetting(this.runtime, "WHATSAPP_WEBHOOK_VERIFY_TOKEN")
|
|
1768
|
+
];
|
|
1769
|
+
if (mode === "subscribe" && challenge && expectedTokens.some((expectedToken) => expectedToken && token === expectedToken)) {
|
|
1770
|
+
return challenge;
|
|
1771
|
+
}
|
|
1772
|
+
return null;
|
|
1773
|
+
}
|
|
1774
|
+
resolveWebhookAccountId(phoneNumberId) {
|
|
1775
|
+
const normalizedPhoneNumberId = typeof phoneNumberId === "string" && phoneNumberId.trim() ? phoneNumberId.trim() : undefined;
|
|
1776
|
+
if (normalizedPhoneNumberId) {
|
|
1777
|
+
for (const [accountId, config] of this.configs) {
|
|
1778
|
+
if (config.transport === "cloudapi" && config.phoneNumberId === normalizedPhoneNumberId) {
|
|
1779
|
+
return accountId;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return this.defaultAccountId;
|
|
1784
|
+
}
|
|
1785
|
+
bindClientEvents(client, accountId) {
|
|
1786
|
+
client.on("connection", (status) => {
|
|
1787
|
+
if (status === "open") {
|
|
1788
|
+
this.connected = true;
|
|
1789
|
+
}
|
|
1790
|
+
if (status === "open" && client instanceof BaileysClient) {
|
|
1791
|
+
const nextPhone = client.getPhoneNumber();
|
|
1792
|
+
const normalizedPhone = (nextPhone && normalizeWhatsAppTarget(nextPhone)) ?? nextPhone;
|
|
1793
|
+
if (normalizedPhone) {
|
|
1794
|
+
this.phoneNumbers.set(accountId, normalizedPhone);
|
|
1795
|
+
}
|
|
1796
|
+
if (accountId === this.defaultAccountId) {
|
|
1797
|
+
this.phoneNumber = normalizedPhone;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
if (status === "close") {
|
|
1801
|
+
this.phoneNumbers.delete(accountId);
|
|
1802
|
+
this.connected = this.phoneNumbers.size > 0 || Array.from(this.configs.values()).some((config) => config.transport === "cloudapi");
|
|
1803
|
+
if (accountId === this.defaultAccountId) {
|
|
1804
|
+
this.phoneNumber = null;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
client.on("ready", () => {
|
|
1809
|
+
this.connected = true;
|
|
1810
|
+
if (client instanceof BaileysClient) {
|
|
1811
|
+
const nextPhone = client.getPhoneNumber();
|
|
1812
|
+
const normalizedPhone = (nextPhone && normalizeWhatsAppTarget(nextPhone)) ?? nextPhone;
|
|
1813
|
+
if (normalizedPhone) {
|
|
1814
|
+
this.phoneNumbers.set(accountId, normalizedPhone);
|
|
1815
|
+
}
|
|
1816
|
+
if (accountId === this.defaultAccountId) {
|
|
1817
|
+
this.phoneNumber = normalizedPhone;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
});
|
|
1821
|
+
client.on("message", (message) => {
|
|
1822
|
+
this.handleNormalizedMessage(message, accountId).catch((error) => {
|
|
1823
|
+
this.runtime.logger.error({
|
|
1824
|
+
src: "plugin:whatsapp",
|
|
1825
|
+
agentId: this.runtime.agentId,
|
|
1826
|
+
accountId,
|
|
1827
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1828
|
+
}, "Failed to process inbound WhatsApp message");
|
|
1829
|
+
});
|
|
1830
|
+
});
|
|
1831
|
+
client.on("error", (error) => {
|
|
1832
|
+
this.runtime.logger.error({
|
|
1833
|
+
src: "plugin:whatsapp",
|
|
1834
|
+
agentId: this.runtime.agentId,
|
|
1835
|
+
accountId,
|
|
1836
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1837
|
+
}, "WhatsApp client error");
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
async handleNormalizedMessage(message, accountId = this.defaultAccountId) {
|
|
1841
|
+
const chatId = message.chatId ?? message.from;
|
|
1842
|
+
const senderId = message.senderId ?? message.from;
|
|
1843
|
+
const text = typeof message.content === "string" ? message.content.trim() : "";
|
|
1844
|
+
if (!chatId || !senderId || !text) {
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
await this.processIncomingMessage({
|
|
1848
|
+
chatId,
|
|
1849
|
+
senderId,
|
|
1850
|
+
text,
|
|
1851
|
+
externalMessageId: message.id,
|
|
1852
|
+
replyToExternalMessageId: message.replyToId,
|
|
1853
|
+
createdAt: toTimestampMs(message.timestamp),
|
|
1854
|
+
accountId
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
async handleIncomingWebhookMessage(message, accountId = this.defaultAccountId) {
|
|
1858
|
+
const text = extractWebhookText(message);
|
|
1859
|
+
if (!text) {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
const normalizedSender = normalizeWhatsAppTarget(message.from) ?? message.from;
|
|
1863
|
+
await this.processIncomingMessage({
|
|
1864
|
+
chatId: normalizedSender,
|
|
1865
|
+
senderId: normalizedSender,
|
|
1866
|
+
text,
|
|
1867
|
+
externalMessageId: message.id,
|
|
1868
|
+
replyToExternalMessageId: message.context?.id,
|
|
1869
|
+
createdAt: toTimestampMs(message.timestamp),
|
|
1870
|
+
accountId
|
|
1871
|
+
});
|
|
1213
1872
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const { identifier, accountConfig, isGroup, groupConfig } = params;
|
|
1218
|
-
if (isGroup) {
|
|
1219
|
-
const policy2 = accountConfig.groupPolicy ?? "allowlist";
|
|
1220
|
-
if (policy2 === "disabled") {
|
|
1221
|
-
return false;
|
|
1873
|
+
async processIncomingMessage(params) {
|
|
1874
|
+
if (!this.runtime.messageService) {
|
|
1875
|
+
throw new Error("WhatsApp connector requires runtime.messageService");
|
|
1222
1876
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1877
|
+
const accountId = this.resolveAccountId(params.accountId);
|
|
1878
|
+
const config = this.getConfigForAccount(accountId);
|
|
1879
|
+
const isGroup = isWhatsAppGroupJid(params.chatId);
|
|
1880
|
+
const normalizedSender = normalizeWhatsAppTarget(params.senderId) ?? params.senderId;
|
|
1881
|
+
const accountConfig = {
|
|
1882
|
+
dmPolicy: config?.dmPolicy,
|
|
1883
|
+
groupPolicy: config?.groupPolicy,
|
|
1884
|
+
allowFrom: config?.allowFrom,
|
|
1885
|
+
groupAllowFrom: config?.groupAllowFrom
|
|
1886
|
+
};
|
|
1887
|
+
const access = await checkWhatsAppUserAccess({
|
|
1888
|
+
runtime: this.runtime,
|
|
1889
|
+
identifier: normalizedSender,
|
|
1890
|
+
accountConfig,
|
|
1891
|
+
isGroup,
|
|
1892
|
+
...isGroup ? { groupId: params.chatId } : {},
|
|
1893
|
+
metadata: { accountId, senderId: normalizedSender }
|
|
1894
|
+
});
|
|
1895
|
+
if (!access.allowed) {
|
|
1896
|
+
if (access.replyMessage) {
|
|
1897
|
+
await this.sendTextMessage(params.chatId, access.replyMessage, undefined, accountId);
|
|
1898
|
+
}
|
|
1899
|
+
return;
|
|
1225
1900
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1901
|
+
const channelType = isGroup ? ChannelType.GROUP : ChannelType.DM;
|
|
1902
|
+
const roomId = this.roomIdFor(params.chatId, accountId);
|
|
1903
|
+
const worldId = this.worldIdFor(params.chatId, accountId);
|
|
1904
|
+
const entityId = this.entityIdFor(normalizedSender, accountId);
|
|
1905
|
+
const inboundMemoryId = toMemoryId(this.runtime, accountId === DEFAULT_ACCOUNT_ID ? params.chatId : `${accountId}:${params.chatId}`, params.externalMessageId);
|
|
1906
|
+
await this.runtime.ensureConnection({
|
|
1907
|
+
entityId,
|
|
1908
|
+
roomId,
|
|
1909
|
+
userId: normalizedSender,
|
|
1910
|
+
userName: normalizedSender,
|
|
1911
|
+
name: normalizedSender,
|
|
1912
|
+
source: "whatsapp",
|
|
1913
|
+
channelId: params.chatId,
|
|
1914
|
+
type: channelType,
|
|
1915
|
+
worldId,
|
|
1916
|
+
worldName: resolveWhatsAppSystemLocation({
|
|
1917
|
+
chatType: isGroup ? "group" : "user",
|
|
1918
|
+
chatId: params.chatId
|
|
1919
|
+
}),
|
|
1920
|
+
metadata: {
|
|
1921
|
+
accountId,
|
|
1922
|
+
chatId: params.chatId,
|
|
1923
|
+
isGroup
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
if (typeof this.runtime.ensureRoomExists === "function") {
|
|
1927
|
+
await this.runtime.ensureRoomExists({
|
|
1928
|
+
id: roomId,
|
|
1929
|
+
name: resolveWhatsAppSystemLocation({
|
|
1930
|
+
chatType: isGroup ? "group" : "user",
|
|
1931
|
+
chatId: params.chatId
|
|
1932
|
+
}),
|
|
1933
|
+
agentId: this.runtime.agentId,
|
|
1934
|
+
source: "whatsapp",
|
|
1935
|
+
type: channelType,
|
|
1936
|
+
channelId: params.chatId,
|
|
1937
|
+
worldId,
|
|
1938
|
+
metadata: {
|
|
1939
|
+
accountId,
|
|
1940
|
+
chatId: params.chatId,
|
|
1941
|
+
isGroup
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1228
1944
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1945
|
+
this.rememberTarget({
|
|
1946
|
+
accountId,
|
|
1947
|
+
chatId: params.chatId,
|
|
1948
|
+
senderId: normalizedSender,
|
|
1949
|
+
label: resolveWhatsAppSystemLocation({
|
|
1950
|
+
chatType: isGroup ? "group" : "user",
|
|
1951
|
+
chatId: params.chatId
|
|
1952
|
+
}),
|
|
1953
|
+
isGroup,
|
|
1954
|
+
lastMessageAt: params.createdAt,
|
|
1955
|
+
roomId
|
|
1956
|
+
});
|
|
1957
|
+
const inboundMemory = {
|
|
1958
|
+
id: inboundMemoryId,
|
|
1959
|
+
entityId,
|
|
1960
|
+
agentId: this.runtime.agentId,
|
|
1961
|
+
roomId,
|
|
1962
|
+
content: {
|
|
1963
|
+
text: params.text,
|
|
1964
|
+
source: "whatsapp",
|
|
1965
|
+
channelType,
|
|
1966
|
+
from: normalizedSender,
|
|
1967
|
+
messageId: params.externalMessageId,
|
|
1968
|
+
...params.replyToExternalMessageId ? {
|
|
1969
|
+
inReplyTo: toMemoryId(this.runtime, accountId === DEFAULT_ACCOUNT_ID ? params.chatId : `${accountId}:${params.chatId}`, params.replyToExternalMessageId)
|
|
1970
|
+
} : {}
|
|
1971
|
+
},
|
|
1972
|
+
metadata: {
|
|
1973
|
+
type: "message",
|
|
1974
|
+
source: "whatsapp",
|
|
1975
|
+
provider: "whatsapp",
|
|
1976
|
+
accountId,
|
|
1977
|
+
timestamp: params.createdAt,
|
|
1978
|
+
entityName: normalizedSender,
|
|
1979
|
+
entityUserName: normalizedSender,
|
|
1980
|
+
fromBot: false,
|
|
1981
|
+
fromId: normalizedSender,
|
|
1982
|
+
sourceId: entityId,
|
|
1983
|
+
chatType: channelType,
|
|
1984
|
+
messageIdFull: params.externalMessageId,
|
|
1985
|
+
sender: {
|
|
1986
|
+
id: normalizedSender,
|
|
1987
|
+
name: normalizedSender,
|
|
1988
|
+
username: normalizedSender
|
|
1989
|
+
},
|
|
1990
|
+
whatsapp: {
|
|
1991
|
+
contactId: normalizedSender,
|
|
1992
|
+
messageId: params.externalMessageId
|
|
1993
|
+
},
|
|
1994
|
+
rawChatId: params.chatId,
|
|
1995
|
+
rawSenderId: params.senderId
|
|
1996
|
+
},
|
|
1997
|
+
createdAt: params.createdAt
|
|
1998
|
+
};
|
|
1999
|
+
const callback = async (content) => {
|
|
2000
|
+
const text = typeof content.text === "string" ? content.text.trim() : "";
|
|
2001
|
+
if (!text) {
|
|
2002
|
+
return [];
|
|
2003
|
+
}
|
|
2004
|
+
const chunks = chunkWhatsAppText(text);
|
|
2005
|
+
const responseMemories = [];
|
|
2006
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
2007
|
+
const response = await this.sendTextMessage(params.chatId, chunk, params.externalMessageId, accountId);
|
|
2008
|
+
const externalResponseId = response.messages[0]?.id ?? `${params.externalMessageId}:response:${index}:${Date.now()}`;
|
|
2009
|
+
responseMemories.push({
|
|
2010
|
+
id: toMemoryId(this.runtime, accountId === DEFAULT_ACCOUNT_ID ? params.chatId : `${accountId}:${params.chatId}`, externalResponseId),
|
|
2011
|
+
entityId: this.runtime.agentId,
|
|
2012
|
+
agentId: this.runtime.agentId,
|
|
2013
|
+
roomId,
|
|
2014
|
+
content: {
|
|
2015
|
+
...content,
|
|
2016
|
+
text: chunk,
|
|
2017
|
+
source: "whatsapp",
|
|
2018
|
+
channelType,
|
|
2019
|
+
inReplyTo: inboundMemoryId
|
|
2020
|
+
},
|
|
2021
|
+
metadata: {
|
|
2022
|
+
type: "message",
|
|
2023
|
+
source: "whatsapp",
|
|
2024
|
+
provider: "whatsapp",
|
|
2025
|
+
accountId,
|
|
2026
|
+
timestamp: Date.now(),
|
|
2027
|
+
fromBot: true,
|
|
2028
|
+
fromId: this.runtime.agentId,
|
|
2029
|
+
sourceId: this.runtime.agentId,
|
|
2030
|
+
chatType: channelType,
|
|
2031
|
+
messageIdFull: externalResponseId,
|
|
2032
|
+
whatsapp: {
|
|
2033
|
+
contactId: params.chatId,
|
|
2034
|
+
messageId: externalResponseId
|
|
2035
|
+
},
|
|
2036
|
+
rawChatId: params.chatId,
|
|
2037
|
+
externalMessageId: externalResponseId
|
|
2038
|
+
},
|
|
2039
|
+
createdAt: Date.now()
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
return responseMemories;
|
|
2043
|
+
};
|
|
2044
|
+
const autoReplyRaw = this.runtime.getSetting("WHATSAPP_AUTO_REPLY");
|
|
2045
|
+
const autoReply = !lifeOpsPassiveConnectorsEnabled(this.runtime) && (autoReplyRaw === true || autoReplyRaw === "true");
|
|
2046
|
+
if (!autoReply) {
|
|
2047
|
+
await this.runtime.createMemory(inboundMemory, "messages");
|
|
2048
|
+
return;
|
|
1231
2049
|
}
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
2050
|
+
await this.runtime.messageService.handleMessage(this.runtime, inboundMemory, callback);
|
|
2051
|
+
}
|
|
2052
|
+
async sendTextMessage(chatId, text, replyToMessageId, accountId) {
|
|
2053
|
+
const normalizedAccountId = this.resolveAccountId(accountId);
|
|
2054
|
+
const client = this.getClientForAccount(normalizedAccountId);
|
|
2055
|
+
const config = this.getConfigForAccount(normalizedAccountId);
|
|
2056
|
+
if (!client || !config) {
|
|
2057
|
+
throw new Error("WhatsApp client is not initialized");
|
|
2058
|
+
}
|
|
2059
|
+
const response = await client.sendMessage({
|
|
2060
|
+
type: "text",
|
|
2061
|
+
to: config.transport === "baileys" ? normalizeBaileysSendTarget(chatId) : normalizeWhatsAppTarget(chatId) ?? chatId,
|
|
2062
|
+
content: text,
|
|
2063
|
+
replyToMessageId
|
|
2064
|
+
});
|
|
2065
|
+
return "data" in response ? response.data : response;
|
|
1237
2066
|
}
|
|
1238
|
-
|
|
1239
|
-
return
|
|
2067
|
+
async sendMessage(message) {
|
|
2068
|
+
return this.sendTextMessage(message.to, message.content, message.replyToMessageId, message.accountId);
|
|
1240
2069
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
2070
|
+
whatsappMediaType(media) {
|
|
2071
|
+
const ct = (media.contentType ?? "").toLowerCase();
|
|
2072
|
+
const mime = (media.mimeType ?? "").toLowerCase();
|
|
2073
|
+
if (ct === "image" || mime.startsWith("image/"))
|
|
2074
|
+
return "image";
|
|
2075
|
+
if (ct === "video" || mime.startsWith("video/"))
|
|
2076
|
+
return "video";
|
|
2077
|
+
if (ct === "audio" || mime.startsWith("audio/"))
|
|
2078
|
+
return "audio";
|
|
2079
|
+
return "document";
|
|
1243
2080
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
2081
|
+
async sendMediaMessage(accountId, to, media) {
|
|
2082
|
+
if (!media.url)
|
|
2083
|
+
return;
|
|
2084
|
+
const client = this.getClientForAccount(accountId);
|
|
2085
|
+
if (!client) {
|
|
2086
|
+
throw new Error("WhatsApp client not initialized");
|
|
2087
|
+
}
|
|
2088
|
+
const type = this.whatsappMediaType(media);
|
|
2089
|
+
const filename = media.filename ?? media.title ?? undefined;
|
|
2090
|
+
const mediaContent = {
|
|
2091
|
+
link: media.url,
|
|
2092
|
+
...media.description ? { caption: media.description } : {},
|
|
2093
|
+
...type === "document" && filename ? { filename } : {}
|
|
2094
|
+
};
|
|
2095
|
+
await client.sendMessage({ type, to, content: mediaContent });
|
|
1246
2096
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
const { groupConfig } = params;
|
|
1251
|
-
return groupConfig?.requireMention ?? false;
|
|
1252
|
-
}
|
|
1253
|
-
async function checkWhatsAppUserAccess(params) {
|
|
1254
|
-
const { runtime, identifier, accountConfig, isGroup, groupConfig, metadata } = params;
|
|
1255
|
-
if (isGroup) {
|
|
1256
|
-
const policy2 = accountConfig.groupPolicy ?? "allowlist";
|
|
1257
|
-
if (policy2 === "disabled") {
|
|
1258
|
-
return { allowed: false };
|
|
2097
|
+
async fetchConnectorMessages(context, params) {
|
|
2098
|
+
if (typeof this.runtime.getMemoriesByRoomIds !== "function") {
|
|
2099
|
+
return [];
|
|
1259
2100
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
2101
|
+
const target = params.target ?? context.target;
|
|
2102
|
+
let accountId = this.resolveAccountId(readTargetAccountId(target) ?? readContextAccountId(context));
|
|
2103
|
+
let chatId = params.channelId;
|
|
2104
|
+
if (!chatId && target) {
|
|
2105
|
+
const resolved = await resolveWhatsAppSendTarget(context.runtime, this, target, accountId);
|
|
2106
|
+
if (resolved) {
|
|
2107
|
+
accountId = resolved.accountId;
|
|
2108
|
+
chatId = resolved.chatId;
|
|
2109
|
+
}
|
|
1262
2110
|
}
|
|
1263
|
-
if (
|
|
1264
|
-
const
|
|
1265
|
-
|
|
2111
|
+
if (!chatId && params.roomId) {
|
|
2112
|
+
const room = await context.runtime.getRoom(params.roomId);
|
|
2113
|
+
chatId = room?.channelId;
|
|
2114
|
+
const metadata = room?.metadata;
|
|
2115
|
+
if (typeof metadata?.accountId === "string") {
|
|
2116
|
+
accountId = this.resolveAccountId(metadata.accountId);
|
|
2117
|
+
}
|
|
1266
2118
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
2119
|
+
const knownTargets = chatId ? [
|
|
2120
|
+
this.getKnownTarget(chatId, accountId) ?? this.findKnownChatByParticipant(chatId, accountId) ?? {
|
|
2121
|
+
accountId,
|
|
2122
|
+
chatId,
|
|
2123
|
+
senderId: chatId,
|
|
2124
|
+
label: chatId,
|
|
2125
|
+
isGroup: isWhatsAppGroupJid(chatId),
|
|
2126
|
+
lastMessageAt: 0,
|
|
2127
|
+
roomId: this.roomIdFor(chatId, accountId)
|
|
2128
|
+
}
|
|
2129
|
+
] : this.listKnownTargets(accountId);
|
|
2130
|
+
const roomIds = knownTargets.map((known) => known.roomId ?? this.roomIdFor(known.chatId, known.accountId)).filter((roomId) => Boolean(roomId));
|
|
2131
|
+
if (roomIds.length === 0) {
|
|
2132
|
+
return [];
|
|
1270
2133
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
2134
|
+
const limit = Number.isFinite(params.limit) ? Math.max(1, Math.min(Number(params.limit), 100)) : 25;
|
|
2135
|
+
const memories = await this.runtime.getMemoriesByRoomIds({
|
|
2136
|
+
tableName: "messages",
|
|
2137
|
+
roomIds,
|
|
2138
|
+
limit: limit * Math.max(roomIds.length, 1)
|
|
2139
|
+
});
|
|
2140
|
+
const chatIds = new Set(knownTargets.map((known) => normalizeWhatsAppConnectorTarget(known.chatId)));
|
|
2141
|
+
const before = params.before ? Number(params.before) : undefined;
|
|
2142
|
+
const after = params.after ? Number(params.after) : undefined;
|
|
2143
|
+
return memories.filter((memory) => memory.content.source === "whatsapp").filter((memory) => this.metadataMatchesAccount(memory, accountId)).filter((memory) => {
|
|
2144
|
+
const metadata = memory.metadata;
|
|
2145
|
+
const rawChatId = typeof metadata?.rawChatId === "string" ? normalizeWhatsAppConnectorTarget(metadata.rawChatId) : undefined;
|
|
2146
|
+
if (chatId && rawChatId && !chatIds.has(rawChatId)) {
|
|
2147
|
+
return false;
|
|
2148
|
+
}
|
|
2149
|
+
const createdAt = Number(memory.createdAt ?? 0);
|
|
2150
|
+
if (before !== undefined && Number.isFinite(before) && createdAt >= before) {
|
|
2151
|
+
return false;
|
|
2152
|
+
}
|
|
2153
|
+
if (after !== undefined && Number.isFinite(after) && createdAt <= after) {
|
|
2154
|
+
return false;
|
|
2155
|
+
}
|
|
2156
|
+
return true;
|
|
2157
|
+
}).sort((left, right) => Number(right.createdAt ?? 0) - Number(left.createdAt ?? 0)).slice(0, limit);
|
|
1279
2158
|
}
|
|
1280
|
-
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
2159
|
+
async searchConnectorMessages(context, params) {
|
|
2160
|
+
const query = params.query?.trim().toLowerCase();
|
|
2161
|
+
if (!query) {
|
|
2162
|
+
return [];
|
|
2163
|
+
}
|
|
2164
|
+
const memories = await this.fetchConnectorMessages(context, {
|
|
2165
|
+
...params,
|
|
2166
|
+
limit: Math.max(params.limit ?? 100, 100)
|
|
2167
|
+
});
|
|
2168
|
+
return memories.filter((memory) => {
|
|
2169
|
+
const text = String(memory.content.text ?? "").toLowerCase();
|
|
2170
|
+
const from = String(memory.content.from ?? "").toLowerCase();
|
|
2171
|
+
return text.includes(query) || from.includes(query);
|
|
2172
|
+
}).slice(0, params.limit ?? 25);
|
|
2173
|
+
}
|
|
2174
|
+
async reactConnectorMessage(runtime, params) {
|
|
2175
|
+
const target = params.target;
|
|
2176
|
+
const resolved = target ? await resolveWhatsAppSendTarget(runtime, this, target) : params.channelId ? { accountId: this.defaultAccountId, chatId: params.channelId } : null;
|
|
2177
|
+
const accountId = this.resolveAccountId(resolved?.accountId ?? readTargetAccountId(target));
|
|
2178
|
+
const client = this.getClientForAccount(accountId);
|
|
2179
|
+
const config = this.getConfigForAccount(accountId);
|
|
2180
|
+
if (!client || !config) {
|
|
2181
|
+
throw new Error("WhatsApp client is not initialized");
|
|
2182
|
+
}
|
|
2183
|
+
const chatId = params.channelId ?? resolved?.chatId ?? (params.roomId ? (await runtime.getRoom(params.roomId))?.channelId : undefined);
|
|
2184
|
+
if (!chatId) {
|
|
2185
|
+
throw new Error("WhatsApp reaction requires a target chat.");
|
|
2186
|
+
}
|
|
2187
|
+
if (!params.messageId) {
|
|
2188
|
+
throw new Error("WhatsApp reaction requires messageId.");
|
|
2189
|
+
}
|
|
2190
|
+
await client.sendMessage({
|
|
2191
|
+
type: "reaction",
|
|
2192
|
+
to: config.transport === "baileys" ? normalizeBaileysSendTarget(chatId) : normalizeWhatsAppTarget(chatId) ?? chatId,
|
|
2193
|
+
content: {
|
|
2194
|
+
messageId: params.messageId,
|
|
2195
|
+
emoji: params.remove ? "" : params.emoji || "\uD83D\uDC4D"
|
|
2196
|
+
}
|
|
1285
2197
|
});
|
|
2198
|
+
}
|
|
2199
|
+
async getConnectorUser(_runtime, params) {
|
|
2200
|
+
const lookup = params.userId ?? params.handle ?? params.username ?? params.query;
|
|
2201
|
+
if (!lookup) {
|
|
2202
|
+
return null;
|
|
2203
|
+
}
|
|
2204
|
+
const normalized = normalizeWhatsAppConnectorTarget(lookup);
|
|
2205
|
+
const known = this.findKnownChatByParticipant(normalized) ?? this.getKnownTarget(normalized);
|
|
2206
|
+
if (!known) {
|
|
2207
|
+
return null;
|
|
2208
|
+
}
|
|
1286
2209
|
return {
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
2210
|
+
id: this.entityIdFor(known.senderId, known.accountId),
|
|
2211
|
+
agentId: this.runtime.agentId,
|
|
2212
|
+
names: [known.label, known.senderId, known.chatId].filter((value) => typeof value === "string" && value.length > 0),
|
|
2213
|
+
metadata: {
|
|
2214
|
+
accountId: known.accountId,
|
|
2215
|
+
source: "whatsapp",
|
|
2216
|
+
whatsapp: {
|
|
2217
|
+
accountId: known.accountId,
|
|
2218
|
+
chatId: known.chatId,
|
|
2219
|
+
senderId: known.senderId,
|
|
2220
|
+
isGroup: known.isGroup
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
1291
2223
|
};
|
|
1292
2224
|
}
|
|
1293
|
-
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
2225
|
+
listKnownTargets(accountId) {
|
|
2226
|
+
const normalizedAccountId = accountId ? this.resolveAccountId(accountId) : null;
|
|
2227
|
+
return Array.from(this.knownTargets.values()).filter((target) => !normalizedAccountId || target.accountId === normalizedAccountId).sort((left, right) => right.lastMessageAt - left.lastMessageAt);
|
|
2228
|
+
}
|
|
2229
|
+
getKnownTarget(chatId, accountId) {
|
|
2230
|
+
const normalized = normalizeWhatsAppConnectorTarget(chatId);
|
|
2231
|
+
if (accountId) {
|
|
2232
|
+
return this.knownTargets.get(this.targetKey(normalized, accountId)) ?? null;
|
|
2233
|
+
}
|
|
2234
|
+
return this.knownTargets.get(this.targetKey(normalized, this.defaultAccountId)) ?? Array.from(this.knownTargets.values()).find((target) => normalizeWhatsAppConnectorTarget(target.chatId) === normalized) ?? null;
|
|
2235
|
+
}
|
|
2236
|
+
findKnownChatByParticipant(participant, accountId) {
|
|
2237
|
+
const normalized = normalizeWhatsAppConnectorTarget(participant);
|
|
2238
|
+
const normalizedAccountId = accountId ? this.resolveAccountId(accountId) : null;
|
|
2239
|
+
for (const target of this.knownTargets.values()) {
|
|
2240
|
+
if (normalizedAccountId && target.accountId !== normalizedAccountId) {
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
2243
|
+
if (normalizeWhatsAppConnectorTarget(target.senderId) === normalized || normalizeWhatsAppConnectorTarget(target.chatId) === normalized) {
|
|
2244
|
+
return target;
|
|
2245
|
+
}
|
|
1297
2246
|
}
|
|
2247
|
+
return null;
|
|
2248
|
+
}
|
|
2249
|
+
rememberTarget(target) {
|
|
2250
|
+
this.knownTargets.set(this.targetKey(target.chatId, target.accountId), {
|
|
2251
|
+
...target,
|
|
2252
|
+
accountId: this.resolveAccountId(target.accountId)
|
|
2253
|
+
});
|
|
1298
2254
|
}
|
|
1299
|
-
const inDynamicAllowlist = await isInAllowlist(runtime, "whatsapp", identifier);
|
|
1300
|
-
return { allowed: inDynamicAllowlist };
|
|
1301
2255
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
2256
|
+
|
|
2257
|
+
// src/setup-routes.ts
|
|
2258
|
+
import fs2 from "node:fs";
|
|
2259
|
+
import path2 from "node:path";
|
|
2260
|
+
|
|
2261
|
+
// src/pairing-service.ts
|
|
2262
|
+
import fs from "node:fs";
|
|
2263
|
+
import path from "node:path";
|
|
2264
|
+
var LOG_PREFIX = "[whatsapp-pairing]";
|
|
2265
|
+
function sanitizeAccountId(raw) {
|
|
2266
|
+
const cleaned = raw.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
2267
|
+
if (!cleaned || cleaned !== raw) {
|
|
2268
|
+
throw new Error(`Invalid accountId: must only contain alphanumeric characters, dashes, and underscores`);
|
|
2269
|
+
}
|
|
2270
|
+
return cleaned;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
class WhatsAppPairingSession {
|
|
2274
|
+
socket = null;
|
|
2275
|
+
status = "idle";
|
|
2276
|
+
options;
|
|
2277
|
+
qrAttempts = 0;
|
|
2278
|
+
MAX_QR_ATTEMPTS = 5;
|
|
2279
|
+
restartTimer = null;
|
|
2280
|
+
constructor(options) {
|
|
2281
|
+
this.options = options;
|
|
2282
|
+
}
|
|
2283
|
+
async start() {
|
|
2284
|
+
this.setStatus("initializing");
|
|
2285
|
+
const baileys = await import("@whiskeysockets/baileys");
|
|
2286
|
+
const makeWASocket2 = baileys.default;
|
|
2287
|
+
const { useMultiFileAuthState: useMultiFileAuthState2, fetchLatestBaileysVersion, DisconnectReason: DisconnectReason2 } = baileys;
|
|
2288
|
+
const QRCode2 = (await import("qrcode")).default;
|
|
2289
|
+
const { Boom } = await import("@hapi/boom");
|
|
2290
|
+
fs.mkdirSync(this.options.authDir, { recursive: true });
|
|
2291
|
+
const { state, saveCreds } = await useMultiFileAuthState2(this.options.authDir);
|
|
2292
|
+
const { version } = await fetchLatestBaileysVersion();
|
|
2293
|
+
const pino2 = (await import("pino")).default;
|
|
2294
|
+
const baileysLogger = pino2({ level: "silent" });
|
|
2295
|
+
this.socket = makeWASocket2({
|
|
2296
|
+
version,
|
|
2297
|
+
auth: state,
|
|
2298
|
+
logger: baileysLogger,
|
|
2299
|
+
printQRInTerminal: false,
|
|
2300
|
+
browser: ["Eliza AI", "Desktop", "1.0.0"]
|
|
2301
|
+
});
|
|
2302
|
+
this.socket.ev.on("creds.update", saveCreds);
|
|
2303
|
+
this.socket.ev.on("connection.update", async (update) => {
|
|
2304
|
+
const { connection, lastDisconnect, qr } = update;
|
|
2305
|
+
if (qr) {
|
|
2306
|
+
this.qrAttempts++;
|
|
2307
|
+
console.info(`${LOG_PREFIX} QR code received (attempt ${this.qrAttempts}/${this.MAX_QR_ATTEMPTS})`);
|
|
2308
|
+
if (this.qrAttempts > this.MAX_QR_ATTEMPTS) {
|
|
2309
|
+
this.setStatus("timeout");
|
|
2310
|
+
this.stop();
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
try {
|
|
2314
|
+
const qrDataUrl = await QRCode2.toDataURL(qr, {
|
|
2315
|
+
width: 256,
|
|
2316
|
+
margin: 2,
|
|
2317
|
+
color: { dark: "#000000", light: "#ffffff" }
|
|
2318
|
+
});
|
|
2319
|
+
this.setStatus("waiting_for_qr");
|
|
2320
|
+
this.options.onEvent({
|
|
2321
|
+
type: "whatsapp-qr",
|
|
2322
|
+
accountId: this.options.accountId,
|
|
2323
|
+
qrDataUrl,
|
|
2324
|
+
expiresInMs: 20000
|
|
2325
|
+
});
|
|
2326
|
+
} catch {}
|
|
2327
|
+
}
|
|
2328
|
+
if (connection === "close") {
|
|
2329
|
+
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
2330
|
+
console.info(`${LOG_PREFIX} Connection closed, statusCode=${statusCode}, status=${this.status}`);
|
|
2331
|
+
if (statusCode === DisconnectReason2.loggedOut) {
|
|
2332
|
+
this.setStatus("disconnected");
|
|
2333
|
+
} else if (statusCode === DisconnectReason2.restartRequired || statusCode === DisconnectReason2.timedOut || statusCode === DisconnectReason2.connectionClosed || statusCode === DisconnectReason2.connectionReplaced) {
|
|
2334
|
+
console.info(`${LOG_PREFIX} Restarting pairing after transient close...`);
|
|
2335
|
+
this.socket = null;
|
|
2336
|
+
this.qrAttempts = 0;
|
|
2337
|
+
this.restartTimer = setTimeout(() => {
|
|
2338
|
+
this.restartTimer = null;
|
|
2339
|
+
this.start().catch((err) => {
|
|
2340
|
+
console.error(`${LOG_PREFIX} Restart failed:`, err);
|
|
2341
|
+
this.setStatus("error");
|
|
2342
|
+
this.options.onEvent({
|
|
2343
|
+
type: "whatsapp-status",
|
|
2344
|
+
accountId: this.options.accountId,
|
|
2345
|
+
status: "error",
|
|
2346
|
+
error: String(err)
|
|
2347
|
+
});
|
|
2348
|
+
});
|
|
2349
|
+
}, 3000);
|
|
2350
|
+
}
|
|
2351
|
+
} else if (connection === "open") {
|
|
2352
|
+
const phoneNumber = this.socket?.user?.id?.split(":")[0] ?? "";
|
|
2353
|
+
this.setStatus("connected");
|
|
2354
|
+
this.options.onEvent({
|
|
2355
|
+
type: "whatsapp-status",
|
|
2356
|
+
accountId: this.options.accountId,
|
|
2357
|
+
status: "connected",
|
|
2358
|
+
phoneNumber
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
stop() {
|
|
2364
|
+
if (this.restartTimer) {
|
|
2365
|
+
clearTimeout(this.restartTimer);
|
|
2366
|
+
this.restartTimer = null;
|
|
1313
2367
|
}
|
|
2368
|
+
try {
|
|
2369
|
+
this.socket?.end(undefined);
|
|
2370
|
+
} catch {}
|
|
2371
|
+
this.socket = null;
|
|
2372
|
+
}
|
|
2373
|
+
getStatus() {
|
|
2374
|
+
return this.status;
|
|
2375
|
+
}
|
|
2376
|
+
setStatus(status) {
|
|
2377
|
+
this.status = status;
|
|
2378
|
+
this.options.onEvent({
|
|
2379
|
+
type: "whatsapp-status",
|
|
2380
|
+
accountId: this.options.accountId,
|
|
2381
|
+
status
|
|
2382
|
+
});
|
|
1314
2383
|
}
|
|
1315
2384
|
}
|
|
1316
|
-
function
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
2385
|
+
function whatsappAuthExists(workspaceDir, accountId = "default") {
|
|
2386
|
+
const credsPath = path.join(workspaceDir, "whatsapp-auth", accountId, "creds.json");
|
|
2387
|
+
return fs.existsSync(credsPath);
|
|
2388
|
+
}
|
|
2389
|
+
async function whatsappLogout(workspaceDir, accountId = "default") {
|
|
2390
|
+
const authDir = path.join(workspaceDir, "whatsapp-auth", accountId);
|
|
2391
|
+
const credsPath = path.join(authDir, "creds.json");
|
|
2392
|
+
if (fs.existsSync(credsPath)) {
|
|
2393
|
+
try {
|
|
2394
|
+
const baileys = await import("@whiskeysockets/baileys");
|
|
2395
|
+
const makeWASocket2 = baileys.default;
|
|
2396
|
+
const { useMultiFileAuthState: useMultiFileAuthState2, fetchLatestBaileysVersion } = baileys;
|
|
2397
|
+
const pino2 = (await import("pino")).default;
|
|
2398
|
+
const logger = pino2({ level: "silent" });
|
|
2399
|
+
const { state } = await useMultiFileAuthState2(authDir);
|
|
2400
|
+
const { version } = await fetchLatestBaileysVersion();
|
|
2401
|
+
const sock = makeWASocket2({
|
|
2402
|
+
version,
|
|
2403
|
+
auth: state,
|
|
2404
|
+
logger,
|
|
2405
|
+
printQRInTerminal: false
|
|
2406
|
+
});
|
|
2407
|
+
await new Promise((resolve) => {
|
|
2408
|
+
let settled = false;
|
|
2409
|
+
const finish = () => {
|
|
2410
|
+
if (settled)
|
|
2411
|
+
return;
|
|
2412
|
+
settled = true;
|
|
2413
|
+
clearTimeout(timeout);
|
|
2414
|
+
try {
|
|
2415
|
+
sock.ev.removeAllListeners("connection.update");
|
|
2416
|
+
} catch {}
|
|
2417
|
+
try {
|
|
2418
|
+
sock.end(undefined);
|
|
2419
|
+
} catch {}
|
|
2420
|
+
resolve();
|
|
2421
|
+
};
|
|
2422
|
+
const timeout = setTimeout(finish, 1e4);
|
|
2423
|
+
sock.ev.on("connection.update", async (update) => {
|
|
2424
|
+
if (update.connection === "open") {
|
|
2425
|
+
try {
|
|
2426
|
+
await sock.logout();
|
|
2427
|
+
} catch {}
|
|
2428
|
+
finish();
|
|
2429
|
+
} else if (update.connection === "close") {
|
|
2430
|
+
finish();
|
|
2431
|
+
}
|
|
2432
|
+
});
|
|
2433
|
+
});
|
|
2434
|
+
} catch {}
|
|
1321
2435
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
2436
|
+
fs.rmSync(authDir, { recursive: true, force: true });
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
// src/webhook-auth.ts
|
|
2440
|
+
import crypto from "node:crypto";
|
|
2441
|
+
var SIGNATURE_HEADER = "x-hub-signature-256";
|
|
2442
|
+
function resolveWhatsAppAppSecret(runtime) {
|
|
2443
|
+
const fromSetting = runtime.getSetting("WHATSAPP_APP_SECRET");
|
|
2444
|
+
if (typeof fromSetting === "string" && fromSetting.trim()) {
|
|
2445
|
+
return fromSetting.trim();
|
|
2446
|
+
}
|
|
2447
|
+
const fromEnv = process.env.WHATSAPP_APP_SECRET;
|
|
2448
|
+
if (typeof fromEnv === "string" && fromEnv.trim()) {
|
|
2449
|
+
return fromEnv.trim();
|
|
1324
2450
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
2451
|
+
return null;
|
|
2452
|
+
}
|
|
2453
|
+
function verifyWhatsAppWebhookSignature(appSecret, signatureHeader, rawBody) {
|
|
2454
|
+
if (!signatureHeader || !appSecret || !signatureHeader.startsWith("sha256=")) {
|
|
2455
|
+
return false;
|
|
1327
2456
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
2457
|
+
try {
|
|
2458
|
+
const expectedSignature = signatureHeader.slice("sha256=".length);
|
|
2459
|
+
const computedSignature = crypto.createHmac("sha256", appSecret).update(rawBody).digest("hex");
|
|
2460
|
+
const expectedBuffer = Buffer.from(expectedSignature, "hex");
|
|
2461
|
+
const computedBuffer = Buffer.from(computedSignature, "hex");
|
|
2462
|
+
if (expectedBuffer.length !== computedBuffer.length) {
|
|
2463
|
+
return false;
|
|
2464
|
+
}
|
|
2465
|
+
return crypto.timingSafeEqual(expectedBuffer, computedBuffer);
|
|
2466
|
+
} catch {
|
|
2467
|
+
return false;
|
|
1330
2468
|
}
|
|
1331
|
-
return digitsOnly;
|
|
1332
2469
|
}
|
|
1333
|
-
function
|
|
1334
|
-
const
|
|
1335
|
-
|
|
1336
|
-
|
|
2470
|
+
function readRouteHeader(req, name) {
|
|
2471
|
+
const headers = req.headers;
|
|
2472
|
+
if (!headers)
|
|
2473
|
+
return;
|
|
2474
|
+
const key = name.toLowerCase();
|
|
2475
|
+
const value = headers[key] ?? headers[name] ?? headers[name.toUpperCase()];
|
|
2476
|
+
if (Array.isArray(value)) {
|
|
2477
|
+
return value[0];
|
|
2478
|
+
}
|
|
2479
|
+
return typeof value === "string" ? value : undefined;
|
|
2480
|
+
}
|
|
2481
|
+
function readWebhookRawBody(req) {
|
|
2482
|
+
if (typeof req.rawBody === "string" && req.rawBody.length > 0) {
|
|
2483
|
+
return req.rawBody;
|
|
2484
|
+
}
|
|
2485
|
+
return null;
|
|
2486
|
+
}
|
|
2487
|
+
function isWhatsAppWebhookAuthorized(runtime, req) {
|
|
2488
|
+
const rawBody = readWebhookRawBody(req);
|
|
2489
|
+
if (rawBody === null) {
|
|
1337
2490
|
return false;
|
|
1338
2491
|
}
|
|
1339
|
-
const
|
|
1340
|
-
if (!
|
|
2492
|
+
const appSecret = resolveWhatsAppAppSecret(runtime);
|
|
2493
|
+
if (!appSecret) {
|
|
1341
2494
|
return false;
|
|
1342
2495
|
}
|
|
1343
|
-
return
|
|
2496
|
+
return verifyWhatsAppWebhookSignature(appSecret, readRouteHeader(req, SIGNATURE_HEADER), rawBody);
|
|
1344
2497
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
2498
|
+
|
|
2499
|
+
// src/setup-routes.ts
|
|
2500
|
+
var whatsappPairingSessions = new Map;
|
|
2501
|
+
var MAX_PAIRING_SESSIONS = 10;
|
|
2502
|
+
function routeHost(req) {
|
|
2503
|
+
const host = req.headers?.host;
|
|
2504
|
+
return (Array.isArray(host) ? host[0] : host) ?? "localhost";
|
|
1348
2505
|
}
|
|
1349
|
-
function
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
2506
|
+
function isConnectorSetupService(service) {
|
|
2507
|
+
return typeof service === "object" && service !== null && typeof service.getConfig === "function" && typeof service.persistConfig === "function" && typeof service.updateConfig === "function" && typeof service.registerEscalationChannel === "function" && typeof service.setOwnerContact === "function" && typeof service.getWorkspaceDir === "function" && typeof service.broadcastWs === "function";
|
|
2508
|
+
}
|
|
2509
|
+
function getSetupService(runtime) {
|
|
2510
|
+
const service = runtime.getService("connector-setup");
|
|
2511
|
+
return isConnectorSetupService(service) ? service : null;
|
|
2512
|
+
}
|
|
2513
|
+
function cleanupStaleSessions() {
|
|
2514
|
+
for (const [id, session] of whatsappPairingSessions) {
|
|
2515
|
+
const status = session.getStatus();
|
|
2516
|
+
if (status === "disconnected" || status === "timeout" || status === "error") {
|
|
2517
|
+
session.stop();
|
|
2518
|
+
whatsappPairingSessions.delete(id);
|
|
2519
|
+
}
|
|
1353
2520
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
2521
|
+
}
|
|
2522
|
+
async function handleWebhookVerify(req, res, runtime) {
|
|
2523
|
+
const url = new URL(req.url ?? "/", `http://${routeHost(req)}`);
|
|
2524
|
+
const mode = url.searchParams.get("hub.mode") ?? "";
|
|
2525
|
+
const token = url.searchParams.get("hub.verify_token") ?? "";
|
|
2526
|
+
const challenge = url.searchParams.get("hub.challenge") ?? "";
|
|
2527
|
+
const accountId = url.searchParams.get("accountId") ?? undefined;
|
|
2528
|
+
const service = runtime.getService("whatsapp");
|
|
2529
|
+
if (!service || typeof service.verifyWebhook !== "function") {
|
|
2530
|
+
res.status(503).json({ error: "WhatsApp service unavailable" });
|
|
2531
|
+
return;
|
|
1357
2532
|
}
|
|
1358
|
-
|
|
2533
|
+
const verifiedChallenge = service.verifyWebhook(mode, token, challenge, accountId);
|
|
2534
|
+
if (!verifiedChallenge) {
|
|
2535
|
+
res.status(403).json({ error: "Webhook verification failed" });
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
res.status(200).json(verifiedChallenge);
|
|
1359
2539
|
}
|
|
1360
|
-
function
|
|
1361
|
-
const
|
|
1362
|
-
if (!
|
|
1363
|
-
|
|
2540
|
+
async function handleWebhookEvent(req, res, runtime) {
|
|
2541
|
+
const service = runtime.getService("whatsapp");
|
|
2542
|
+
if (!service || typeof service.handleWebhook !== "function") {
|
|
2543
|
+
res.status(503).json({ error: "WhatsApp service unavailable" });
|
|
2544
|
+
return;
|
|
1364
2545
|
}
|
|
1365
|
-
if (
|
|
1366
|
-
|
|
1367
|
-
return
|
|
2546
|
+
if (!isWhatsAppWebhookAuthorized(runtime, req)) {
|
|
2547
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
2548
|
+
return;
|
|
1368
2549
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
}
|
|
1374
|
-
const normalized2 = normalizeE164(phone);
|
|
1375
|
-
return normalized2.length > 1 ? normalized2 : null;
|
|
2550
|
+
const rawBody = readWebhookRawBody(req);
|
|
2551
|
+
if (!rawBody) {
|
|
2552
|
+
res.status(400).json({ error: "Missing request body" });
|
|
2553
|
+
return;
|
|
1376
2554
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
2555
|
+
let body;
|
|
2556
|
+
try {
|
|
2557
|
+
const parsed = JSON.parse(rawBody);
|
|
2558
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2559
|
+
res.status(400).json({ error: "Invalid request body" });
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
body = parsed;
|
|
2563
|
+
} catch {
|
|
2564
|
+
res.status(400).json({ error: "Invalid request body" });
|
|
2565
|
+
return;
|
|
1379
2566
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
2567
|
+
await service.handleWebhook(body);
|
|
2568
|
+
res.status(200).json("EVENT_RECEIVED");
|
|
1382
2569
|
}
|
|
1383
|
-
function
|
|
1384
|
-
|
|
1385
|
-
|
|
2570
|
+
async function handlePair(req, res, runtime) {
|
|
2571
|
+
cleanupStaleSessions();
|
|
2572
|
+
const setupService = getSetupService(runtime);
|
|
2573
|
+
const body = req.body;
|
|
2574
|
+
let accountId;
|
|
2575
|
+
try {
|
|
2576
|
+
accountId = sanitizeAccountId(body && typeof body.accountId === "string" && body.accountId.trim() ? body.accountId.trim() : "default");
|
|
2577
|
+
} catch (err) {
|
|
2578
|
+
res.status(400).json({ error: err.message });
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
const isReplacing = whatsappPairingSessions.has(accountId);
|
|
2582
|
+
if (!isReplacing && whatsappPairingSessions.size >= MAX_PAIRING_SESSIONS) {
|
|
2583
|
+
res.status(429).json({
|
|
2584
|
+
error: `Too many concurrent pairing sessions (max ${MAX_PAIRING_SESSIONS})`
|
|
2585
|
+
});
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
const workspaceDir = setupService?.getWorkspaceDir() ?? ".";
|
|
2589
|
+
const authDir = path2.join(workspaceDir, "whatsapp-auth", accountId);
|
|
2590
|
+
whatsappPairingSessions.get(accountId)?.stop();
|
|
2591
|
+
const session = new WhatsAppPairingSession({
|
|
2592
|
+
authDir,
|
|
2593
|
+
accountId,
|
|
2594
|
+
onEvent: (event) => {
|
|
2595
|
+
setupService?.broadcastWs(event);
|
|
2596
|
+
if (event.status === "connected") {
|
|
2597
|
+
if (setupService) {
|
|
2598
|
+
setupService.updateConfig((config) => {
|
|
2599
|
+
if (!config.connectors)
|
|
2600
|
+
config.connectors = {};
|
|
2601
|
+
const connectors = config.connectors;
|
|
2602
|
+
const previousConfig = connectors.whatsapp;
|
|
2603
|
+
if (accountId === "default") {
|
|
2604
|
+
connectors.whatsapp = {
|
|
2605
|
+
...previousConfig,
|
|
2606
|
+
authDir,
|
|
2607
|
+
transport: "baileys",
|
|
2608
|
+
enabled: true
|
|
2609
|
+
};
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
const accounts = typeof previousConfig.accounts === "object" && previousConfig.accounts !== null ? { ...previousConfig.accounts } : {};
|
|
2613
|
+
accounts[accountId] = {
|
|
2614
|
+
...accounts[accountId] ?? {},
|
|
2615
|
+
authDir,
|
|
2616
|
+
transport: "baileys",
|
|
2617
|
+
enabled: true
|
|
2618
|
+
};
|
|
2619
|
+
connectors.whatsapp = {
|
|
2620
|
+
...previousConfig,
|
|
2621
|
+
accounts,
|
|
2622
|
+
enabled: true
|
|
2623
|
+
};
|
|
2624
|
+
});
|
|
2625
|
+
const phoneNumber = event.phoneNumber;
|
|
2626
|
+
setupService.setOwnerContact({
|
|
2627
|
+
source: "whatsapp",
|
|
2628
|
+
channelId: phoneNumber ?? undefined
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
whatsappPairingSessions.set(accountId, session);
|
|
2635
|
+
try {
|
|
2636
|
+
await session.start();
|
|
2637
|
+
res.status(200).json({ ok: true, accountId, status: session.getStatus() });
|
|
2638
|
+
} catch (err) {
|
|
2639
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
1386
2640
|
}
|
|
1387
|
-
const normalized = normalizeWhatsAppTarget(id);
|
|
1388
|
-
return normalized || id;
|
|
1389
|
-
}
|
|
1390
|
-
function isWhatsAppGroup(id) {
|
|
1391
|
-
return isWhatsAppGroupJid(id);
|
|
1392
2641
|
}
|
|
1393
|
-
function
|
|
1394
|
-
|
|
2642
|
+
async function handleStatus(req, res, runtime) {
|
|
2643
|
+
cleanupStaleSessions();
|
|
2644
|
+
const setupService = getSetupService(runtime);
|
|
2645
|
+
const url = new URL(req.url ?? "/", `http://${routeHost(req)}`);
|
|
2646
|
+
let accountId;
|
|
2647
|
+
try {
|
|
2648
|
+
accountId = sanitizeAccountId(url.searchParams.get("accountId") || "default");
|
|
2649
|
+
} catch (err) {
|
|
2650
|
+
res.status(400).json({ error: err.message });
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
const session = whatsappPairingSessions.get(accountId);
|
|
2654
|
+
const workspaceDir = setupService?.getWorkspaceDir() ?? ".";
|
|
2655
|
+
let serviceConnected = false;
|
|
2656
|
+
let servicePhone = null;
|
|
2657
|
+
try {
|
|
2658
|
+
const waService = runtime.getService("whatsapp");
|
|
2659
|
+
if (waService && typeof waService === "object") {
|
|
2660
|
+
const waState = waService;
|
|
2661
|
+
serviceConnected = Boolean(waState.connected);
|
|
2662
|
+
servicePhone = typeof waState.phoneNumber === "string" ? waState.phoneNumber : null;
|
|
2663
|
+
}
|
|
2664
|
+
} catch {}
|
|
2665
|
+
res.status(200).json({
|
|
2666
|
+
accountId,
|
|
2667
|
+
status: session?.getStatus() ?? "idle",
|
|
2668
|
+
authExists: whatsappAuthExists(workspaceDir, accountId),
|
|
2669
|
+
serviceConnected,
|
|
2670
|
+
servicePhone
|
|
2671
|
+
});
|
|
1395
2672
|
}
|
|
1396
|
-
function
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
2673
|
+
async function handlePairStop(req, res, _runtime) {
|
|
2674
|
+
const body = req.body;
|
|
2675
|
+
let accountId;
|
|
2676
|
+
try {
|
|
2677
|
+
accountId = sanitizeAccountId(body && typeof body.accountId === "string" && body.accountId.trim() ? body.accountId.trim() : "default");
|
|
2678
|
+
} catch (err) {
|
|
2679
|
+
res.status(400).json({ error: err.message });
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
const session = whatsappPairingSessions.get(accountId);
|
|
2683
|
+
if (session) {
|
|
2684
|
+
session.stop();
|
|
2685
|
+
whatsappPairingSessions.delete(accountId);
|
|
2686
|
+
}
|
|
2687
|
+
res.status(200).json({ ok: true, accountId, status: "idle" });
|
|
1400
2688
|
}
|
|
1401
|
-
function
|
|
1402
|
-
|
|
1403
|
-
|
|
2689
|
+
async function handleDisconnect(req, res, runtime) {
|
|
2690
|
+
const setupService = getSetupService(runtime);
|
|
2691
|
+
const body = req.body;
|
|
2692
|
+
let accountId;
|
|
2693
|
+
try {
|
|
2694
|
+
accountId = sanitizeAccountId(body && typeof body.accountId === "string" && body.accountId.trim() ? body.accountId.trim() : "default");
|
|
2695
|
+
} catch (err) {
|
|
2696
|
+
res.status(400).json({ error: err.message });
|
|
2697
|
+
return;
|
|
1404
2698
|
}
|
|
1405
|
-
const
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
2699
|
+
const session = whatsappPairingSessions.get(accountId);
|
|
2700
|
+
if (session) {
|
|
2701
|
+
session.stop();
|
|
2702
|
+
whatsappPairingSessions.delete(accountId);
|
|
2703
|
+
}
|
|
2704
|
+
const workspaceDir = setupService?.getWorkspaceDir() ?? ".";
|
|
2705
|
+
try {
|
|
2706
|
+
await whatsappLogout(workspaceDir, accountId);
|
|
2707
|
+
} catch (logoutErr) {
|
|
2708
|
+
console.warn(`[whatsapp] Logout failed for ${accountId}, deleting auth files directly:`, String(logoutErr));
|
|
2709
|
+
const authDir = path2.join(workspaceDir, "whatsapp-auth", accountId);
|
|
2710
|
+
try {
|
|
2711
|
+
fs2.rmSync(authDir, { recursive: true, force: true });
|
|
2712
|
+
} catch {}
|
|
2713
|
+
}
|
|
2714
|
+
if (setupService) {
|
|
2715
|
+
setupService.updateConfig((config) => {
|
|
2716
|
+
const connectors = config.connectors;
|
|
2717
|
+
if (connectors) {
|
|
2718
|
+
if (accountId === "default") {
|
|
2719
|
+
delete connectors.whatsapp;
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
const whatsappConfig = connectors.whatsapp;
|
|
2723
|
+
const accounts = whatsappConfig?.accounts;
|
|
2724
|
+
if (accounts) {
|
|
2725
|
+
delete accounts[accountId];
|
|
2726
|
+
}
|
|
2727
|
+
connectors.whatsapp = {
|
|
2728
|
+
...whatsappConfig ?? {},
|
|
2729
|
+
...accounts ? { accounts } : {}
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
});
|
|
1414
2733
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
2734
|
+
res.status(200).json({ ok: true, accountId });
|
|
2735
|
+
}
|
|
2736
|
+
var whatsappSetupRoutes = [
|
|
2737
|
+
{
|
|
2738
|
+
name: "whatsapp-webhook-verify",
|
|
2739
|
+
type: "GET",
|
|
2740
|
+
path: "/api/whatsapp/webhook",
|
|
2741
|
+
handler: handleWebhookVerify,
|
|
2742
|
+
rawPath: true,
|
|
2743
|
+
public: true
|
|
2744
|
+
},
|
|
2745
|
+
{
|
|
2746
|
+
name: "whatsapp-webhook-event",
|
|
2747
|
+
type: "POST",
|
|
2748
|
+
path: "/api/whatsapp/webhook",
|
|
2749
|
+
handler: handleWebhookEvent,
|
|
2750
|
+
rawPath: true,
|
|
2751
|
+
public: true
|
|
2752
|
+
},
|
|
2753
|
+
{
|
|
2754
|
+
type: "POST",
|
|
2755
|
+
path: "/api/whatsapp/pair",
|
|
2756
|
+
handler: handlePair,
|
|
2757
|
+
rawPath: true
|
|
2758
|
+
},
|
|
2759
|
+
{
|
|
2760
|
+
type: "GET",
|
|
2761
|
+
path: "/api/whatsapp/status",
|
|
2762
|
+
handler: handleStatus,
|
|
2763
|
+
rawPath: true
|
|
2764
|
+
},
|
|
2765
|
+
{
|
|
2766
|
+
type: "POST",
|
|
2767
|
+
path: "/api/whatsapp/pair/stop",
|
|
2768
|
+
handler: handlePairStop,
|
|
2769
|
+
rawPath: true
|
|
2770
|
+
},
|
|
2771
|
+
{
|
|
2772
|
+
type: "POST",
|
|
2773
|
+
path: "/api/whatsapp/disconnect",
|
|
2774
|
+
handler: handleDisconnect,
|
|
2775
|
+
rawPath: true
|
|
2776
|
+
}
|
|
2777
|
+
];
|
|
2778
|
+
function stopAllPairingSessions() {
|
|
2779
|
+
for (const session of whatsappPairingSessions.values()) {
|
|
2780
|
+
try {
|
|
2781
|
+
session.stop();
|
|
2782
|
+
} catch {}
|
|
1422
2783
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
2784
|
+
whatsappPairingSessions.clear();
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
// src/workflow-credential-provider.ts
|
|
2788
|
+
import { Service as Service2 } from "@elizaos/core";
|
|
2789
|
+
var WORKFLOW_CREDENTIAL_PROVIDER_TYPE = "workflow_credential_provider";
|
|
2790
|
+
var SUPPORTED = ["whatsAppApi"];
|
|
2791
|
+
|
|
2792
|
+
class WhatsAppWorkflowCredentialProvider extends Service2 {
|
|
2793
|
+
static serviceType = WORKFLOW_CREDENTIAL_PROVIDER_TYPE;
|
|
2794
|
+
capabilityDescription = "Supplies WhatsApp credentials to the workflow plugin.";
|
|
2795
|
+
static async start(runtime) {
|
|
2796
|
+
return new WhatsAppWorkflowCredentialProvider(runtime);
|
|
2797
|
+
}
|
|
2798
|
+
async stop() {}
|
|
2799
|
+
async resolve(_userId, credType) {
|
|
2800
|
+
if (credType !== "whatsAppApi")
|
|
2801
|
+
return null;
|
|
2802
|
+
const accessToken = this.runtime.getSetting("WHATSAPP_ACCESS_TOKEN");
|
|
2803
|
+
const phoneNumberId = this.runtime.getSetting("WHATSAPP_PHONE_NUMBER_ID");
|
|
2804
|
+
if (!accessToken?.trim() || !phoneNumberId?.trim())
|
|
2805
|
+
return null;
|
|
1425
2806
|
return {
|
|
1426
|
-
|
|
1427
|
-
|
|
2807
|
+
status: "credential_data",
|
|
2808
|
+
data: { accessToken: accessToken.trim(), phoneNumberId: phoneNumberId.trim() }
|
|
1428
2809
|
};
|
|
1429
2810
|
}
|
|
1430
|
-
|
|
1431
|
-
if (space > limit * 0.5) {
|
|
2811
|
+
checkCredentialTypes(credTypes) {
|
|
1432
2812
|
return {
|
|
1433
|
-
|
|
1434
|
-
|
|
2813
|
+
supported: credTypes.filter((t) => SUPPORTED.includes(t)),
|
|
2814
|
+
unsupported: credTypes.filter((t) => !SUPPORTED.includes(t))
|
|
1435
2815
|
};
|
|
1436
2816
|
}
|
|
1437
|
-
return {
|
|
1438
|
-
chunk: text.slice(0, limit),
|
|
1439
|
-
remainder: text.slice(limit)
|
|
1440
|
-
};
|
|
1441
2817
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
2818
|
+
// src/api/whatsapp-routes.ts
|
|
2819
|
+
import fs3 from "node:fs";
|
|
2820
|
+
import path3 from "node:path";
|
|
2821
|
+
import { logger } from "@elizaos/core";
|
|
2822
|
+
var MAX_BODY_BYTES = 1048576;
|
|
2823
|
+
var MAX_PAIRING_SESSIONS2 = 10;
|
|
2824
|
+
async function readJsonBody(req, res) {
|
|
2825
|
+
let bytes = 0;
|
|
2826
|
+
let body = "";
|
|
2827
|
+
try {
|
|
2828
|
+
for await (const chunk of req) {
|
|
2829
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
2830
|
+
bytes += Buffer.byteLength(text);
|
|
2831
|
+
if (bytes > MAX_BODY_BYTES) {
|
|
2832
|
+
json(res, { error: "Request body too large" }, 413);
|
|
2833
|
+
return null;
|
|
2834
|
+
}
|
|
2835
|
+
body += text;
|
|
2836
|
+
}
|
|
2837
|
+
} catch (err) {
|
|
2838
|
+
logger.warn({ err }, "Failed to read WhatsApp request body");
|
|
2839
|
+
json(res, { error: "Failed to read request body" }, 400);
|
|
2840
|
+
return null;
|
|
1446
2841
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
return [normalizedText];
|
|
2842
|
+
if (!body.trim()) {
|
|
2843
|
+
return {};
|
|
1450
2844
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
chunks.push(chunk);
|
|
1457
|
-
}
|
|
1458
|
-
remaining = remainder;
|
|
2845
|
+
try {
|
|
2846
|
+
return JSON.parse(body);
|
|
2847
|
+
} catch {
|
|
2848
|
+
json(res, { error: "Invalid JSON body" }, 400);
|
|
2849
|
+
return null;
|
|
1459
2850
|
}
|
|
1460
|
-
return chunks.filter((c) => c.length > 0);
|
|
1461
2851
|
}
|
|
1462
|
-
function
|
|
1463
|
-
if (
|
|
1464
|
-
|
|
2852
|
+
function json(res, data, status = 200) {
|
|
2853
|
+
if (!res.headersSent) {
|
|
2854
|
+
res.statusCode = status;
|
|
2855
|
+
res.setHeader("Content-Type", "application/json");
|
|
1465
2856
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
2857
|
+
res.end(JSON.stringify(data));
|
|
2858
|
+
}
|
|
2859
|
+
function setOwnerContact(config, update) {
|
|
2860
|
+
if (!update.source)
|
|
2861
|
+
return false;
|
|
2862
|
+
if (!config.agents)
|
|
2863
|
+
config.agents = {};
|
|
2864
|
+
if (!config.agents.defaults)
|
|
2865
|
+
config.agents.defaults = {};
|
|
2866
|
+
if (!config.agents.defaults.ownerContacts)
|
|
2867
|
+
config.agents.defaults.ownerContacts = {};
|
|
2868
|
+
const existing = config.agents.defaults.ownerContacts[update.source];
|
|
2869
|
+
const entry = {};
|
|
2870
|
+
if (update.channelId)
|
|
2871
|
+
entry.channelId = update.channelId;
|
|
2872
|
+
if (update.entityId)
|
|
2873
|
+
entry.entityId = update.entityId;
|
|
2874
|
+
if (update.roomId)
|
|
2875
|
+
entry.roomId = update.roomId;
|
|
2876
|
+
if (Object.keys(entry).length === 0)
|
|
2877
|
+
return false;
|
|
2878
|
+
if (existing && existing.channelId === entry.channelId && existing.entityId === entry.entityId && existing.roomId === entry.roomId) {
|
|
2879
|
+
return false;
|
|
1468
2880
|
}
|
|
1469
|
-
|
|
2881
|
+
config.agents.defaults.ownerContacts[update.source] = entry;
|
|
2882
|
+
return true;
|
|
1470
2883
|
}
|
|
1471
|
-
function
|
|
1472
|
-
|
|
1473
|
-
const name = chatName || chatId.slice(0, 8);
|
|
1474
|
-
return `WhatsApp ${chatType}:${name}`;
|
|
2884
|
+
function shouldConfigurePlugin(body) {
|
|
2885
|
+
return body?.configurePlugin !== false;
|
|
1475
2886
|
}
|
|
1476
|
-
function
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
2887
|
+
function resolveAuthScope(value) {
|
|
2888
|
+
return value === "lifeops" ? "lifeops" : "platform";
|
|
2889
|
+
}
|
|
2890
|
+
function resolveSessionKey(authScope, accountId) {
|
|
2891
|
+
return `${authScope}:${accountId}`;
|
|
2892
|
+
}
|
|
2893
|
+
function resolveAuthDir(workspaceDir, accountId, authScope) {
|
|
2894
|
+
return path3.join(workspaceDir, authScope === "lifeops" ? "lifeops-whatsapp-auth" : "whatsapp-auth", accountId);
|
|
2895
|
+
}
|
|
2896
|
+
function authExistsForScope(state, deps, accountId, authScope) {
|
|
2897
|
+
if (authScope === "platform") {
|
|
2898
|
+
return deps.whatsappAuthExists(state.workspaceDir, accountId);
|
|
1480
2899
|
}
|
|
1481
|
-
|
|
2900
|
+
return fs3.existsSync(path3.join(resolveAuthDir(state.workspaceDir, accountId, authScope), "creds.json"));
|
|
2901
|
+
}
|
|
2902
|
+
async function handleWhatsAppRoute(req, res, pathname, method, state, deps) {
|
|
2903
|
+
if (!pathname.startsWith("/api/whatsapp"))
|
|
1482
2904
|
return false;
|
|
2905
|
+
if (pathname === "/api/whatsapp/webhook" && method === "GET") {
|
|
2906
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
2907
|
+
const mode = url.searchParams.get("hub.mode") ?? "";
|
|
2908
|
+
const token = url.searchParams.get("hub.verify_token") ?? "";
|
|
2909
|
+
const challenge = url.searchParams.get("hub.challenge") ?? "";
|
|
2910
|
+
const service = state.runtime?.getService("whatsapp");
|
|
2911
|
+
if (!service || typeof service.verifyWebhook !== "function") {
|
|
2912
|
+
json(res, { error: "WhatsApp service unavailable" }, 503);
|
|
2913
|
+
return true;
|
|
2914
|
+
}
|
|
2915
|
+
const verifiedChallenge = service.verifyWebhook(mode, token, challenge);
|
|
2916
|
+
if (!verifiedChallenge) {
|
|
2917
|
+
json(res, { error: "Webhook verification failed" }, 403);
|
|
2918
|
+
return true;
|
|
2919
|
+
}
|
|
2920
|
+
res.statusCode = 200;
|
|
2921
|
+
res.setHeader("Content-Type", "text/plain");
|
|
2922
|
+
res.end(verifiedChallenge);
|
|
2923
|
+
return true;
|
|
1483
2924
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
2925
|
+
if (pathname === "/api/whatsapp/webhook" && method === "POST") {
|
|
2926
|
+
const service = state.runtime?.getService("whatsapp");
|
|
2927
|
+
if (!service || typeof service.handleWebhook !== "function") {
|
|
2928
|
+
json(res, { error: "WhatsApp service unavailable" }, 503);
|
|
2929
|
+
return true;
|
|
2930
|
+
}
|
|
2931
|
+
const body = await readJsonBody(req, res);
|
|
2932
|
+
if (!body) {
|
|
2933
|
+
return true;
|
|
2934
|
+
}
|
|
2935
|
+
await service.handleWebhook(body);
|
|
2936
|
+
res.statusCode = 200;
|
|
2937
|
+
res.setHeader("Content-Type", "text/plain");
|
|
2938
|
+
res.end("EVENT_RECEIVED");
|
|
2939
|
+
return true;
|
|
2940
|
+
}
|
|
2941
|
+
if (method === "POST" && pathname === "/api/whatsapp/pair") {
|
|
2942
|
+
const body = await readJsonBody(req, res);
|
|
2943
|
+
const authScope = resolveAuthScope(body?.authScope);
|
|
2944
|
+
const configurePlugin = authScope === "platform" && shouldConfigurePlugin(body);
|
|
2945
|
+
let accountId;
|
|
2946
|
+
try {
|
|
2947
|
+
accountId = deps.sanitizeAccountId(body && typeof body.accountId === "string" && body.accountId.trim() ? body.accountId.trim() : "default");
|
|
2948
|
+
} catch (err) {
|
|
2949
|
+
json(res, { error: err.message }, 400);
|
|
2950
|
+
return true;
|
|
2951
|
+
}
|
|
2952
|
+
const sessionKey = resolveSessionKey(authScope, accountId);
|
|
2953
|
+
const isReplacing = state.whatsappPairingSessions.has(sessionKey);
|
|
2954
|
+
if (!isReplacing && state.whatsappPairingSessions.size >= MAX_PAIRING_SESSIONS2) {
|
|
2955
|
+
json(res, {
|
|
2956
|
+
error: `Too many concurrent pairing sessions (max ${MAX_PAIRING_SESSIONS2})`
|
|
2957
|
+
}, 429);
|
|
2958
|
+
return true;
|
|
2959
|
+
}
|
|
2960
|
+
const authDir = resolveAuthDir(state.workspaceDir, accountId, authScope);
|
|
2961
|
+
state.whatsappPairingSessions.get(sessionKey)?.stop();
|
|
2962
|
+
const session = deps.createWhatsAppPairingSession({
|
|
2963
|
+
authDir,
|
|
2964
|
+
accountId,
|
|
2965
|
+
onEvent: (event) => {
|
|
2966
|
+
state.broadcastWs?.({ ...event, authScope });
|
|
2967
|
+
if (event.status === "connected") {
|
|
2968
|
+
let configChanged = false;
|
|
2969
|
+
if (configurePlugin) {
|
|
2970
|
+
if (!state.config.connectors)
|
|
2971
|
+
state.config.connectors = {};
|
|
2972
|
+
state.config.connectors.whatsapp = {
|
|
2973
|
+
...state.config.connectors.whatsapp ?? {},
|
|
2974
|
+
authDir,
|
|
2975
|
+
enabled: true
|
|
2976
|
+
};
|
|
2977
|
+
configChanged = true;
|
|
2978
|
+
}
|
|
2979
|
+
const phoneNumber = event.phoneNumber;
|
|
2980
|
+
configChanged = setOwnerContact(state.config, {
|
|
2981
|
+
source: "whatsapp",
|
|
2982
|
+
channelId: phoneNumber ?? undefined
|
|
2983
|
+
}) || configChanged;
|
|
2984
|
+
if (!configChanged) {
|
|
2985
|
+
return;
|
|
2986
|
+
}
|
|
2987
|
+
try {
|
|
2988
|
+
state.saveConfig();
|
|
2989
|
+
} catch {}
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
});
|
|
2993
|
+
state.whatsappPairingSessions.set(sessionKey, session);
|
|
2994
|
+
try {
|
|
2995
|
+
await session.start();
|
|
2996
|
+
json(res, {
|
|
2997
|
+
ok: true,
|
|
2998
|
+
accountId,
|
|
2999
|
+
authScope,
|
|
3000
|
+
status: session.getStatus()
|
|
3001
|
+
});
|
|
3002
|
+
} catch (err) {
|
|
3003
|
+
json(res, { ok: false, error: String(err) }, 500);
|
|
3004
|
+
}
|
|
3005
|
+
return true;
|
|
3006
|
+
}
|
|
3007
|
+
if (method === "GET" && pathname === "/api/whatsapp/status") {
|
|
3008
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
3009
|
+
let accountId;
|
|
3010
|
+
try {
|
|
3011
|
+
accountId = deps.sanitizeAccountId(url.searchParams.get("accountId") || "default");
|
|
3012
|
+
} catch (err) {
|
|
3013
|
+
json(res, { error: err.message }, 400);
|
|
3014
|
+
return true;
|
|
3015
|
+
}
|
|
3016
|
+
const authScope = resolveAuthScope(url.searchParams.get("authScope"));
|
|
3017
|
+
const sessionKey = resolveSessionKey(authScope, accountId);
|
|
3018
|
+
const session = state.whatsappPairingSessions.get(sessionKey);
|
|
3019
|
+
let serviceConnected = false;
|
|
3020
|
+
let servicePhone = null;
|
|
3021
|
+
if (state.runtime) {
|
|
3022
|
+
try {
|
|
3023
|
+
const waService = state.runtime.getService("whatsapp");
|
|
3024
|
+
if (waService) {
|
|
3025
|
+
serviceConnected = Boolean(waService.connected);
|
|
3026
|
+
servicePhone = waService.phoneNumber ?? null;
|
|
3027
|
+
}
|
|
3028
|
+
} catch {}
|
|
3029
|
+
}
|
|
3030
|
+
json(res, {
|
|
3031
|
+
accountId,
|
|
3032
|
+
authScope,
|
|
3033
|
+
status: session?.getStatus() ?? "idle",
|
|
3034
|
+
authExists: authExistsForScope(state, deps, accountId, authScope),
|
|
3035
|
+
serviceConnected,
|
|
3036
|
+
servicePhone
|
|
3037
|
+
});
|
|
3038
|
+
return true;
|
|
3039
|
+
}
|
|
3040
|
+
if (method === "POST" && pathname === "/api/whatsapp/pair/stop") {
|
|
3041
|
+
const body = await readJsonBody(req, res);
|
|
3042
|
+
const authScope = resolveAuthScope(body?.authScope);
|
|
3043
|
+
let accountId;
|
|
3044
|
+
try {
|
|
3045
|
+
accountId = deps.sanitizeAccountId(body && typeof body.accountId === "string" && body.accountId.trim() ? body.accountId.trim() : "default");
|
|
3046
|
+
} catch (err) {
|
|
3047
|
+
json(res, { error: err.message }, 400);
|
|
3048
|
+
return true;
|
|
3049
|
+
}
|
|
3050
|
+
const sessionKey = resolveSessionKey(authScope, accountId);
|
|
3051
|
+
const session = state.whatsappPairingSessions.get(sessionKey);
|
|
3052
|
+
if (session) {
|
|
3053
|
+
session.stop();
|
|
3054
|
+
state.whatsappPairingSessions.delete(sessionKey);
|
|
3055
|
+
}
|
|
3056
|
+
json(res, { ok: true, accountId, authScope, status: "idle" });
|
|
3057
|
+
return true;
|
|
3058
|
+
}
|
|
3059
|
+
if (method === "POST" && pathname === "/api/whatsapp/disconnect") {
|
|
3060
|
+
const body = await readJsonBody(req, res);
|
|
3061
|
+
const authScope = resolveAuthScope(body?.authScope);
|
|
3062
|
+
const configurePlugin = authScope === "platform" && shouldConfigurePlugin(body);
|
|
3063
|
+
let accountId;
|
|
3064
|
+
try {
|
|
3065
|
+
accountId = deps.sanitizeAccountId(body && typeof body.accountId === "string" && body.accountId.trim() ? body.accountId.trim() : "default");
|
|
3066
|
+
} catch (err) {
|
|
3067
|
+
json(res, { error: err.message }, 400);
|
|
3068
|
+
return true;
|
|
3069
|
+
}
|
|
3070
|
+
const sessionKey = resolveSessionKey(authScope, accountId);
|
|
3071
|
+
const session = state.whatsappPairingSessions.get(sessionKey);
|
|
3072
|
+
if (session) {
|
|
3073
|
+
session.stop();
|
|
3074
|
+
state.whatsappPairingSessions.delete(sessionKey);
|
|
3075
|
+
}
|
|
3076
|
+
const authDir = resolveAuthDir(state.workspaceDir, accountId, authScope);
|
|
3077
|
+
try {
|
|
3078
|
+
if (authScope === "platform") {
|
|
3079
|
+
await deps.whatsappLogout(state.workspaceDir, accountId);
|
|
3080
|
+
} else {
|
|
3081
|
+
fs3.rmSync(authDir, { recursive: true, force: true });
|
|
3082
|
+
}
|
|
3083
|
+
} catch (logoutErr) {
|
|
3084
|
+
logger.warn({
|
|
3085
|
+
accountId,
|
|
3086
|
+
error: logoutErr instanceof Error ? logoutErr.message : String(logoutErr)
|
|
3087
|
+
}, "[whatsapp] Logout failed, deleting auth files directly");
|
|
3088
|
+
try {
|
|
3089
|
+
fs3.rmSync(authDir, { recursive: true, force: true });
|
|
3090
|
+
} catch {}
|
|
3091
|
+
}
|
|
3092
|
+
if (configurePlugin && state.config.connectors) {
|
|
3093
|
+
delete state.config.connectors.whatsapp;
|
|
3094
|
+
try {
|
|
3095
|
+
state.saveConfig();
|
|
3096
|
+
} catch {}
|
|
3097
|
+
}
|
|
3098
|
+
json(res, { ok: true, accountId, authScope });
|
|
3099
|
+
return true;
|
|
3100
|
+
}
|
|
3101
|
+
return false;
|
|
1486
3102
|
}
|
|
1487
|
-
function
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
3103
|
+
function applyWhatsAppQrOverride(plugins, workspaceDir) {
|
|
3104
|
+
try {
|
|
3105
|
+
const waCredsPath = path3.join(workspaceDir, "whatsapp-auth", "default", "creds.json");
|
|
3106
|
+
if (fs3.existsSync(waCredsPath)) {
|
|
3107
|
+
const waPlugin = plugins.find((plugin) => plugin.id === "whatsapp");
|
|
3108
|
+
if (waPlugin) {
|
|
3109
|
+
waPlugin.validationErrors = [];
|
|
3110
|
+
waPlugin.configured = true;
|
|
3111
|
+
waPlugin.qrConnected = true;
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
} catch {}
|
|
3115
|
+
}
|
|
3116
|
+
// src/utils/config-detector.ts
|
|
3117
|
+
function detectAuthMethod(config) {
|
|
3118
|
+
const explicitMethod = config.authMethod;
|
|
3119
|
+
if (explicitMethod !== undefined) {
|
|
3120
|
+
if (explicitMethod === "baileys" || explicitMethod === "cloudapi") {
|
|
3121
|
+
return explicitMethod;
|
|
3122
|
+
}
|
|
3123
|
+
throw new Error(`Invalid authMethod: "${String(explicitMethod)}". Must be either "baileys" or "cloudapi".`);
|
|
1491
3124
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
return normalized;
|
|
3125
|
+
if ("authDir" in config && config.authDir) {
|
|
3126
|
+
return "baileys";
|
|
1495
3127
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
3128
|
+
if ("accessToken" in config && "phoneNumberId" in config) {
|
|
3129
|
+
return "cloudapi";
|
|
3130
|
+
}
|
|
3131
|
+
throw new Error("Cannot detect auth method. Provide either authDir (Baileys) or accessToken + phoneNumberId (Cloud API).");
|
|
1499
3132
|
}
|
|
3133
|
+
|
|
3134
|
+
// src/clients/factory.ts
|
|
3135
|
+
var ClientFactory = {
|
|
3136
|
+
create(config) {
|
|
3137
|
+
const authMethod = detectAuthMethod(config);
|
|
3138
|
+
if (authMethod === "baileys") {
|
|
3139
|
+
return new BaileysClient(config);
|
|
3140
|
+
}
|
|
3141
|
+
return new WhatsAppClient(config);
|
|
3142
|
+
}
|
|
3143
|
+
};
|
|
1500
3144
|
// src/types.ts
|
|
1501
3145
|
var WhatsAppEventType;
|
|
1502
3146
|
((WhatsAppEventType2) => {
|
|
@@ -1522,61 +3166,83 @@ var WHATSAPP_REACTIONS = {
|
|
|
1522
3166
|
FIRE: "\uD83D\uDD25",
|
|
1523
3167
|
CELEBRATION: "\uD83C\uDF89"
|
|
1524
3168
|
};
|
|
1525
|
-
|
|
1526
3169
|
// src/index.ts
|
|
1527
|
-
class WhatsAppPlugin extends EventEmitter4 {
|
|
1528
|
-
client;
|
|
1529
|
-
messageHandler;
|
|
1530
|
-
webhookHandler;
|
|
1531
|
-
name;
|
|
1532
|
-
description;
|
|
1533
|
-
actions = [sendMessageAction, sendReactionAction];
|
|
1534
|
-
constructor(config) {
|
|
1535
|
-
super();
|
|
1536
|
-
this.name = "WhatsApp Plugin";
|
|
1537
|
-
this.description = "WhatsApp integration supporting Cloud API and Baileys (QR auth)";
|
|
1538
|
-
this.client = ClientFactory.create(config);
|
|
1539
|
-
this.messageHandler = new MessageHandler(this.client);
|
|
1540
|
-
this.webhookHandler = new WebhookHandler;
|
|
1541
|
-
this.setupEventForwarding();
|
|
1542
|
-
}
|
|
1543
|
-
setupEventForwarding() {
|
|
1544
|
-
this.client.on("message", (payload) => this.emit("message", payload));
|
|
1545
|
-
this.client.on("qr", (payload) => this.emit("qr", payload));
|
|
1546
|
-
this.client.on("ready", () => this.emit("ready"));
|
|
1547
|
-
this.client.on("connection", (status) => this.emit("connection", status));
|
|
1548
|
-
this.client.on("error", (error) => this.emit("error", error));
|
|
1549
|
-
}
|
|
1550
|
-
async start() {
|
|
1551
|
-
await this.client.start();
|
|
1552
|
-
}
|
|
1553
|
-
async stop() {
|
|
1554
|
-
await this.client.stop();
|
|
1555
|
-
}
|
|
1556
|
-
getConnectionStatus() {
|
|
1557
|
-
return this.client.getConnectionStatus();
|
|
1558
|
-
}
|
|
1559
|
-
async sendMessage(message2) {
|
|
1560
|
-
return this.messageHandler.send(message2);
|
|
1561
|
-
}
|
|
1562
|
-
async handleWebhook(event) {
|
|
1563
|
-
return this.webhookHandler.handle(event);
|
|
1564
|
-
}
|
|
1565
|
-
async verifyWebhook(token) {
|
|
1566
|
-
if (!this.client.verifyWebhook) {
|
|
1567
|
-
throw new Error("verifyWebhook is only supported by Cloud API authentication");
|
|
1568
|
-
}
|
|
1569
|
-
return this.client.verifyWebhook(token);
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
3170
|
var whatsappPlugin = {
|
|
1573
3171
|
name: "whatsapp",
|
|
1574
3172
|
description: "WhatsApp integration for ElizaOS (Cloud API + Baileys)",
|
|
1575
|
-
actions: [
|
|
3173
|
+
actions: [],
|
|
3174
|
+
services: [WhatsAppConnectorService, WhatsAppWorkflowCredentialProvider],
|
|
3175
|
+
routes: whatsappSetupRoutes,
|
|
3176
|
+
autoEnable: {
|
|
3177
|
+
connectorKeys: ["whatsapp"]
|
|
3178
|
+
},
|
|
3179
|
+
init: async (_config, runtime) => {
|
|
3180
|
+
try {
|
|
3181
|
+
const manager = getConnectorAccountManager(runtime);
|
|
3182
|
+
manager.registerProvider(createWhatsAppConnectorAccountProvider(runtime));
|
|
3183
|
+
} catch (err) {
|
|
3184
|
+
logger2.warn({
|
|
3185
|
+
src: "plugin:whatsapp",
|
|
3186
|
+
err: err instanceof Error ? err.message : String(err)
|
|
3187
|
+
}, "Failed to register WhatsApp provider with ConnectorAccountManager");
|
|
3188
|
+
}
|
|
3189
|
+
},
|
|
3190
|
+
async dispose(runtime) {
|
|
3191
|
+
const svc = runtime.getService(WhatsAppConnectorService.serviceType);
|
|
3192
|
+
await svc?.stop();
|
|
3193
|
+
}
|
|
1576
3194
|
};
|
|
1577
3195
|
var src_default = whatsappPlugin;
|
|
3196
|
+
var __bundle_safety_PLUGINS_PLUGIN_WHATSAPP_SRC_INDEX__ = [
|
|
3197
|
+
checkWhatsAppUserAccess,
|
|
3198
|
+
DEFAULT_ACCOUNT_ID,
|
|
3199
|
+
isMultiAccountEnabled,
|
|
3200
|
+
isWhatsAppMentionRequired,
|
|
3201
|
+
isWhatsAppUserAllowed,
|
|
3202
|
+
listEnabledWhatsAppAccounts,
|
|
3203
|
+
listWhatsAppAccountIds,
|
|
3204
|
+
normalizeAccountId,
|
|
3205
|
+
resolveDefaultWhatsAppAccountId,
|
|
3206
|
+
resolveWhatsAppAccount,
|
|
3207
|
+
resolveWhatsAppGroupConfig,
|
|
3208
|
+
resolveWhatsAppToken,
|
|
3209
|
+
applyWhatsAppQrOverride,
|
|
3210
|
+
handleWhatsAppRoute,
|
|
3211
|
+
MAX_PAIRING_SESSIONS2,
|
|
3212
|
+
ClientFactory,
|
|
3213
|
+
createWhatsAppConnectorAccountProvider,
|
|
3214
|
+
WHATSAPP_PROVIDER_ID,
|
|
3215
|
+
buildWhatsAppUserJid,
|
|
3216
|
+
chunkWhatsAppText,
|
|
3217
|
+
formatWhatsAppId,
|
|
3218
|
+
formatWhatsAppPhoneNumber,
|
|
3219
|
+
getWhatsAppChatType,
|
|
3220
|
+
isValidWhatsAppNumber,
|
|
3221
|
+
isWhatsAppGroup,
|
|
3222
|
+
isWhatsAppGroupJid,
|
|
3223
|
+
isWhatsAppUserTarget,
|
|
3224
|
+
normalizeE164,
|
|
3225
|
+
normalizeWhatsAppTarget,
|
|
3226
|
+
resolveWhatsAppSystemLocation,
|
|
3227
|
+
truncateText,
|
|
3228
|
+
WHATSAPP_TEXT_CHUNK_LIMIT,
|
|
3229
|
+
sanitizeAccountId,
|
|
3230
|
+
WhatsAppPairingSession,
|
|
3231
|
+
whatsappAuthExists,
|
|
3232
|
+
whatsappLogout,
|
|
3233
|
+
WhatsAppConnectorService,
|
|
3234
|
+
stopAllPairingSessions,
|
|
3235
|
+
whatsappSetupRoutes
|
|
3236
|
+
];
|
|
3237
|
+
var bundleSafetyGlobal = globalThis;
|
|
3238
|
+
bundleSafetyGlobal.__bundle_safety_PLUGINS_PLUGIN_WHATSAPP_SRC_INDEX__ = __bundle_safety_PLUGINS_PLUGIN_WHATSAPP_SRC_INDEX__;
|
|
1578
3239
|
export {
|
|
3240
|
+
whatsappSetupRoutes,
|
|
3241
|
+
whatsappLogout,
|
|
3242
|
+
whatsappAuthExists,
|
|
1579
3243
|
truncateText,
|
|
3244
|
+
stopAllPairingSessions,
|
|
3245
|
+
sanitizeAccountId as sanitizeWhatsAppAccountId,
|
|
1580
3246
|
resolveWhatsAppToken,
|
|
1581
3247
|
resolveWhatsAppSystemLocation,
|
|
1582
3248
|
resolveWhatsAppGroupConfig,
|
|
@@ -1594,20 +3260,26 @@ export {
|
|
|
1594
3260
|
isWhatsAppGroup,
|
|
1595
3261
|
isValidWhatsAppNumber,
|
|
1596
3262
|
isMultiAccountEnabled,
|
|
3263
|
+
handleWhatsAppRoute,
|
|
1597
3264
|
getWhatsAppChatType,
|
|
1598
3265
|
formatWhatsAppPhoneNumber,
|
|
1599
3266
|
formatWhatsAppId,
|
|
1600
3267
|
src_default as default,
|
|
3268
|
+
createWhatsAppConnectorAccountProvider,
|
|
1601
3269
|
chunkWhatsAppText,
|
|
1602
3270
|
checkWhatsAppUserAccess,
|
|
1603
3271
|
buildWhatsAppUserJid,
|
|
1604
|
-
|
|
3272
|
+
applyWhatsAppQrOverride,
|
|
3273
|
+
WhatsAppPairingSession,
|
|
1605
3274
|
WhatsAppEventType,
|
|
3275
|
+
WhatsAppConnectorService,
|
|
1606
3276
|
WHATSAPP_TEXT_CHUNK_LIMIT,
|
|
1607
3277
|
WHATSAPP_REACTIONS,
|
|
3278
|
+
WHATSAPP_PROVIDER_ID,
|
|
3279
|
+
MAX_PAIRING_SESSIONS2 as WHATSAPP_MAX_PAIRING_SESSIONS,
|
|
1608
3280
|
DEFAULT_ACCOUNT_ID,
|
|
1609
3281
|
ClientFactory
|
|
1610
3282
|
};
|
|
1611
3283
|
|
|
1612
|
-
//# debugId=
|
|
3284
|
+
//# debugId=8DCA091A96E61C8E64756E2164756E21
|
|
1613
3285
|
//# sourceMappingURL=index.js.map
|