@elizaos/plugin-whatsapp 2.0.0-alpha.8 → 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.
Files changed (54) hide show
  1. package/README.md +283 -0
  2. package/auto-enable.ts +21 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/accounts.d.ts +205 -0
  5. package/dist/accounts.d.ts.map +1 -0
  6. package/dist/actions/index.d.ts +1 -0
  7. package/dist/actions/index.d.ts.map +1 -0
  8. package/dist/api/whatsapp-routes.d.ts +53 -0
  9. package/dist/api/whatsapp-routes.d.ts.map +1 -0
  10. package/dist/baileys/auth.d.ts +10 -0
  11. package/dist/baileys/auth.d.ts.map +1 -0
  12. package/dist/baileys/connection.d.ts +19 -0
  13. package/dist/baileys/connection.d.ts.map +1 -0
  14. package/dist/baileys/message-adapter.d.ts +14 -0
  15. package/dist/baileys/message-adapter.d.ts.map +1 -0
  16. package/dist/baileys/qr-code.d.ts +6 -0
  17. package/dist/baileys/qr-code.d.ts.map +1 -0
  18. package/dist/client.d.ts +99 -0
  19. package/dist/client.d.ts.map +1 -0
  20. package/dist/clients/baileys-client.d.ts +18 -0
  21. package/dist/clients/baileys-client.d.ts.map +1 -0
  22. package/dist/clients/factory.d.ts +6 -0
  23. package/dist/clients/factory.d.ts.map +1 -0
  24. package/dist/clients/interface.d.ts +10 -0
  25. package/dist/clients/interface.d.ts.map +1 -0
  26. package/dist/config.d.ts +135 -0
  27. package/dist/config.d.ts.map +1 -0
  28. package/dist/connector-account-provider.d.ts +19 -0
  29. package/dist/connector-account-provider.d.ts.map +1 -0
  30. package/dist/index.d.ts +14 -2
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +2353 -823
  33. package/dist/index.js.map +20 -18
  34. package/dist/normalize.d.ts +69 -0
  35. package/dist/normalize.d.ts.map +1 -0
  36. package/dist/pairing-service.d.ts +41 -0
  37. package/dist/pairing-service.d.ts.map +1 -0
  38. package/dist/runtime-service.d.ts +116 -0
  39. package/dist/runtime-service.d.ts.map +1 -0
  40. package/dist/services/whatsapp-pairing.d.ts +41 -0
  41. package/dist/services/whatsapp-pairing.d.ts.map +1 -0
  42. package/dist/setup-routes.d.ts +26 -0
  43. package/dist/setup-routes.d.ts.map +1 -0
  44. package/dist/types.d.ts +370 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/dist/utils/config-detector.d.ts +3 -0
  47. package/dist/utils/config-detector.d.ts.map +1 -0
  48. package/dist/utils/index.d.ts +3 -0
  49. package/dist/utils/index.d.ts.map +1 -0
  50. package/dist/utils/validators.d.ts +10 -0
  51. package/dist/utils/validators.d.ts.map +1 -0
  52. package/dist/workflow-credential-provider.d.ts +21 -0
  53. package/dist/workflow-credential-provider.d.ts.map +1 -0
  54. package/package.json +24 -16
package/dist/index.js CHANGED
@@ -1,372 +1,355 @@
1
- // src/index.ts
2
- import { EventEmitter as EventEmitter4 } from "node:events";
3
-
4
- // src/actions/sendMessage.ts
5
- import { composePromptFromState, ModelType, parseJSONObjectFromText } from "@elizaos/core";
6
- var WHATSAPP_SEND_MESSAGE_ACTION = "WHATSAPP_SEND_MESSAGE";
7
- var SEND_MESSAGE_TEMPLATE = `
8
- You are extracting WhatsApp message parameters from a conversation.
9
-
10
- The user wants to send a WhatsApp message. Extract the following:
11
- 1. to: The phone number to send to (E.164 format, e.g., +14155552671)
12
- 2. text: The message text to send
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
13
3
 
14
- {{recentMessages}}
15
-
16
- Based on the conversation, extract the message parameters.
4
+ // src/index.ts
5
+ import { getConnectorAccountManager, logger as logger2 } from "@elizaos/core";
17
6
 
18
- Respond with a JSON object:
19
- {
20
- "to": "+14155552671",
21
- "text": "Hello from WhatsApp!"
22
- }
23
- `;
24
- var sendMessageAction = {
25
- name: WHATSAPP_SEND_MESSAGE_ACTION,
26
- similes: ["SEND_WHATSAPP", "WHATSAPP_MESSAGE", "TEXT_WHATSAPP", "SEND_WHATSAPP_MESSAGE"],
27
- description: "Send a text message via WhatsApp",
28
- validate: async (runtime, message, state, options) => {
29
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
30
- const __avText = __avTextRaw.toLowerCase();
31
- const __avKeywords = ["whatsapp", "send", "message"];
32
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
33
- const __avRegex = new RegExp("\\b(?:whatsapp|send|message)\\b", "i");
34
- const __avRegexOk = __avRegex.test(__avText);
35
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
36
- const __avExpectedSource = "whatsapp";
37
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
38
- const __avOptions = options && typeof options === "object" ? options : {};
39
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
40
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
41
- return false;
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
+ }
42
57
  }
43
- const __avLegacyValidate = async (_runtime, message2) => {
44
- const source = message2.content?.source;
45
- return source === "whatsapp";
46
- };
47
- try {
48
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
49
- } catch {
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") {
50
177
  return false;
51
178
  }
52
- },
53
- handler: async (runtime, message, state, _options, callback) => {
54
- const accessToken = runtime.getSetting("WHATSAPP_ACCESS_TOKEN");
55
- const phoneNumberId = runtime.getSetting("WHATSAPP_PHONE_NUMBER_ID");
56
- const apiVersion = runtime.getSetting("WHATSAPP_API_VERSION") || "v24.0";
57
- if (!accessToken || !phoneNumberId) {
58
- if (callback) {
59
- await callback({
60
- text: "WhatsApp is not configured. Missing access token or phone number ID."
61
- });
62
- }
63
- return { success: false, error: "WhatsApp not configured" };
179
+ if (policy2 === "open") {
180
+ return true;
64
181
  }
65
- const currentState = state ?? await runtime.composeState(message);
66
- const prompt = composePromptFromState({
67
- state: currentState,
68
- template: SEND_MESSAGE_TEMPLATE
69
- });
70
- let params;
71
- try {
72
- const response = await runtime.useModel(ModelType.TEXT_SMALL, {
73
- prompt
74
- });
75
- const parsed = parseJSONObjectFromText(response);
76
- if (!parsed || !parsed.to || !parsed.text) {
77
- const to = message.content?.from;
78
- const text = currentState.values?.response?.toString() || "";
79
- if (!to) {
80
- if (callback) {
81
- await callback({
82
- text: "Could not determine who to send the message to"
83
- });
84
- }
85
- return { success: false, error: "Missing recipient" };
86
- }
87
- if (!text || text.trim() === "") {
88
- if (callback) {
89
- await callback({
90
- text: "Cannot send an empty message. Please provide message content."
91
- });
92
- }
93
- return { success: false, error: "Empty message text" };
94
- }
95
- params = { to, text };
96
- } else {
97
- if (!parsed.text.trim()) {
98
- if (callback) {
99
- await callback({
100
- text: "Cannot send an empty message. Please provide message content."
101
- });
102
- }
103
- return { success: false, error: "Empty message text" };
104
- }
105
- params = parsed;
106
- }
107
- } catch {
108
- if (callback) {
109
- await callback({
110
- text: "Failed to parse message parameters"
111
- });
112
- }
113
- return { success: false, error: "Failed to parse message parameters" };
182
+ if (groupConfig?.allowFrom?.length) {
183
+ return groupConfig.allowFrom.some((allowed) => String(allowed) === identifier);
114
184
  }
115
- try {
116
- const url = `https://graph.facebook.com/${apiVersion}/${phoneNumberId}/messages`;
117
- const response = await fetch(url, {
118
- method: "POST",
119
- headers: {
120
- Authorization: `Bearer ${accessToken}`,
121
- "Content-Type": "application/json"
122
- },
123
- body: JSON.stringify({
124
- messaging_product: "whatsapp",
125
- recipient_type: "individual",
126
- to: params.to,
127
- type: "text",
128
- text: {
129
- preview_url: false,
130
- body: params.text
131
- }
132
- })
133
- });
134
- if (!response.ok) {
135
- const errorData = await response.json();
136
- throw new Error(errorData.error?.message || `HTTP ${response.status}`);
137
- }
138
- const data = await response.json();
139
- const messageId = data.messages?.[0]?.id;
140
- if (callback) {
141
- await callback({
142
- text: `Message sent to ${params.to}`,
143
- action: WHATSAPP_SEND_MESSAGE_ACTION
144
- });
145
- }
146
- return {
147
- success: true,
148
- data: {
149
- action: WHATSAPP_SEND_MESSAGE_ACTION,
150
- to: params.to,
151
- messageId
152
- }
153
- };
154
- } catch (error) {
155
- const errorMessage = error instanceof Error ? error.message : String(error);
156
- if (callback) {
157
- await callback({
158
- text: `Failed to send WhatsApp message: ${errorMessage}`
159
- });
160
- }
161
- return { success: false, error: errorMessage };
185
+ if (accountConfig.groupAllowFrom?.length) {
186
+ return accountConfig.groupAllowFrom.some((allowed) => String(allowed) === identifier);
162
187
  }
163
- },
164
- examples: [
165
- [
166
- {
167
- name: "{{name1}}",
168
- content: {
169
- text: "Send a WhatsApp message to +14155552671 saying hello"
170
- }
171
- },
172
- {
173
- name: "{{agentName}}",
174
- content: {
175
- text: "I'll send that WhatsApp message now.",
176
- actions: [WHATSAPP_SEND_MESSAGE_ACTION]
177
- }
178
- }
179
- ]
180
- ]
181
- };
182
- // src/actions/sendReaction.ts
183
- import { composePromptFromState as composePromptFromState2, ModelType as ModelType2, parseJSONObjectFromText as parseJSONObjectFromText2 } from "@elizaos/core";
184
- var WHATSAPP_SEND_REACTION_ACTION = "WHATSAPP_SEND_REACTION";
185
- var REACTION_TEMPLATE = `
186
- You are extracting WhatsApp reaction parameters from a conversation.
187
-
188
- The user wants to react to a WhatsApp message. Extract the following:
189
- 1. messageId: The ID of the message to react to
190
- 2. emoji: The emoji to use as a reaction
191
-
192
- {{recentMessages}}
193
-
194
- Based on the conversation, extract the reaction parameters.
195
-
196
- Respond with a JSON object:
197
- {
198
- "messageId": "wamid.xxx",
199
- "emoji": "\uD83D\uDC4D"
200
- }
201
- `;
202
- var sendReactionAction = {
203
- name: WHATSAPP_SEND_REACTION_ACTION,
204
- similes: ["WHATSAPP_REACT", "REACT_WHATSAPP", "WHATSAPP_EMOJI"],
205
- description: "Send a reaction emoji to a WhatsApp message",
206
- validate: async (runtime, message, state, options) => {
207
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
208
- const __avText = __avTextRaw.toLowerCase();
209
- const __avKeywords = ["whatsapp", "send", "reaction"];
210
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
211
- const __avRegex = new RegExp("\\b(?:whatsapp|send|reaction)\\b", "i");
212
- const __avRegexOk = __avRegex.test(__avText);
213
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
214
- const __avExpectedSource = "whatsapp";
215
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
216
- const __avOptions = options && typeof options === "object" ? options : {};
217
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
218
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
219
- 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 };
220
215
  }
221
- const __avLegacyValidate = async (_runtime, message2) => {
222
- const source = message2.content?.source;
223
- return source === "whatsapp";
224
- };
225
- try {
226
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
227
- } catch {
228
- return false;
216
+ if (policy2 === "open") {
217
+ return { allowed: true };
229
218
  }
230
- },
231
- handler: async (runtime, message, state, _options, callback) => {
232
- const accessToken = runtime.getSetting("WHATSAPP_ACCESS_TOKEN");
233
- const phoneNumberId = runtime.getSetting("WHATSAPP_PHONE_NUMBER_ID");
234
- const apiVersion = runtime.getSetting("WHATSAPP_API_VERSION") || "v24.0";
235
- if (!accessToken || !phoneNumberId) {
236
- if (callback) {
237
- await callback({
238
- text: "WhatsApp is not configured. Missing access token or phone number ID."
239
- });
240
- }
241
- 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 };
242
222
  }
243
- const currentState = state ?? await runtime.composeState(message);
244
- const prompt = composePromptFromState2({
245
- state: currentState,
246
- template: REACTION_TEMPLATE
247
- });
248
- let params;
249
- try {
250
- const response = await runtime.useModel(ModelType2.TEXT_SMALL, {
251
- prompt
252
- });
253
- const parsed = parseJSONObjectFromText2(response);
254
- if (!parsed || !parsed.messageId || !parsed.emoji) {
255
- const messageId = message.content?.messageId;
256
- if (!messageId) {
257
- if (callback) {
258
- await callback({
259
- text: "Could not determine which message to react to"
260
- });
261
- }
262
- return { success: false, error: "Missing message ID" };
263
- }
264
- params = { messageId, emoji: "\uD83D\uDC4D" };
265
- } else {
266
- params = parsed;
267
- }
268
- } catch {
269
- if (callback) {
270
- await callback({
271
- text: "Failed to parse reaction parameters"
272
- });
273
- }
274
- 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 };
275
253
  }
276
- const to = message.content?.from;
277
- if (!to) {
278
- if (callback) {
279
- await callback({
280
- text: "Could not determine the recipient for the reaction"
281
- });
282
- }
283
- return { success: false, error: "Missing recipient" };
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"
284
295
  }
285
- try {
286
- const url = `https://graph.facebook.com/${apiVersion}/${phoneNumberId}/messages`;
287
- const response = await fetch(url, {
288
- method: "POST",
289
- headers: {
290
- Authorization: `Bearer ${accessToken}`,
291
- "Content-Type": "application/json"
292
- },
293
- body: JSON.stringify({
294
- messaging_product: "whatsapp",
295
- recipient_type: "individual",
296
- to,
297
- type: "reaction",
298
- reaction: {
299
- message_id: params.messageId,
300
- emoji: params.emoji
301
- }
302
- })
303
- });
304
- if (!response.ok) {
305
- const errorData = await response.json();
306
- throw new Error(errorData.error?.message || `HTTP ${response.status}`);
307
- }
308
- if (callback) {
309
- await callback({
310
- text: `Reacted with ${params.emoji}`,
311
- action: WHATSAPP_SEND_REACTION_ACTION
312
- });
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);
313
306
  }
307
+ const fallback = resolveWhatsAppAccount(runtime, DEFAULT_ACCOUNT_ID);
308
+ return [toConnectorAccount(fallback)];
309
+ },
310
+ createAccount: async (input, _manager) => {
314
311
  return {
315
- success: true,
316
- data: {
317
- action: WHATSAPP_SEND_REACTION_ACTION,
318
- messageId: params.messageId,
319
- emoji: params.emoji
320
- }
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"
321
318
  };
322
- } catch (error) {
323
- const errorMessage = error instanceof Error ? error.message : String(error);
324
- if (callback) {
325
- await callback({
326
- text: `Failed to send reaction: ${errorMessage}`
327
- });
328
- }
329
- return { success: false, error: errorMessage };
330
- }
331
- },
332
- examples: [
333
- [
334
- {
335
- name: "{{name1}}",
336
- content: {
337
- text: "React with a thumbs up"
338
- }
339
- },
340
- {
341
- name: "{{agentName}}",
342
- content: {
343
- text: "I'll add that reaction.",
344
- actions: [WHATSAPP_SEND_REACTION_ACTION]
345
- }
346
- }
347
- ]
348
- ]
349
- };
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
+
350
335
  // src/client.ts
351
- import axios from "axios";
352
336
  import { EventEmitter } from "node:events";
353
337
  var DEFAULT_API_VERSION = "v24.0";
354
338
 
355
339
  class WhatsAppClient extends EventEmitter {
356
- client;
340
+ baseUrl;
341
+ headers;
357
342
  config;
358
343
  connectionStatus = "close";
359
344
  constructor(config) {
360
345
  super();
361
346
  this.config = config;
362
347
  const apiVersion = config.apiVersion || DEFAULT_API_VERSION;
363
- this.client = axios.create({
364
- baseURL: `https://graph.facebook.com/${apiVersion}`,
365
- headers: {
366
- Authorization: `Bearer ${config.accessToken}`,
367
- "Content-Type": "application/json"
368
- }
369
- });
348
+ this.baseUrl = `https://graph.facebook.com/${apiVersion}`;
349
+ this.headers = {
350
+ Authorization: `Bearer ${config.accessToken}`,
351
+ "Content-Type": "application/json"
352
+ };
370
353
  }
371
354
  async start() {
372
355
  this.connectionStatus = "open";
@@ -386,7 +369,7 @@ class WhatsAppClient extends EventEmitter {
386
369
  async sendMessage(message) {
387
370
  const endpoint = `/${this.config.phoneNumberId}/messages`;
388
371
  const payload = this.buildMessagePayload(message);
389
- return this.client.post(endpoint, payload);
372
+ return this.post(endpoint, payload);
390
373
  }
391
374
  async sendTextMessage(to, text, _previewUrl = false) {
392
375
  return this.sendMessage({
@@ -408,7 +391,7 @@ class WhatsAppClient extends EventEmitter {
408
391
  }
409
392
  };
410
393
  try {
411
- const response = await this.client.post(endpoint, payload);
394
+ const response = await this.post(endpoint, payload);
412
395
  return {
413
396
  success: true,
414
397
  messageId: response.data.messages?.[0]?.id
@@ -532,7 +515,7 @@ class WhatsAppClient extends EventEmitter {
532
515
  message_id: messageId
533
516
  };
534
517
  try {
535
- await this.client.post(endpoint, payload);
518
+ await this.post(endpoint, payload);
536
519
  return true;
537
520
  } catch {
538
521
  return false;
@@ -540,7 +523,7 @@ class WhatsAppClient extends EventEmitter {
540
523
  }
541
524
  async getMediaUrl(mediaId) {
542
525
  try {
543
- const response = await this.client.get(`/${mediaId}`);
526
+ const response = await this.get(`/${mediaId}`);
544
527
  return response.data.url || null;
545
528
  } catch {
546
529
  return null;
@@ -549,6 +532,44 @@ class WhatsAppClient extends EventEmitter {
549
532
  async verifyWebhook(token) {
550
533
  return token === this.config.webhookVerifyToken;
551
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
+ }
552
573
  buildMessagePayload(message) {
553
574
  const basePayload = {
554
575
  messaging_product: "whatsapp",
@@ -653,24 +674,6 @@ class WhatsAppClient extends EventEmitter {
653
674
  }
654
675
  }
655
676
 
656
- // src/utils/config-detector.ts
657
- function detectAuthMethod(config) {
658
- const explicitMethod = config.authMethod;
659
- if (explicitMethod !== undefined) {
660
- if (explicitMethod === "baileys" || explicitMethod === "cloudapi") {
661
- return explicitMethod;
662
- }
663
- throw new Error(`Invalid authMethod: "${String(explicitMethod)}". Must be either "baileys" or "cloudapi".`);
664
- }
665
- if ("authDir" in config && config.authDir) {
666
- return "baileys";
667
- }
668
- if ("accessToken" in config && "phoneNumberId" in config) {
669
- return "cloudapi";
670
- }
671
- throw new Error("Cannot detect auth method. Provide either authDir (Baileys) or accessToken + phoneNumberId (Cloud API).");
672
- }
673
-
674
677
  // src/clients/baileys-client.ts
675
678
  import { EventEmitter as EventEmitter3 } from "node:events";
676
679
 
@@ -698,8 +701,8 @@ class BaileysAuthManager {
698
701
  }
699
702
 
700
703
  // src/baileys/connection.ts
701
- import makeWASocket, { DisconnectReason } from "@whiskeysockets/baileys";
702
704
  import { EventEmitter as EventEmitter2 } from "node:events";
705
+ import makeWASocket, { DisconnectReason } from "@whiskeysockets/baileys";
703
706
  import pino from "pino";
704
707
 
705
708
  class BaileysConnection extends EventEmitter2 {
@@ -766,7 +769,7 @@ class BaileysConnection extends EventEmitter2 {
766
769
  try {
767
770
  this.reconnectAttempts += 1;
768
771
  const baseDelayMs = isQRTimeout ? 1000 : 3000;
769
- const backoffMs = Math.min(baseDelayMs * Math.pow(2, this.reconnectAttempts - 1), 30000);
772
+ const backoffMs = Math.min(baseDelayMs * 2 ** (this.reconnectAttempts - 1), 30000);
770
773
  await new Promise((resolve) => setTimeout(resolve, backoffMs));
771
774
  await this.connect();
772
775
  } catch (error) {
@@ -792,8 +795,9 @@ class BaileysConnection extends EventEmitter2 {
792
795
  if (!this.socket) {
793
796
  return;
794
797
  }
795
- this.socket.ev.removeAllListeners();
796
- this.socket.ws?.close?.();
798
+ const socket = this.socket;
799
+ socket.ev.removeAllListeners();
800
+ socket.ws?.close?.();
797
801
  this.socket = undefined;
798
802
  this.connectionStatus = "close";
799
803
  this.emit("connection", "close");
@@ -802,13 +806,18 @@ class BaileysConnection extends EventEmitter2 {
802
806
 
803
807
  // src/baileys/message-adapter.ts
804
808
  class MessageAdapter {
805
- toUnified(msg) {
809
+ toNormalized(msg) {
810
+ const chatId = msg.key?.remoteJid ?? "";
811
+ const senderId = msg.key?.participant ?? chatId;
806
812
  return {
807
813
  id: msg.key?.id ?? "",
808
- from: msg.key?.remoteJid ?? "",
814
+ from: chatId,
809
815
  timestamp: Number(msg.messageTimestamp ?? 0),
810
816
  type: this.detectType(msg),
811
- content: this.extractContent(msg)
817
+ content: this.extractContent(msg),
818
+ chatId,
819
+ senderId,
820
+ replyToId: this.extractReplyToId(msg)
812
821
  };
813
822
  }
814
823
  toBaileys(msg) {
@@ -873,7 +882,11 @@ class MessageAdapter {
873
882
  return "text";
874
883
  }
875
884
  extractContent(msg) {
876
- 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;
877
890
  }
878
891
  renderTemplate(template) {
879
892
  const params = template.components?.flatMap((component) => component.parameters.map((parameter) => parameter.text).filter(Boolean));
@@ -883,7 +896,7 @@ class MessageAdapter {
883
896
 
884
897
  // src/baileys/qr-code.ts
885
898
  import QRCode from "qrcode";
886
- import QRCodeTerminal from "qrcode-terminal";
899
+ import * as QRCodeTerminal from "qrcode-terminal";
887
900
 
888
901
  class QRCodeGenerator {
889
902
  async generate(qrString) {
@@ -943,7 +956,7 @@ class BaileysClient extends EventEmitter3 {
943
956
  for (const message of messages) {
944
957
  const maybe = message;
945
958
  if (!maybe.key?.fromMe && maybe.message) {
946
- this.emit("message", this.adapter.toUnified(message));
959
+ this.emit("message", this.adapter.toNormalized(message));
947
960
  }
948
961
  }
949
962
  });
@@ -971,514 +984,2008 @@ class BaileysClient extends EventEmitter3 {
971
984
  messages: [{ id }]
972
985
  };
973
986
  }
974
- getConnectionStatus() {
975
- 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;
976
1158
  }
1159
+ if (maxLength <= 3) {
1160
+ return "...".slice(0, maxLength);
1161
+ }
1162
+ return `${text.slice(0, maxLength - 3)}...`;
977
1163
  }
978
-
979
- // src/clients/factory.ts
980
- class ClientFactory {
981
- static create(config) {
982
- const authMethod = detectAuthMethod(config);
983
- if (authMethod === "baileys") {
984
- return new BaileysClient(config);
985
- }
986
- return new WhatsAppClient(config);
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;
987
1173
  }
1174
+ if (!normalized.startsWith("+")) {
1175
+ return false;
1176
+ }
1177
+ const digits = normalized.replace(/^\+/, "");
1178
+ return /^\d{10,15}$/.test(digits);
988
1179
  }
989
-
990
- // src/handlers/message.handler.ts
991
- class MessageHandler {
992
- client;
993
- constructor(client) {
994
- this.client = client;
1180
+ function formatWhatsAppPhoneNumber(phoneNumber) {
1181
+ const normalized = normalizeE164(phoneNumber);
1182
+ if (!normalized) {
1183
+ return phoneNumber;
995
1184
  }
996
- async send(message) {
997
- try {
998
- const response = await this.client.sendMessage(message);
999
- if (response && typeof response === "object" && "data" in response) {
1000
- return response.data;
1001
- }
1002
- return response;
1003
- } catch (error) {
1004
- if (error instanceof Error) {
1005
- throw new Error(`Failed to send WhatsApp message: ${error.message}`);
1006
- }
1007
- throw new Error("Failed to send WhatsApp message");
1008
- }
1185
+ const digits = normalized.replace(/^\+/, "");
1186
+ if (digits.length <= 10) {
1187
+ return normalized;
1009
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)}`;
1010
1192
  }
1011
- // src/handlers/webhook.handler.ts
1012
- class WebhookHandler {
1013
- async handle(event) {
1014
- try {
1015
- if (event.entry?.[0]?.changes?.[0]?.value?.messages) {
1016
- const messages = event.entry[0].changes[0].value.messages;
1017
- for (const message of messages) {
1018
- await this.handleMessage(message);
1019
- }
1020
- }
1021
- if (event.entry?.[0]?.changes?.[0]?.value?.statuses) {
1022
- const statuses = event.entry[0].changes[0].value.statuses;
1023
- for (const status of statuses) {
1024
- await this.handleStatus(status);
1025
- }
1026
- }
1027
- } catch (error) {
1028
- if (error instanceof Error) {
1029
- throw new Error(`Failed to send WhatsApp message: ${error.message}`);
1030
- }
1031
- throw new Error("Failed to send WhatsApp message");
1032
- }
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();
1033
1199
  }
1034
- async handleMessage(message) {
1035
- console.log("Received message:", message);
1200
+ const envValue = process.env[key];
1201
+ if (typeof envValue === "string" && envValue.trim().length > 0) {
1202
+ return envValue.trim();
1036
1203
  }
1037
- async handleStatus(status) {
1038
- console.log("Received status update:", status);
1204
+ return;
1205
+ }
1206
+ function readCsvSetting(runtime, key) {
1207
+ const value = readStringSetting(runtime, key);
1208
+ if (!value) {
1209
+ return [];
1039
1210
  }
1211
+ return value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
1040
1212
  }
1041
- // src/accounts.ts
1042
- import { checkPairingAllowed, isInAllowlist } from "@elizaos/core";
1043
- var DEFAULT_ACCOUNT_ID = "default";
1044
- function normalizeAccountId(accountId) {
1045
- if (!accountId || typeof accountId !== "string") {
1046
- return DEFAULT_ACCOUNT_ID;
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
+ };
1047
1229
  }
1048
- const trimmed = accountId.trim().toLowerCase();
1049
- if (!trimmed || trimmed === "default") {
1050
- return DEFAULT_ACCOUNT_ID;
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
+ };
1051
1245
  }
1052
- return trimmed;
1246
+ return null;
1053
1247
  }
1054
- function getMultiAccountConfig(runtime) {
1055
- const characterWhatsApp = runtime.character?.settings?.whatsapp;
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] ?? {};
1056
1251
  return {
1057
- enabled: characterWhatsApp?.enabled,
1058
- accessToken: characterWhatsApp?.accessToken,
1059
- phoneNumberId: characterWhatsApp?.phoneNumberId,
1060
- businessAccountId: characterWhatsApp?.businessAccountId,
1061
- webhookVerifyToken: characterWhatsApp?.webhookVerifyToken,
1062
- apiVersion: characterWhatsApp?.apiVersion,
1063
- dmPolicy: characterWhatsApp?.dmPolicy,
1064
- groupPolicy: characterWhatsApp?.groupPolicy,
1065
- mediaMaxMb: characterWhatsApp?.mediaMaxMb,
1066
- textChunkLimit: characterWhatsApp?.textChunkLimit,
1067
- accounts: characterWhatsApp?.accounts,
1068
- groups: characterWhatsApp?.groups
1252
+ ...config,
1253
+ accounts: undefined,
1254
+ groups: undefined,
1255
+ ...accountConfig
1069
1256
  };
1070
1257
  }
1071
- function listWhatsAppAccountIds(runtime) {
1072
- const config = getMultiAccountConfig(runtime);
1073
- const accounts = config.accounts;
1074
- const ids = new Set;
1075
- const envToken = runtime.getSetting("WHATSAPP_ACCESS_TOKEN");
1076
- const envPhoneId = runtime.getSetting("WHATSAPP_PHONE_NUMBER_ID");
1077
- const baseConfigured = Boolean(config.accessToken?.trim() && config.phoneNumberId?.trim());
1078
- const envConfigured = Boolean(envToken?.trim() && envPhoneId?.trim());
1079
- if (baseConfigured || envConfigured) {
1080
- ids.add(DEFAULT_ACCOUNT_ID);
1081
- }
1082
- if (accounts && typeof accounts === "object") {
1083
- for (const id of Object.keys(accounts)) {
1084
- if (id) {
1085
- ids.add(normalizeAccountId(id));
1086
- }
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
+ });
1087
1296
  }
1088
1297
  }
1089
- const result = Array.from(ids);
1090
- if (result.length === 0) {
1091
- return [DEFAULT_ACCOUNT_ID];
1298
+ if (configs.length > 0) {
1299
+ return configs;
1092
1300
  }
1093
- return result.slice().sort((a, b) => a.localeCompare(b));
1301
+ const legacy = resolveRuntimeConfig(runtime);
1302
+ return legacy ? [legacy] : [];
1094
1303
  }
1095
- function resolveDefaultWhatsAppAccountId(runtime) {
1096
- const ids = listWhatsAppAccountIds(runtime);
1097
- if (ids.includes(DEFAULT_ACCOUNT_ID)) {
1098
- return DEFAULT_ACCOUNT_ID;
1304
+ function toTimestampMs(value) {
1305
+ const parsed = Number(value);
1306
+ if (!Number.isFinite(parsed) || parsed <= 0) {
1307
+ return Date.now();
1099
1308
  }
1100
- return ids[0] ?? DEFAULT_ACCOUNT_ID;
1309
+ return parsed >= 1000000000000 ? parsed : parsed * 1000;
1101
1310
  }
1102
- function getAccountConfig(runtime, accountId) {
1103
- const config = getMultiAccountConfig(runtime);
1104
- const accounts = config.accounts;
1105
- if (!accounts || typeof accounts !== "object") {
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);
1106
1327
  return;
1107
1328
  }
1108
- const direct = accounts[accountId];
1109
- if (direct) {
1110
- return direct;
1329
+ if (registration.sendHandler) {
1330
+ runtime.registerSendHandler(registration.source, registration.sendHandler);
1111
1331
  }
1112
- const normalized = normalizeAccountId(accountId);
1113
- const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized);
1114
- return matchKey ? accounts[matchKey] : undefined;
1115
1332
  }
1116
- function resolveWhatsAppToken(runtime, accountId) {
1117
- const multiConfig = getMultiAccountConfig(runtime);
1118
- const accountConfig = getAccountConfig(runtime, accountId);
1119
- if (accountConfig?.accessToken?.trim()) {
1120
- return { token: accountConfig.accessToken.trim(), source: "config" };
1333
+ function normalizeBaileysSendTarget(target) {
1334
+ if (isWhatsAppGroupJid(target) || isWhatsAppUserTarget(target)) {
1335
+ return target;
1121
1336
  }
1122
- if (accountId === DEFAULT_ACCOUNT_ID) {
1123
- if (multiConfig.accessToken?.trim()) {
1124
- return { token: multiConfig.accessToken.trim(), source: "config" };
1125
- }
1126
- const envToken = runtime.getSetting("WHATSAPP_ACCESS_TOKEN");
1127
- if (envToken?.trim()) {
1128
- return { token: envToken.trim(), source: "env" };
1129
- }
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;
1130
1346
  }
1131
- return { token: "", source: "none" };
1347
+ return normalizeWhatsAppTarget(trimmed) ?? trimmed;
1132
1348
  }
1133
- function filterDefined(obj) {
1134
- return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
1349
+ function isWhatsAppAddress(value) {
1350
+ return isWhatsAppGroupJid(value) || isWhatsAppUserTarget(value) || normalizeWhatsAppTarget(value) !== null;
1135
1351
  }
1136
- function mergeWhatsAppAccountConfig(runtime, accountId) {
1137
- const multiConfig = getMultiAccountConfig(runtime);
1138
- const { accounts: _ignored, ...baseConfig } = multiConfig;
1139
- const accountConfig = getAccountConfig(runtime, accountId) ?? {};
1140
- const envToken = runtime.getSetting("WHATSAPP_ACCESS_TOKEN");
1141
- const envPhoneId = runtime.getSetting("WHATSAPP_PHONE_NUMBER_ID");
1142
- const envBusinessId = runtime.getSetting("WHATSAPP_BUSINESS_ACCOUNT_ID");
1143
- const envWebhookToken = runtime.getSetting("WHATSAPP_WEBHOOK_VERIFY_TOKEN");
1144
- const envDmPolicy = runtime.getSetting("WHATSAPP_DM_POLICY");
1145
- const envGroupPolicy = runtime.getSetting("WHATSAPP_GROUP_POLICY");
1146
- const envConfig = {
1147
- accessToken: envToken || undefined,
1148
- phoneNumberId: envPhoneId || undefined,
1149
- businessAccountId: envBusinessId || undefined,
1150
- webhookVerifyToken: envWebhookToken || undefined,
1151
- dmPolicy: envDmPolicy,
1152
- groupPolicy: envGroupPolicy
1153
- };
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;
1154
1374
  return {
1155
- ...filterDefined(envConfig),
1156
- ...filterDefined(baseConfig),
1157
- ...filterDefined(accountConfig)
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
+ }
1158
1391
  };
1159
1392
  }
1160
- function resolveWhatsAppAccount(runtime, accountId) {
1161
- const normalizedAccountId = normalizeAccountId(accountId);
1162
- const multiConfig = getMultiAccountConfig(runtime);
1163
- const baseEnabled = multiConfig.enabled !== false;
1164
- const merged = mergeWhatsAppAccountConfig(runtime, normalizedAccountId);
1165
- const accountEnabled = merged.enabled !== false;
1166
- const enabled = baseEnabled && accountEnabled;
1167
- const { token, source: tokenSource } = resolveWhatsAppToken(runtime, normalizedAccountId);
1168
- const phoneNumberId = merged.phoneNumberId?.trim() || "";
1169
- 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;
1170
1397
  return {
1171
- accountId: normalizedAccountId,
1172
- enabled,
1173
- name: merged.name?.trim() || undefined,
1174
- accessToken: token,
1175
- phoneNumberId,
1176
- businessAccountId: merged.businessAccountId?.trim() || undefined,
1177
- tokenSource,
1178
- configured,
1179
- config: merged
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
+ }
1180
1410
  };
1181
1411
  }
1182
- function listEnabledWhatsAppAccounts(runtime) {
1183
- return listWhatsAppAccountIds(runtime).map((accountId) => resolveWhatsAppAccount(runtime, accountId)).filter((account) => account.enabled && account.configured);
1184
- }
1185
- function isMultiAccountEnabled(runtime) {
1186
- const accounts = listEnabledWhatsAppAccounts(runtime);
1187
- return accounts.length > 1;
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;
1188
1442
  }
1189
- function resolveWhatsAppGroupConfig(runtime, accountId, groupId) {
1190
- const multiConfig = getMultiAccountConfig(runtime);
1191
- const accountConfig = getAccountConfig(runtime, accountId);
1192
- const accountGroup = accountConfig?.groups?.[groupId];
1193
- if (accountGroup) {
1194
- return accountGroup;
1443
+ function extractWebhookText(message) {
1444
+ if (typeof message.text?.body === "string" && message.text.body.trim()) {
1445
+ return message.text.body.trim();
1195
1446
  }
1196
- return multiConfig.groups?.[groupId];
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 "";
1197
1473
  }
1198
- function isWhatsAppUserAllowed(params) {
1199
- const { identifier, accountConfig, isGroup, groupConfig } = params;
1200
- if (isGroup) {
1201
- const policy2 = accountConfig.groupPolicy ?? "allowlist";
1202
- if (policy2 === "disabled") {
1203
- return false;
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;
1204
1491
  }
1205
- if (policy2 === "open") {
1206
- return true;
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();
1207
1686
  }
1208
- if (groupConfig?.allowFrom?.length) {
1209
- return groupConfig.allowFrom.some((allowed) => String(allowed) === identifier);
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
+ }
1210
1710
  }
1211
- if (accountConfig.groupAllowFrom?.length) {
1212
- return accountConfig.groupAllowFrom.some((allowed) => String(allowed) === identifier);
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;
1213
1719
  }
1214
- return policy2 !== "allowlist";
1720
+ return null;
1215
1721
  }
1216
- const policy = accountConfig.dmPolicy ?? "pairing";
1217
- if (policy === "disabled") {
1218
- return false;
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;
1219
1732
  }
1220
- if (policy === "open") {
1221
- return true;
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
+ });
1222
1787
  }
1223
- if (policy === "pairing") {
1224
- return true;
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
+ });
1225
1804
  }
1226
- if (accountConfig.allowFrom?.length) {
1227
- return accountConfig.allowFrom.some((allowed) => String(allowed) === identifier);
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
+ });
1228
1820
  }
1229
- return false;
1230
- }
1231
- function isWhatsAppMentionRequired(params) {
1232
- const { groupConfig } = params;
1233
- return groupConfig?.requireMention ?? false;
1234
- }
1235
- async function checkWhatsAppUserAccess(params) {
1236
- const { runtime, identifier, accountConfig, isGroup, groupConfig, metadata } = params;
1237
- if (isGroup) {
1238
- const policy2 = accountConfig.groupPolicy ?? "allowlist";
1239
- if (policy2 === "disabled") {
1240
- return { allowed: false };
1821
+ async processIncomingMessage(params) {
1822
+ if (!this.runtime.messageService) {
1823
+ throw new Error("WhatsApp connector requires runtime.messageService");
1241
1824
  }
1242
- if (policy2 === "open") {
1243
- return { allowed: true };
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;
1244
1848
  }
1245
- if (groupConfig?.allowFrom?.length) {
1246
- const allowed = groupConfig.allowFrom.some((a) => String(a) === identifier);
1247
- return { allowed };
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
+ });
1248
1892
  }
1249
- if (accountConfig.groupAllowFrom?.length) {
1250
- const allowed = accountConfig.groupAllowFrom.some((a) => String(a) === identifier);
1251
- return { allowed };
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;
1252
1997
  }
1253
- return { allowed: policy2 !== "allowlist" };
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;
1254
2014
  }
1255
- const policy = accountConfig.dmPolicy ?? "pairing";
1256
- if (policy === "disabled") {
1257
- return { allowed: false };
2015
+ async sendMessage(message) {
2016
+ return this.sendTextMessage(message.to, message.content, message.replyToMessageId, message.accountId);
1258
2017
  }
1259
- if (policy === "open") {
1260
- return { allowed: true };
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);
1261
2079
  }
1262
- if (policy === "pairing") {
1263
- const result = await checkPairingAllowed(runtime, {
1264
- channel: "whatsapp",
1265
- senderId: identifier,
1266
- metadata
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
+ }
1267
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
+ }
1268
2130
  return {
1269
- allowed: result.allowed,
1270
- pairingCode: result.pairingCode,
1271
- newPairingRequest: result.newRequest,
1272
- replyMessage: result.replyMessage
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
+ }
1273
2144
  };
1274
2145
  }
1275
- if (accountConfig.allowFrom?.length) {
1276
- const allowed = accountConfig.allowFrom.some((a) => String(a) === identifier);
1277
- if (allowed) {
1278
- return { allowed: true };
1279
- }
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);
1280
2149
  }
1281
- const inDynamicAllowlist = await isInAllowlist(runtime, "whatsapp", identifier);
1282
- return { allowed: inDynamicAllowlist };
1283
- }
1284
- // src/normalize.ts
1285
- var WHATSAPP_TEXT_CHUNK_LIMIT = 4096;
1286
- var WHATSAPP_USER_JID_RE = /^(\d+)(?::\d+)?@s\.whatsapp\.net$/i;
1287
- var WHATSAPP_LID_RE = /^(\d+)@lid$/i;
1288
- function stripWhatsAppTargetPrefixes(value) {
1289
- let candidate = value.trim();
1290
- for (;; ) {
1291
- const before = candidate;
1292
- candidate = candidate.replace(/^whatsapp:/i, "").trim();
1293
- if (candidate === before) {
1294
- return candidate;
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
+ }
1295
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
+ });
1296
2175
  }
1297
2176
  }
1298
- function normalizeE164(input) {
1299
- const stripped = input.replace(/[\s\-().]+/g, "");
1300
- const digitsOnly = stripped.replace(/[^\d+]/g, "");
1301
- if (!digitsOnly) {
1302
- return "";
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;
1303
2203
  }
1304
- if (digitsOnly.startsWith("+")) {
1305
- return digitsOnly;
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
+ });
1306
2283
  }
1307
- if (digitsOnly.startsWith("00")) {
1308
- return `+${digitsOnly.slice(2)}`;
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;
1309
2293
  }
1310
- if (digitsOnly.length >= 10) {
1311
- return `+${digitsOnly}`;
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
+ });
1312
2304
  }
1313
- return digitsOnly;
1314
2305
  }
1315
- function isWhatsAppGroupJid(value) {
1316
- const candidate = stripWhatsAppTargetPrefixes(value);
1317
- const lower = candidate.toLowerCase();
1318
- if (!lower.endsWith("@g.us")) {
1319
- return false;
1320
- }
1321
- const localPart = candidate.slice(0, candidate.length - "@g.us".length);
1322
- if (!localPart || localPart.includes("@")) {
1323
- return false;
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 {}
1324
2356
  }
1325
- return /^[0-9]+(-[0-9]+)*$/.test(localPart);
2357
+ fs.rmSync(authDir, { recursive: true, force: true });
1326
2358
  }
1327
- function isWhatsAppUserTarget(value) {
1328
- const candidate = stripWhatsAppTargetPrefixes(value);
1329
- return WHATSAPP_USER_JID_RE.test(candidate) || WHATSAPP_LID_RE.test(candidate);
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";
1330
2366
  }
1331
- function extractUserJidPhone(jid) {
1332
- const userMatch = jid.match(WHATSAPP_USER_JID_RE);
1333
- if (userMatch) {
1334
- return userMatch[1];
1335
- }
1336
- const lidMatch = jid.match(WHATSAPP_LID_RE);
1337
- if (lidMatch) {
1338
- return lidMatch[1];
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
+ }
1339
2381
  }
1340
- return null;
1341
2382
  }
1342
- function normalizeWhatsAppTarget(value) {
1343
- const candidate = stripWhatsAppTargetPrefixes(value);
1344
- if (!candidate) {
1345
- return null;
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;
1346
2393
  }
1347
- if (isWhatsAppGroupJid(candidate)) {
1348
- const localPart = candidate.slice(0, candidate.length - "@g.us".length);
1349
- return `${localPart}@g.us`;
2394
+ const verifiedChallenge = service.verifyWebhook(mode, token, challenge, accountId);
2395
+ if (!verifiedChallenge) {
2396
+ res.status(403).json({ error: "Webhook verification failed" });
2397
+ return;
1350
2398
  }
1351
- if (isWhatsAppUserTarget(candidate)) {
1352
- const phone = extractUserJidPhone(candidate);
1353
- if (!phone) {
1354
- return null;
1355
- }
1356
- const normalized2 = normalizeE164(phone);
1357
- return normalized2.length > 1 ? normalized2 : null;
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;
1358
2406
  }
1359
- if (candidate.includes("@")) {
1360
- return null;
2407
+ const body = req.body;
2408
+ if (!body) {
2409
+ res.status(400).json({ error: "Missing request body" });
2410
+ return;
1361
2411
  }
1362
- const normalized = normalizeE164(candidate);
1363
- return normalized.length > 1 ? normalized : null;
2412
+ await service.handleWebhook(body);
2413
+ res.status(200).json("EVENT_RECEIVED");
1364
2414
  }
1365
- function formatWhatsAppId(id) {
1366
- if (isWhatsAppGroupJid(id)) {
1367
- return `group:${id}`;
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) });
1368
2485
  }
1369
- const normalized = normalizeWhatsAppTarget(id);
1370
- return normalized || id;
1371
- }
1372
- function isWhatsAppGroup(id) {
1373
- return isWhatsAppGroupJid(id);
1374
2486
  }
1375
- function getWhatsAppChatType(id) {
1376
- return isWhatsAppGroupJid(id) ? "group" : "user";
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
+ });
1377
2517
  }
1378
- function buildWhatsAppUserJid(phoneNumber) {
1379
- const normalized = normalizeE164(phoneNumber);
1380
- const digits = normalized.replace(/^\+/, "");
1381
- return `${digits}@s.whatsapp.net`;
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" });
1382
2533
  }
1383
- function splitAtBreakPoint(text, limit) {
1384
- if (text.length <= limit) {
1385
- return { chunk: text, remainder: "" };
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;
1386
2543
  }
1387
- const searchArea = text.slice(0, limit);
1388
- const doubleNewline = searchArea.lastIndexOf(`
1389
-
1390
- `);
1391
- if (doubleNewline > limit * 0.5) {
1392
- return {
1393
- chunk: text.slice(0, doubleNewline).trimEnd(),
1394
- remainder: text.slice(doubleNewline + 2).trimStart()
1395
- };
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
+ });
1396
2578
  }
1397
- const singleNewline = searchArea.lastIndexOf(`
1398
- `);
1399
- if (singleNewline > limit * 0.5) {
1400
- return {
1401
- chunk: text.slice(0, singleNewline).trimEnd(),
1402
- remainder: text.slice(singleNewline + 1).trimStart()
1403
- };
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 {}
1404
2628
  }
1405
- const sentenceEnd = Math.max(searchArea.lastIndexOf(". "), searchArea.lastIndexOf("! "), searchArea.lastIndexOf("? "));
1406
- if (sentenceEnd > limit * 0.5) {
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;
1407
2651
  return {
1408
- chunk: text.slice(0, sentenceEnd + 1).trimEnd(),
1409
- remainder: text.slice(sentenceEnd + 2).trimStart()
2652
+ status: "credential_data",
2653
+ data: { accessToken: accessToken.trim(), phoneNumberId: phoneNumberId.trim() }
1410
2654
  };
1411
2655
  }
1412
- const space = searchArea.lastIndexOf(" ");
1413
- if (space > limit * 0.5) {
2656
+ checkCredentialTypes(credTypes) {
1414
2657
  return {
1415
- chunk: text.slice(0, space).trimEnd(),
1416
- remainder: text.slice(space + 1).trimStart()
2658
+ supported: credTypes.filter((t) => SUPPORTED.includes(t)),
2659
+ unsupported: credTypes.filter((t) => !SUPPORTED.includes(t))
1417
2660
  };
1418
2661
  }
1419
- return {
1420
- chunk: text.slice(0, limit),
1421
- remainder: text.slice(limit)
1422
- };
1423
2662
  }
1424
- function chunkWhatsAppText(text, opts = {}) {
1425
- const limit = opts.limit ?? WHATSAPP_TEXT_CHUNK_LIMIT;
1426
- if (!text?.trim()) {
1427
- return [];
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;
1428
2686
  }
1429
- const normalizedText = text.trim();
1430
- if (normalizedText.length <= limit) {
1431
- return [normalizedText];
2687
+ if (!body.trim()) {
2688
+ return {};
1432
2689
  }
1433
- const chunks = [];
1434
- let remaining = normalizedText;
1435
- while (remaining.length > 0) {
1436
- const { chunk, remainder } = splitAtBreakPoint(remaining, limit);
1437
- if (chunk) {
1438
- chunks.push(chunk);
1439
- }
1440
- remaining = remainder;
2690
+ try {
2691
+ return JSON.parse(body);
2692
+ } catch {
2693
+ json(res, { error: "Invalid JSON body" }, 400);
2694
+ return null;
1441
2695
  }
1442
- return chunks.filter((c) => c.length > 0);
1443
2696
  }
1444
- function truncateText(text, maxLength) {
1445
- if (text.length <= maxLength) {
1446
- return text;
2697
+ function json(res, data, status = 200) {
2698
+ if (!res.headersSent) {
2699
+ res.statusCode = status;
2700
+ res.setHeader("Content-Type", "application/json");
1447
2701
  }
1448
- if (maxLength <= 3) {
1449
- return "...".slice(0, maxLength);
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;
1450
2725
  }
1451
- return `${text.slice(0, maxLength - 3)}...`;
2726
+ config.agents.defaults.ownerContacts[update.source] = entry;
2727
+ return true;
1452
2728
  }
1453
- function resolveWhatsAppSystemLocation(params) {
1454
- const { chatType, chatId, chatName } = params;
1455
- const name = chatName || chatId.slice(0, 8);
1456
- return `WhatsApp ${chatType}:${name}`;
2729
+ function shouldConfigurePlugin(body) {
2730
+ return body?.configurePlugin !== false;
1457
2731
  }
1458
- function isValidWhatsAppNumber(value) {
1459
- const normalized = normalizeWhatsAppTarget(value);
1460
- if (!normalized) {
1461
- return false;
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);
1462
2744
  }
1463
- if (!normalized.startsWith("+")) {
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"))
1464
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;
1465
2769
  }
1466
- const digits = normalized.replace(/^\+/, "");
1467
- return /^\d{10,15}$/.test(digits);
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;
1468
2947
  }
1469
- function formatWhatsAppPhoneNumber(phoneNumber) {
1470
- const normalized = normalizeE164(phoneNumber);
1471
- if (!normalized) {
1472
- return phoneNumber;
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".`);
1473
2969
  }
1474
- const digits = normalized.replace(/^\+/, "");
1475
- if (digits.length <= 10) {
1476
- return normalized;
2970
+ if ("authDir" in config && config.authDir) {
2971
+ return "baileys";
1477
2972
  }
1478
- const countryCode = digits.slice(0, digits.length - 10);
1479
- const rest = digits.slice(-10);
1480
- return `+${countryCode} ${rest.slice(0, 3)} ${rest.slice(3, 6)} ${rest.slice(6)}`;
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).");
1481
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
+ };
1482
2989
  // src/types.ts
1483
2990
  var WhatsAppEventType;
1484
2991
  ((WhatsAppEventType2) => {
@@ -1504,61 +3011,78 @@ var WHATSAPP_REACTIONS = {
1504
3011
  FIRE: "\uD83D\uDD25",
1505
3012
  CELEBRATION: "\uD83C\uDF89"
1506
3013
  };
1507
-
1508
3014
  // src/index.ts
1509
- class WhatsAppPlugin extends EventEmitter4 {
1510
- client;
1511
- messageHandler;
1512
- webhookHandler;
1513
- name;
1514
- description;
1515
- actions = [sendMessageAction, sendReactionAction];
1516
- constructor(config) {
1517
- super();
1518
- this.name = "WhatsApp Plugin";
1519
- this.description = "WhatsApp integration supporting Cloud API and Baileys (QR auth)";
1520
- this.client = ClientFactory.create(config);
1521
- this.messageHandler = new MessageHandler(this.client);
1522
- this.webhookHandler = new WebhookHandler;
1523
- this.setupEventForwarding();
1524
- }
1525
- setupEventForwarding() {
1526
- this.client.on("message", (payload) => this.emit("message", payload));
1527
- this.client.on("qr", (payload) => this.emit("qr", payload));
1528
- this.client.on("ready", () => this.emit("ready"));
1529
- this.client.on("connection", (status) => this.emit("connection", status));
1530
- this.client.on("error", (error) => this.emit("error", error));
1531
- }
1532
- async start() {
1533
- await this.client.start();
1534
- }
1535
- async stop() {
1536
- await this.client.stop();
1537
- }
1538
- getConnectionStatus() {
1539
- return this.client.getConnectionStatus();
1540
- }
1541
- async sendMessage(message2) {
1542
- return this.messageHandler.send(message2);
1543
- }
1544
- async handleWebhook(event) {
1545
- return this.webhookHandler.handle(event);
1546
- }
1547
- async verifyWebhook(token) {
1548
- if (!this.client.verifyWebhook) {
1549
- throw new Error("verifyWebhook is only supported by Cloud API authentication");
1550
- }
1551
- return this.client.verifyWebhook(token);
1552
- }
1553
- }
1554
3015
  var whatsappPlugin = {
1555
3016
  name: "whatsapp",
1556
3017
  description: "WhatsApp integration for ElizaOS (Cloud API + Baileys)",
1557
- actions: [sendMessageAction, sendReactionAction]
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
+ }
1558
3035
  };
1559
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__;
1560
3079
  export {
3080
+ whatsappSetupRoutes,
3081
+ whatsappLogout,
3082
+ whatsappAuthExists,
1561
3083
  truncateText,
3084
+ stopAllPairingSessions,
3085
+ sanitizeAccountId as sanitizeWhatsAppAccountId,
1562
3086
  resolveWhatsAppToken,
1563
3087
  resolveWhatsAppSystemLocation,
1564
3088
  resolveWhatsAppGroupConfig,
@@ -1576,20 +3100,26 @@ export {
1576
3100
  isWhatsAppGroup,
1577
3101
  isValidWhatsAppNumber,
1578
3102
  isMultiAccountEnabled,
3103
+ handleWhatsAppRoute,
1579
3104
  getWhatsAppChatType,
1580
3105
  formatWhatsAppPhoneNumber,
1581
3106
  formatWhatsAppId,
1582
3107
  src_default as default,
3108
+ createWhatsAppConnectorAccountProvider,
1583
3109
  chunkWhatsAppText,
1584
3110
  checkWhatsAppUserAccess,
1585
3111
  buildWhatsAppUserJid,
1586
- WhatsAppPlugin,
3112
+ applyWhatsAppQrOverride,
3113
+ WhatsAppPairingSession,
1587
3114
  WhatsAppEventType,
3115
+ WhatsAppConnectorService,
1588
3116
  WHATSAPP_TEXT_CHUNK_LIMIT,
1589
3117
  WHATSAPP_REACTIONS,
3118
+ WHATSAPP_PROVIDER_ID,
3119
+ MAX_PAIRING_SESSIONS2 as WHATSAPP_MAX_PAIRING_SESSIONS,
1590
3120
  DEFAULT_ACCOUNT_ID,
1591
3121
  ClientFactory
1592
3122
  };
1593
3123
 
1594
- //# debugId=3ABF74EE0B1ACBEC64756E2164756E21
3124
+ //# debugId=ACCEAA76DD452ADF64756E2164756E21
1595
3125
  //# sourceMappingURL=index.js.map