@elizaos/plugin-whatsapp 2.0.0-alpha.5 → 2.0.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1595 -0
- package/dist/index.js.map +25 -0
- package/package.json +34 -10
package/dist/index.js
ADDED
|
@@ -0,0 +1,1595 @@
|
|
|
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
|
|
13
|
+
|
|
14
|
+
{{recentMessages}}
|
|
15
|
+
|
|
16
|
+
Based on the conversation, extract the message parameters.
|
|
17
|
+
|
|
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;
|
|
42
|
+
}
|
|
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 {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
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" };
|
|
64
|
+
}
|
|
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" };
|
|
114
|
+
}
|
|
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 };
|
|
162
|
+
}
|
|
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;
|
|
220
|
+
}
|
|
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;
|
|
229
|
+
}
|
|
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" };
|
|
242
|
+
}
|
|
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" };
|
|
275
|
+
}
|
|
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" };
|
|
284
|
+
}
|
|
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
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
success: true,
|
|
316
|
+
data: {
|
|
317
|
+
action: WHATSAPP_SEND_REACTION_ACTION,
|
|
318
|
+
messageId: params.messageId,
|
|
319
|
+
emoji: params.emoji
|
|
320
|
+
}
|
|
321
|
+
};
|
|
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
|
+
};
|
|
350
|
+
// src/client.ts
|
|
351
|
+
import axios from "axios";
|
|
352
|
+
import { EventEmitter } from "node:events";
|
|
353
|
+
var DEFAULT_API_VERSION = "v24.0";
|
|
354
|
+
|
|
355
|
+
class WhatsAppClient extends EventEmitter {
|
|
356
|
+
client;
|
|
357
|
+
config;
|
|
358
|
+
connectionStatus = "close";
|
|
359
|
+
constructor(config) {
|
|
360
|
+
super();
|
|
361
|
+
this.config = config;
|
|
362
|
+
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
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async start() {
|
|
372
|
+
this.connectionStatus = "open";
|
|
373
|
+
this.emit("connection", "open");
|
|
374
|
+
this.emit("ready");
|
|
375
|
+
}
|
|
376
|
+
async stop() {
|
|
377
|
+
this.connectionStatus = "close";
|
|
378
|
+
this.emit("connection", "close");
|
|
379
|
+
}
|
|
380
|
+
getConnectionStatus() {
|
|
381
|
+
return this.connectionStatus;
|
|
382
|
+
}
|
|
383
|
+
getPhoneNumberId() {
|
|
384
|
+
return this.config.phoneNumberId;
|
|
385
|
+
}
|
|
386
|
+
async sendMessage(message) {
|
|
387
|
+
const endpoint = `/${this.config.phoneNumberId}/messages`;
|
|
388
|
+
const payload = this.buildMessagePayload(message);
|
|
389
|
+
return this.client.post(endpoint, payload);
|
|
390
|
+
}
|
|
391
|
+
async sendTextMessage(to, text, _previewUrl = false) {
|
|
392
|
+
return this.sendMessage({
|
|
393
|
+
type: "text",
|
|
394
|
+
to,
|
|
395
|
+
content: text
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
async sendReaction(params) {
|
|
399
|
+
const endpoint = `/${this.config.phoneNumberId}/messages`;
|
|
400
|
+
const payload = {
|
|
401
|
+
messaging_product: "whatsapp",
|
|
402
|
+
recipient_type: "individual",
|
|
403
|
+
to: params.to,
|
|
404
|
+
type: "reaction",
|
|
405
|
+
reaction: {
|
|
406
|
+
message_id: params.messageId,
|
|
407
|
+
emoji: params.emoji
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
try {
|
|
411
|
+
const response = await this.client.post(endpoint, payload);
|
|
412
|
+
return {
|
|
413
|
+
success: true,
|
|
414
|
+
messageId: response.data.messages?.[0]?.id
|
|
415
|
+
};
|
|
416
|
+
} catch (error) {
|
|
417
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
418
|
+
return {
|
|
419
|
+
success: false,
|
|
420
|
+
error: errorMessage
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async removeReaction(to, messageId) {
|
|
425
|
+
return this.sendReaction({
|
|
426
|
+
to,
|
|
427
|
+
messageId,
|
|
428
|
+
emoji: ""
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async sendImage(to, imageUrl, caption) {
|
|
432
|
+
return this.sendMessage({
|
|
433
|
+
type: "image",
|
|
434
|
+
to,
|
|
435
|
+
content: {
|
|
436
|
+
link: imageUrl,
|
|
437
|
+
caption
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
async sendVideo(to, videoUrl, caption) {
|
|
442
|
+
return this.sendMessage({
|
|
443
|
+
type: "video",
|
|
444
|
+
to,
|
|
445
|
+
content: {
|
|
446
|
+
link: videoUrl,
|
|
447
|
+
caption
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
async sendAudio(to, audioUrl) {
|
|
452
|
+
return this.sendMessage({
|
|
453
|
+
type: "audio",
|
|
454
|
+
to,
|
|
455
|
+
content: {
|
|
456
|
+
link: audioUrl
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
async sendDocument(to, documentUrl, filename, caption) {
|
|
461
|
+
return this.sendMessage({
|
|
462
|
+
type: "document",
|
|
463
|
+
to,
|
|
464
|
+
content: {
|
|
465
|
+
link: documentUrl,
|
|
466
|
+
filename,
|
|
467
|
+
caption
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
async sendLocation(to, latitude, longitude, name, address) {
|
|
472
|
+
return this.sendMessage({
|
|
473
|
+
type: "location",
|
|
474
|
+
to,
|
|
475
|
+
content: {
|
|
476
|
+
latitude,
|
|
477
|
+
longitude,
|
|
478
|
+
name,
|
|
479
|
+
address
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
async sendButtonMessage(to, bodyText, buttons, headerText, footerText) {
|
|
484
|
+
const interactive = {
|
|
485
|
+
type: "button",
|
|
486
|
+
body: { text: bodyText },
|
|
487
|
+
action: {
|
|
488
|
+
buttons: buttons.map((btn) => ({
|
|
489
|
+
type: "reply",
|
|
490
|
+
reply: { id: btn.id, title: btn.title }
|
|
491
|
+
}))
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
if (headerText) {
|
|
495
|
+
interactive.header = { type: "text", text: headerText };
|
|
496
|
+
}
|
|
497
|
+
if (footerText) {
|
|
498
|
+
interactive.footer = { text: footerText };
|
|
499
|
+
}
|
|
500
|
+
return this.sendMessage({
|
|
501
|
+
type: "interactive",
|
|
502
|
+
to,
|
|
503
|
+
content: interactive
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
async sendListMessage(to, bodyText, buttonText, sections, headerText, footerText) {
|
|
507
|
+
const interactive = {
|
|
508
|
+
type: "list",
|
|
509
|
+
body: { text: bodyText },
|
|
510
|
+
action: {
|
|
511
|
+
button: buttonText,
|
|
512
|
+
sections
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
if (headerText) {
|
|
516
|
+
interactive.header = { type: "text", text: headerText };
|
|
517
|
+
}
|
|
518
|
+
if (footerText) {
|
|
519
|
+
interactive.footer = { text: footerText };
|
|
520
|
+
}
|
|
521
|
+
return this.sendMessage({
|
|
522
|
+
type: "interactive",
|
|
523
|
+
to,
|
|
524
|
+
content: interactive
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async markMessageAsRead(messageId) {
|
|
528
|
+
const endpoint = `/${this.config.phoneNumberId}/messages`;
|
|
529
|
+
const payload = {
|
|
530
|
+
messaging_product: "whatsapp",
|
|
531
|
+
status: "read",
|
|
532
|
+
message_id: messageId
|
|
533
|
+
};
|
|
534
|
+
try {
|
|
535
|
+
await this.client.post(endpoint, payload);
|
|
536
|
+
return true;
|
|
537
|
+
} catch {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async getMediaUrl(mediaId) {
|
|
542
|
+
try {
|
|
543
|
+
const response = await this.client.get(`/${mediaId}`);
|
|
544
|
+
return response.data.url || null;
|
|
545
|
+
} catch {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async verifyWebhook(token) {
|
|
550
|
+
return token === this.config.webhookVerifyToken;
|
|
551
|
+
}
|
|
552
|
+
buildMessagePayload(message) {
|
|
553
|
+
const basePayload = {
|
|
554
|
+
messaging_product: "whatsapp",
|
|
555
|
+
recipient_type: "individual",
|
|
556
|
+
to: message.to,
|
|
557
|
+
type: message.type
|
|
558
|
+
};
|
|
559
|
+
const contextPayload = message.replyToMessageId ? { context: { message_id: message.replyToMessageId } } : {};
|
|
560
|
+
switch (message.type) {
|
|
561
|
+
case "text":
|
|
562
|
+
return {
|
|
563
|
+
...basePayload,
|
|
564
|
+
...contextPayload,
|
|
565
|
+
text: {
|
|
566
|
+
body: message.content
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
case "template":
|
|
570
|
+
return {
|
|
571
|
+
...basePayload,
|
|
572
|
+
...contextPayload,
|
|
573
|
+
template: message.content
|
|
574
|
+
};
|
|
575
|
+
case "image": {
|
|
576
|
+
const imageContent = message.content;
|
|
577
|
+
return {
|
|
578
|
+
...basePayload,
|
|
579
|
+
...contextPayload,
|
|
580
|
+
image: {
|
|
581
|
+
link: imageContent.link,
|
|
582
|
+
caption: imageContent.caption
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
case "video": {
|
|
587
|
+
const videoContent = message.content;
|
|
588
|
+
return {
|
|
589
|
+
...basePayload,
|
|
590
|
+
...contextPayload,
|
|
591
|
+
video: {
|
|
592
|
+
link: videoContent.link,
|
|
593
|
+
caption: videoContent.caption
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
case "audio": {
|
|
598
|
+
const audioContent = message.content;
|
|
599
|
+
return {
|
|
600
|
+
...basePayload,
|
|
601
|
+
...contextPayload,
|
|
602
|
+
audio: {
|
|
603
|
+
link: audioContent.link
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
case "document": {
|
|
608
|
+
const docContent = message.content;
|
|
609
|
+
return {
|
|
610
|
+
...basePayload,
|
|
611
|
+
...contextPayload,
|
|
612
|
+
document: {
|
|
613
|
+
link: docContent.link,
|
|
614
|
+
filename: docContent.filename,
|
|
615
|
+
caption: docContent.caption
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
case "location": {
|
|
620
|
+
const locContent = message.content;
|
|
621
|
+
return {
|
|
622
|
+
...basePayload,
|
|
623
|
+
...contextPayload,
|
|
624
|
+
location: {
|
|
625
|
+
latitude: locContent.latitude,
|
|
626
|
+
longitude: locContent.longitude,
|
|
627
|
+
name: locContent.name,
|
|
628
|
+
address: locContent.address
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
case "reaction": {
|
|
633
|
+
const reactionContent = message.content;
|
|
634
|
+
return {
|
|
635
|
+
...basePayload,
|
|
636
|
+
reaction: {
|
|
637
|
+
message_id: reactionContent.messageId,
|
|
638
|
+
emoji: reactionContent.emoji
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
case "interactive": {
|
|
643
|
+
const interactiveContent = message.content;
|
|
644
|
+
return {
|
|
645
|
+
...basePayload,
|
|
646
|
+
...contextPayload,
|
|
647
|
+
interactive: interactiveContent
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
default:
|
|
651
|
+
return basePayload;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
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
|
+
// src/clients/baileys-client.ts
|
|
675
|
+
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
676
|
+
|
|
677
|
+
// src/baileys/auth.ts
|
|
678
|
+
import { useMultiFileAuthState } from "@whiskeysockets/baileys";
|
|
679
|
+
|
|
680
|
+
class BaileysAuthManager {
|
|
681
|
+
authDir;
|
|
682
|
+
state;
|
|
683
|
+
saveCreds;
|
|
684
|
+
constructor(authDir) {
|
|
685
|
+
this.authDir = authDir;
|
|
686
|
+
}
|
|
687
|
+
async initialize() {
|
|
688
|
+
const result = await useMultiFileAuthState(this.authDir);
|
|
689
|
+
this.state = result.state;
|
|
690
|
+
this.saveCreds = result.saveCreds;
|
|
691
|
+
return this.state;
|
|
692
|
+
}
|
|
693
|
+
async save() {
|
|
694
|
+
if (this.saveCreds) {
|
|
695
|
+
await this.saveCreds();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/baileys/connection.ts
|
|
701
|
+
import makeWASocket, { DisconnectReason } from "@whiskeysockets/baileys";
|
|
702
|
+
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
703
|
+
import pino from "pino";
|
|
704
|
+
|
|
705
|
+
class BaileysConnection extends EventEmitter2 {
|
|
706
|
+
socket;
|
|
707
|
+
authManager;
|
|
708
|
+
connectionStatus = "close";
|
|
709
|
+
reconnecting = false;
|
|
710
|
+
reconnectAttempts = 0;
|
|
711
|
+
maxReconnectAttempts = 10;
|
|
712
|
+
constructor(authManager) {
|
|
713
|
+
super();
|
|
714
|
+
this.authManager = authManager;
|
|
715
|
+
}
|
|
716
|
+
async connect() {
|
|
717
|
+
this.connectionStatus = "connecting";
|
|
718
|
+
this.emit("connection", "connecting");
|
|
719
|
+
const state = await this.authManager.initialize();
|
|
720
|
+
this.socket = makeWASocket({
|
|
721
|
+
auth: state,
|
|
722
|
+
printQRInTerminal: false,
|
|
723
|
+
logger: pino({ level: "silent" }),
|
|
724
|
+
browser: ["Chrome (Linux)", "", ""]
|
|
725
|
+
});
|
|
726
|
+
this.setupEventHandlers();
|
|
727
|
+
return this.socket;
|
|
728
|
+
}
|
|
729
|
+
setupEventHandlers() {
|
|
730
|
+
if (!this.socket) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
this.socket.ev.on("connection.update", async (update) => {
|
|
734
|
+
const { connection, qr, lastDisconnect } = update;
|
|
735
|
+
if (qr) {
|
|
736
|
+
this.emit("qr", qr);
|
|
737
|
+
}
|
|
738
|
+
if (connection) {
|
|
739
|
+
this.connectionStatus = connection;
|
|
740
|
+
this.emit("connection", connection);
|
|
741
|
+
}
|
|
742
|
+
if (connection === "open") {
|
|
743
|
+
this.reconnectAttempts = 0;
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (connection !== "close") {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
750
|
+
const isQRTimeout = statusCode === 515;
|
|
751
|
+
const shouldReconnect = statusCode !== DisconnectReason.loggedOut && statusCode !== 405;
|
|
752
|
+
if (lastDisconnect?.error && !isQRTimeout) {
|
|
753
|
+
this.emit("error", lastDisconnect.error);
|
|
754
|
+
}
|
|
755
|
+
if (!shouldReconnect) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (this.reconnecting) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
762
|
+
this.emit("error", new Error("Max reconnection attempts reached"));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
this.reconnecting = true;
|
|
766
|
+
try {
|
|
767
|
+
this.reconnectAttempts += 1;
|
|
768
|
+
const baseDelayMs = isQRTimeout ? 1000 : 3000;
|
|
769
|
+
const backoffMs = Math.min(baseDelayMs * Math.pow(2, this.reconnectAttempts - 1), 30000);
|
|
770
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
771
|
+
await this.connect();
|
|
772
|
+
} catch (error) {
|
|
773
|
+
this.emit("error", error);
|
|
774
|
+
} finally {
|
|
775
|
+
this.reconnecting = false;
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
this.socket.ev.on("creds.update", async () => {
|
|
779
|
+
await this.authManager.save();
|
|
780
|
+
});
|
|
781
|
+
this.socket.ev.on("messages.upsert", ({ messages }) => {
|
|
782
|
+
this.emit("messages", messages);
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
getSocket() {
|
|
786
|
+
return this.socket;
|
|
787
|
+
}
|
|
788
|
+
getStatus() {
|
|
789
|
+
return this.connectionStatus;
|
|
790
|
+
}
|
|
791
|
+
async disconnect() {
|
|
792
|
+
if (!this.socket) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
this.socket.ev.removeAllListeners();
|
|
796
|
+
this.socket.ws?.close?.();
|
|
797
|
+
this.socket = undefined;
|
|
798
|
+
this.connectionStatus = "close";
|
|
799
|
+
this.emit("connection", "close");
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/baileys/message-adapter.ts
|
|
804
|
+
class MessageAdapter {
|
|
805
|
+
toUnified(msg) {
|
|
806
|
+
return {
|
|
807
|
+
id: msg.key?.id ?? "",
|
|
808
|
+
from: msg.key?.remoteJid ?? "",
|
|
809
|
+
timestamp: Number(msg.messageTimestamp ?? 0),
|
|
810
|
+
type: this.detectType(msg),
|
|
811
|
+
content: this.extractContent(msg)
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
toBaileys(msg) {
|
|
815
|
+
switch (msg.type) {
|
|
816
|
+
case "text":
|
|
817
|
+
return { text: msg.content };
|
|
818
|
+
case "image":
|
|
819
|
+
return this.mediaWithCaption("image", msg.content);
|
|
820
|
+
case "video":
|
|
821
|
+
return this.mediaWithCaption("video", msg.content);
|
|
822
|
+
case "audio":
|
|
823
|
+
return this.mediaNoCaption("audio", msg.content);
|
|
824
|
+
case "document":
|
|
825
|
+
return this.mediaWithFilename(msg.content);
|
|
826
|
+
case "template":
|
|
827
|
+
return { text: this.renderTemplate(msg.content) };
|
|
828
|
+
default:
|
|
829
|
+
throw new Error(`Message type ${msg.type} is not yet supported for Baileys`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
mediaWithCaption(key, media) {
|
|
833
|
+
if (!media?.link) {
|
|
834
|
+
throw new Error(`${key} message requires a media link`);
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
[key]: { url: media.link },
|
|
838
|
+
...media.caption ? { caption: media.caption } : {}
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
mediaNoCaption(key, media) {
|
|
842
|
+
if (!media?.link) {
|
|
843
|
+
throw new Error(`${key} message requires a media link`);
|
|
844
|
+
}
|
|
845
|
+
return { [key]: { url: media.link } };
|
|
846
|
+
}
|
|
847
|
+
mediaWithFilename(media) {
|
|
848
|
+
if (!media?.link) {
|
|
849
|
+
throw new Error("document message requires a media link");
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
document: { url: media.link },
|
|
853
|
+
...media.filename ? { fileName: media.filename } : {},
|
|
854
|
+
...media.caption ? { caption: media.caption } : {}
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
detectType(msg) {
|
|
858
|
+
if (msg.message?.conversation || msg.message?.extendedTextMessage) {
|
|
859
|
+
return "text";
|
|
860
|
+
}
|
|
861
|
+
if (msg.message?.imageMessage) {
|
|
862
|
+
return "image";
|
|
863
|
+
}
|
|
864
|
+
if (msg.message?.audioMessage) {
|
|
865
|
+
return "audio";
|
|
866
|
+
}
|
|
867
|
+
if (msg.message?.videoMessage) {
|
|
868
|
+
return "video";
|
|
869
|
+
}
|
|
870
|
+
if (msg.message?.documentMessage) {
|
|
871
|
+
return "document";
|
|
872
|
+
}
|
|
873
|
+
return "text";
|
|
874
|
+
}
|
|
875
|
+
extractContent(msg) {
|
|
876
|
+
return msg.message?.conversation ?? msg.message?.extendedTextMessage?.text ?? "";
|
|
877
|
+
}
|
|
878
|
+
renderTemplate(template) {
|
|
879
|
+
const params = template.components?.flatMap((component) => component.parameters.map((parameter) => parameter.text).filter(Boolean));
|
|
880
|
+
return params && params.length > 0 ? `${template.name}: ${params.join(", ")}` : template.name;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/baileys/qr-code.ts
|
|
885
|
+
import QRCode from "qrcode";
|
|
886
|
+
import QRCodeTerminal from "qrcode-terminal";
|
|
887
|
+
|
|
888
|
+
class QRCodeGenerator {
|
|
889
|
+
async generate(qrString) {
|
|
890
|
+
return {
|
|
891
|
+
terminal: await this.generateTerminal(qrString),
|
|
892
|
+
dataURL: await QRCode.toDataURL(qrString),
|
|
893
|
+
raw: qrString
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
async generateTerminal(qr) {
|
|
897
|
+
return new Promise((resolve) => {
|
|
898
|
+
QRCodeTerminal.generate(qr, { small: true }, (output) => {
|
|
899
|
+
resolve(output);
|
|
900
|
+
});
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/clients/baileys-client.ts
|
|
906
|
+
class BaileysClient extends EventEmitter3 {
|
|
907
|
+
config;
|
|
908
|
+
authManager;
|
|
909
|
+
connection;
|
|
910
|
+
qrGenerator;
|
|
911
|
+
adapter;
|
|
912
|
+
constructor(config) {
|
|
913
|
+
super();
|
|
914
|
+
this.config = config;
|
|
915
|
+
this.authManager = new BaileysAuthManager(config.authDir);
|
|
916
|
+
this.connection = new BaileysConnection(this.authManager);
|
|
917
|
+
this.qrGenerator = new QRCodeGenerator;
|
|
918
|
+
this.adapter = new MessageAdapter;
|
|
919
|
+
this.setupEventForwarding();
|
|
920
|
+
}
|
|
921
|
+
setupEventForwarding() {
|
|
922
|
+
this.connection.on("qr", async (qr) => {
|
|
923
|
+
try {
|
|
924
|
+
const qrData = await this.qrGenerator.generate(qr);
|
|
925
|
+
if (this.config.printQRInTerminal !== false) {
|
|
926
|
+
console.log(`
|
|
927
|
+
=== Scan QR Code ===
|
|
928
|
+
`);
|
|
929
|
+
console.log(qrData.terminal);
|
|
930
|
+
}
|
|
931
|
+
this.emit("qr", qrData);
|
|
932
|
+
} catch (error) {
|
|
933
|
+
this.emit("error", error);
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
this.connection.on("connection", (status) => {
|
|
937
|
+
this.emit("connection", status);
|
|
938
|
+
if (status === "open") {
|
|
939
|
+
this.emit("ready");
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
this.connection.on("messages", (messages) => {
|
|
943
|
+
for (const message of messages) {
|
|
944
|
+
const maybe = message;
|
|
945
|
+
if (!maybe.key?.fromMe && maybe.message) {
|
|
946
|
+
this.emit("message", this.adapter.toUnified(message));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
this.connection.on("error", (error) => {
|
|
951
|
+
this.emit("error", error);
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
async start() {
|
|
955
|
+
await this.connection.connect();
|
|
956
|
+
}
|
|
957
|
+
async stop() {
|
|
958
|
+
await this.connection.disconnect();
|
|
959
|
+
}
|
|
960
|
+
async sendMessage(message) {
|
|
961
|
+
const socket = this.connection.getSocket();
|
|
962
|
+
if (!socket) {
|
|
963
|
+
throw new Error("Not connected to WhatsApp via Baileys");
|
|
964
|
+
}
|
|
965
|
+
const payload = this.adapter.toBaileys(message);
|
|
966
|
+
const result = await socket.sendMessage(message.to, payload);
|
|
967
|
+
const id = result?.key?.id ?? "";
|
|
968
|
+
return {
|
|
969
|
+
messaging_product: "whatsapp",
|
|
970
|
+
contacts: [{ input: message.to, wa_id: message.to }],
|
|
971
|
+
messages: [{ id }]
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
getConnectionStatus() {
|
|
975
|
+
return this.connection.getStatus();
|
|
976
|
+
}
|
|
977
|
+
}
|
|
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);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/handlers/message.handler.ts
|
|
991
|
+
class MessageHandler {
|
|
992
|
+
client;
|
|
993
|
+
constructor(client) {
|
|
994
|
+
this.client = client;
|
|
995
|
+
}
|
|
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
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
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
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
async handleMessage(message) {
|
|
1035
|
+
console.log("Received message:", message);
|
|
1036
|
+
}
|
|
1037
|
+
async handleStatus(status) {
|
|
1038
|
+
console.log("Received status update:", status);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
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;
|
|
1047
|
+
}
|
|
1048
|
+
const trimmed = accountId.trim().toLowerCase();
|
|
1049
|
+
if (!trimmed || trimmed === "default") {
|
|
1050
|
+
return DEFAULT_ACCOUNT_ID;
|
|
1051
|
+
}
|
|
1052
|
+
return trimmed;
|
|
1053
|
+
}
|
|
1054
|
+
function getMultiAccountConfig(runtime) {
|
|
1055
|
+
const characterWhatsApp = runtime.character?.settings?.whatsapp;
|
|
1056
|
+
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
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
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
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const result = Array.from(ids);
|
|
1090
|
+
if (result.length === 0) {
|
|
1091
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
1092
|
+
}
|
|
1093
|
+
return result.slice().sort((a, b) => a.localeCompare(b));
|
|
1094
|
+
}
|
|
1095
|
+
function resolveDefaultWhatsAppAccountId(runtime) {
|
|
1096
|
+
const ids = listWhatsAppAccountIds(runtime);
|
|
1097
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
1098
|
+
return DEFAULT_ACCOUNT_ID;
|
|
1099
|
+
}
|
|
1100
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
1101
|
+
}
|
|
1102
|
+
function getAccountConfig(runtime, accountId) {
|
|
1103
|
+
const config = getMultiAccountConfig(runtime);
|
|
1104
|
+
const accounts = config.accounts;
|
|
1105
|
+
if (!accounts || typeof accounts !== "object") {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const direct = accounts[accountId];
|
|
1109
|
+
if (direct) {
|
|
1110
|
+
return direct;
|
|
1111
|
+
}
|
|
1112
|
+
const normalized = normalizeAccountId(accountId);
|
|
1113
|
+
const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized);
|
|
1114
|
+
return matchKey ? accounts[matchKey] : undefined;
|
|
1115
|
+
}
|
|
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" };
|
|
1121
|
+
}
|
|
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
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return { token: "", source: "none" };
|
|
1132
|
+
}
|
|
1133
|
+
function filterDefined(obj) {
|
|
1134
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
1135
|
+
}
|
|
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
|
+
};
|
|
1154
|
+
return {
|
|
1155
|
+
...filterDefined(envConfig),
|
|
1156
|
+
...filterDefined(baseConfig),
|
|
1157
|
+
...filterDefined(accountConfig)
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
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);
|
|
1170
|
+
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
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
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;
|
|
1188
|
+
}
|
|
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;
|
|
1195
|
+
}
|
|
1196
|
+
return multiConfig.groups?.[groupId];
|
|
1197
|
+
}
|
|
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;
|
|
1204
|
+
}
|
|
1205
|
+
if (policy2 === "open") {
|
|
1206
|
+
return true;
|
|
1207
|
+
}
|
|
1208
|
+
if (groupConfig?.allowFrom?.length) {
|
|
1209
|
+
return groupConfig.allowFrom.some((allowed) => String(allowed) === identifier);
|
|
1210
|
+
}
|
|
1211
|
+
if (accountConfig.groupAllowFrom?.length) {
|
|
1212
|
+
return accountConfig.groupAllowFrom.some((allowed) => String(allowed) === identifier);
|
|
1213
|
+
}
|
|
1214
|
+
return policy2 !== "allowlist";
|
|
1215
|
+
}
|
|
1216
|
+
const policy = accountConfig.dmPolicy ?? "pairing";
|
|
1217
|
+
if (policy === "disabled") {
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
if (policy === "open") {
|
|
1221
|
+
return true;
|
|
1222
|
+
}
|
|
1223
|
+
if (policy === "pairing") {
|
|
1224
|
+
return true;
|
|
1225
|
+
}
|
|
1226
|
+
if (accountConfig.allowFrom?.length) {
|
|
1227
|
+
return accountConfig.allowFrom.some((allowed) => String(allowed) === identifier);
|
|
1228
|
+
}
|
|
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 };
|
|
1241
|
+
}
|
|
1242
|
+
if (policy2 === "open") {
|
|
1243
|
+
return { allowed: true };
|
|
1244
|
+
}
|
|
1245
|
+
if (groupConfig?.allowFrom?.length) {
|
|
1246
|
+
const allowed = groupConfig.allowFrom.some((a) => String(a) === identifier);
|
|
1247
|
+
return { allowed };
|
|
1248
|
+
}
|
|
1249
|
+
if (accountConfig.groupAllowFrom?.length) {
|
|
1250
|
+
const allowed = accountConfig.groupAllowFrom.some((a) => String(a) === identifier);
|
|
1251
|
+
return { allowed };
|
|
1252
|
+
}
|
|
1253
|
+
return { allowed: policy2 !== "allowlist" };
|
|
1254
|
+
}
|
|
1255
|
+
const policy = accountConfig.dmPolicy ?? "pairing";
|
|
1256
|
+
if (policy === "disabled") {
|
|
1257
|
+
return { allowed: false };
|
|
1258
|
+
}
|
|
1259
|
+
if (policy === "open") {
|
|
1260
|
+
return { allowed: true };
|
|
1261
|
+
}
|
|
1262
|
+
if (policy === "pairing") {
|
|
1263
|
+
const result = await checkPairingAllowed(runtime, {
|
|
1264
|
+
channel: "whatsapp",
|
|
1265
|
+
senderId: identifier,
|
|
1266
|
+
metadata
|
|
1267
|
+
});
|
|
1268
|
+
return {
|
|
1269
|
+
allowed: result.allowed,
|
|
1270
|
+
pairingCode: result.pairingCode,
|
|
1271
|
+
newPairingRequest: result.newRequest,
|
|
1272
|
+
replyMessage: result.replyMessage
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
if (accountConfig.allowFrom?.length) {
|
|
1276
|
+
const allowed = accountConfig.allowFrom.some((a) => String(a) === identifier);
|
|
1277
|
+
if (allowed) {
|
|
1278
|
+
return { allowed: true };
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
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;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function normalizeE164(input) {
|
|
1299
|
+
const stripped = input.replace(/[\s\-().]+/g, "");
|
|
1300
|
+
const digitsOnly = stripped.replace(/[^\d+]/g, "");
|
|
1301
|
+
if (!digitsOnly) {
|
|
1302
|
+
return "";
|
|
1303
|
+
}
|
|
1304
|
+
if (digitsOnly.startsWith("+")) {
|
|
1305
|
+
return digitsOnly;
|
|
1306
|
+
}
|
|
1307
|
+
if (digitsOnly.startsWith("00")) {
|
|
1308
|
+
return `+${digitsOnly.slice(2)}`;
|
|
1309
|
+
}
|
|
1310
|
+
if (digitsOnly.length >= 10) {
|
|
1311
|
+
return `+${digitsOnly}`;
|
|
1312
|
+
}
|
|
1313
|
+
return digitsOnly;
|
|
1314
|
+
}
|
|
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;
|
|
1324
|
+
}
|
|
1325
|
+
return /^[0-9]+(-[0-9]+)*$/.test(localPart);
|
|
1326
|
+
}
|
|
1327
|
+
function isWhatsAppUserTarget(value) {
|
|
1328
|
+
const candidate = stripWhatsAppTargetPrefixes(value);
|
|
1329
|
+
return WHATSAPP_USER_JID_RE.test(candidate) || WHATSAPP_LID_RE.test(candidate);
|
|
1330
|
+
}
|
|
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];
|
|
1339
|
+
}
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
function normalizeWhatsAppTarget(value) {
|
|
1343
|
+
const candidate = stripWhatsAppTargetPrefixes(value);
|
|
1344
|
+
if (!candidate) {
|
|
1345
|
+
return null;
|
|
1346
|
+
}
|
|
1347
|
+
if (isWhatsAppGroupJid(candidate)) {
|
|
1348
|
+
const localPart = candidate.slice(0, candidate.length - "@g.us".length);
|
|
1349
|
+
return `${localPart}@g.us`;
|
|
1350
|
+
}
|
|
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;
|
|
1358
|
+
}
|
|
1359
|
+
if (candidate.includes("@")) {
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
1362
|
+
const normalized = normalizeE164(candidate);
|
|
1363
|
+
return normalized.length > 1 ? normalized : null;
|
|
1364
|
+
}
|
|
1365
|
+
function formatWhatsAppId(id) {
|
|
1366
|
+
if (isWhatsAppGroupJid(id)) {
|
|
1367
|
+
return `group:${id}`;
|
|
1368
|
+
}
|
|
1369
|
+
const normalized = normalizeWhatsAppTarget(id);
|
|
1370
|
+
return normalized || id;
|
|
1371
|
+
}
|
|
1372
|
+
function isWhatsAppGroup(id) {
|
|
1373
|
+
return isWhatsAppGroupJid(id);
|
|
1374
|
+
}
|
|
1375
|
+
function getWhatsAppChatType(id) {
|
|
1376
|
+
return isWhatsAppGroupJid(id) ? "group" : "user";
|
|
1377
|
+
}
|
|
1378
|
+
function buildWhatsAppUserJid(phoneNumber) {
|
|
1379
|
+
const normalized = normalizeE164(phoneNumber);
|
|
1380
|
+
const digits = normalized.replace(/^\+/, "");
|
|
1381
|
+
return `${digits}@s.whatsapp.net`;
|
|
1382
|
+
}
|
|
1383
|
+
function splitAtBreakPoint(text, limit) {
|
|
1384
|
+
if (text.length <= limit) {
|
|
1385
|
+
return { chunk: text, remainder: "" };
|
|
1386
|
+
}
|
|
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
|
+
};
|
|
1396
|
+
}
|
|
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
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
const sentenceEnd = Math.max(searchArea.lastIndexOf(". "), searchArea.lastIndexOf("! "), searchArea.lastIndexOf("? "));
|
|
1406
|
+
if (sentenceEnd > limit * 0.5) {
|
|
1407
|
+
return {
|
|
1408
|
+
chunk: text.slice(0, sentenceEnd + 1).trimEnd(),
|
|
1409
|
+
remainder: text.slice(sentenceEnd + 2).trimStart()
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
const space = searchArea.lastIndexOf(" ");
|
|
1413
|
+
if (space > limit * 0.5) {
|
|
1414
|
+
return {
|
|
1415
|
+
chunk: text.slice(0, space).trimEnd(),
|
|
1416
|
+
remainder: text.slice(space + 1).trimStart()
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
return {
|
|
1420
|
+
chunk: text.slice(0, limit),
|
|
1421
|
+
remainder: text.slice(limit)
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
function chunkWhatsAppText(text, opts = {}) {
|
|
1425
|
+
const limit = opts.limit ?? WHATSAPP_TEXT_CHUNK_LIMIT;
|
|
1426
|
+
if (!text?.trim()) {
|
|
1427
|
+
return [];
|
|
1428
|
+
}
|
|
1429
|
+
const normalizedText = text.trim();
|
|
1430
|
+
if (normalizedText.length <= limit) {
|
|
1431
|
+
return [normalizedText];
|
|
1432
|
+
}
|
|
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;
|
|
1441
|
+
}
|
|
1442
|
+
return chunks.filter((c) => c.length > 0);
|
|
1443
|
+
}
|
|
1444
|
+
function truncateText(text, maxLength) {
|
|
1445
|
+
if (text.length <= maxLength) {
|
|
1446
|
+
return text;
|
|
1447
|
+
}
|
|
1448
|
+
if (maxLength <= 3) {
|
|
1449
|
+
return "...".slice(0, maxLength);
|
|
1450
|
+
}
|
|
1451
|
+
return `${text.slice(0, maxLength - 3)}...`;
|
|
1452
|
+
}
|
|
1453
|
+
function resolveWhatsAppSystemLocation(params) {
|
|
1454
|
+
const { chatType, chatId, chatName } = params;
|
|
1455
|
+
const name = chatName || chatId.slice(0, 8);
|
|
1456
|
+
return `WhatsApp ${chatType}:${name}`;
|
|
1457
|
+
}
|
|
1458
|
+
function isValidWhatsAppNumber(value) {
|
|
1459
|
+
const normalized = normalizeWhatsAppTarget(value);
|
|
1460
|
+
if (!normalized) {
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
if (!normalized.startsWith("+")) {
|
|
1464
|
+
return false;
|
|
1465
|
+
}
|
|
1466
|
+
const digits = normalized.replace(/^\+/, "");
|
|
1467
|
+
return /^\d{10,15}$/.test(digits);
|
|
1468
|
+
}
|
|
1469
|
+
function formatWhatsAppPhoneNumber(phoneNumber) {
|
|
1470
|
+
const normalized = normalizeE164(phoneNumber);
|
|
1471
|
+
if (!normalized) {
|
|
1472
|
+
return phoneNumber;
|
|
1473
|
+
}
|
|
1474
|
+
const digits = normalized.replace(/^\+/, "");
|
|
1475
|
+
if (digits.length <= 10) {
|
|
1476
|
+
return normalized;
|
|
1477
|
+
}
|
|
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)}`;
|
|
1481
|
+
}
|
|
1482
|
+
// src/types.ts
|
|
1483
|
+
var WhatsAppEventType;
|
|
1484
|
+
((WhatsAppEventType2) => {
|
|
1485
|
+
WhatsAppEventType2["MESSAGE_RECEIVED"] = "WHATSAPP_MESSAGE_RECEIVED";
|
|
1486
|
+
WhatsAppEventType2["MESSAGE_SENT"] = "WHATSAPP_MESSAGE_SENT";
|
|
1487
|
+
WhatsAppEventType2["MESSAGE_DELIVERED"] = "WHATSAPP_MESSAGE_DELIVERED";
|
|
1488
|
+
WhatsAppEventType2["MESSAGE_READ"] = "WHATSAPP_MESSAGE_READ";
|
|
1489
|
+
WhatsAppEventType2["MESSAGE_FAILED"] = "WHATSAPP_MESSAGE_FAILED";
|
|
1490
|
+
WhatsAppEventType2["REACTION_RECEIVED"] = "WHATSAPP_REACTION_RECEIVED";
|
|
1491
|
+
WhatsAppEventType2["REACTION_SENT"] = "WHATSAPP_REACTION_SENT";
|
|
1492
|
+
WhatsAppEventType2["INTERACTIVE_REPLY"] = "WHATSAPP_INTERACTIVE_REPLY";
|
|
1493
|
+
WhatsAppEventType2["WEBHOOK_VERIFIED"] = "WHATSAPP_WEBHOOK_VERIFIED";
|
|
1494
|
+
})(WhatsAppEventType ||= {});
|
|
1495
|
+
var WHATSAPP_REACTIONS = {
|
|
1496
|
+
THUMBS_UP: "\uD83D\uDC4D",
|
|
1497
|
+
THUMBS_DOWN: "\uD83D\uDC4E",
|
|
1498
|
+
HEART: "❤️",
|
|
1499
|
+
LAUGHING: "\uD83D\uDE02",
|
|
1500
|
+
SURPRISED: "\uD83D\uDE2E",
|
|
1501
|
+
SAD: "\uD83D\uDE22",
|
|
1502
|
+
PRAYING: "\uD83D\uDE4F",
|
|
1503
|
+
CLAPPING: "\uD83D\uDC4F",
|
|
1504
|
+
FIRE: "\uD83D\uDD25",
|
|
1505
|
+
CELEBRATION: "\uD83C\uDF89"
|
|
1506
|
+
};
|
|
1507
|
+
|
|
1508
|
+
// 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
|
+
var whatsappPlugin = {
|
|
1555
|
+
name: "whatsapp",
|
|
1556
|
+
description: "WhatsApp integration for ElizaOS (Cloud API + Baileys)",
|
|
1557
|
+
actions: [sendMessageAction, sendReactionAction]
|
|
1558
|
+
};
|
|
1559
|
+
var src_default = whatsappPlugin;
|
|
1560
|
+
export {
|
|
1561
|
+
truncateText,
|
|
1562
|
+
resolveWhatsAppToken,
|
|
1563
|
+
resolveWhatsAppSystemLocation,
|
|
1564
|
+
resolveWhatsAppGroupConfig,
|
|
1565
|
+
resolveWhatsAppAccount,
|
|
1566
|
+
resolveDefaultWhatsAppAccountId,
|
|
1567
|
+
normalizeWhatsAppTarget,
|
|
1568
|
+
normalizeE164,
|
|
1569
|
+
normalizeAccountId,
|
|
1570
|
+
listWhatsAppAccountIds,
|
|
1571
|
+
listEnabledWhatsAppAccounts,
|
|
1572
|
+
isWhatsAppUserTarget,
|
|
1573
|
+
isWhatsAppUserAllowed,
|
|
1574
|
+
isWhatsAppMentionRequired,
|
|
1575
|
+
isWhatsAppGroupJid,
|
|
1576
|
+
isWhatsAppGroup,
|
|
1577
|
+
isValidWhatsAppNumber,
|
|
1578
|
+
isMultiAccountEnabled,
|
|
1579
|
+
getWhatsAppChatType,
|
|
1580
|
+
formatWhatsAppPhoneNumber,
|
|
1581
|
+
formatWhatsAppId,
|
|
1582
|
+
src_default as default,
|
|
1583
|
+
chunkWhatsAppText,
|
|
1584
|
+
checkWhatsAppUserAccess,
|
|
1585
|
+
buildWhatsAppUserJid,
|
|
1586
|
+
WhatsAppPlugin,
|
|
1587
|
+
WhatsAppEventType,
|
|
1588
|
+
WHATSAPP_TEXT_CHUNK_LIMIT,
|
|
1589
|
+
WHATSAPP_REACTIONS,
|
|
1590
|
+
DEFAULT_ACCOUNT_ID,
|
|
1591
|
+
ClientFactory
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
//# debugId=3ABF74EE0B1ACBEC64756E2164756E21
|
|
1595
|
+
//# sourceMappingURL=index.js.map
|