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