@elizaos/plugin-twilio 2.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +343 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1650 -0
- package/dist/index.js.map +1 -0
- package/package.json +103 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1650 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { logger as logger7 } from "@elizaos/core";
|
|
3
|
+
|
|
4
|
+
// src/actions/sendSms.ts
|
|
5
|
+
import {
|
|
6
|
+
logger as logger2
|
|
7
|
+
} from "@elizaos/core";
|
|
8
|
+
|
|
9
|
+
// src/constants.ts
|
|
10
|
+
var MESSAGE_CONSTANTS = {
|
|
11
|
+
MAX_MESSAGES: 10,
|
|
12
|
+
RECENT_MESSAGE_COUNT: 3,
|
|
13
|
+
CHAT_HISTORY_COUNT: 5,
|
|
14
|
+
INTEREST_DECAY_TIME: 5 * 60 * 1e3,
|
|
15
|
+
// 5 minutes
|
|
16
|
+
PARTIAL_INTEREST_DECAY: 3 * 60 * 1e3,
|
|
17
|
+
// 3 minutes
|
|
18
|
+
DEFAULT_SIMILARITY_THRESHOLD: 0.3,
|
|
19
|
+
DEFAULT_SIMILARITY_THRESHOLD_FOLLOW_UPS: 0.2
|
|
20
|
+
};
|
|
21
|
+
var TWILIO_SERVICE_NAME = "twilio";
|
|
22
|
+
var TWILIO_CONSTANTS = {
|
|
23
|
+
// Message limits
|
|
24
|
+
SMS_MAX_LENGTH: 1600,
|
|
25
|
+
MMS_MAX_MEDIA_SIZE: 5242880,
|
|
26
|
+
// 5MB
|
|
27
|
+
MMS_MAX_MEDIA_COUNT: 10,
|
|
28
|
+
// Voice constants
|
|
29
|
+
DEFAULT_VOICE: "alice",
|
|
30
|
+
DEFAULT_LANGUAGE: "en-US",
|
|
31
|
+
VOICE_STREAM_TIMEOUT: 3e5,
|
|
32
|
+
// 5 minutes
|
|
33
|
+
// API endpoints
|
|
34
|
+
API_BASE_URL: "https://api.twilio.com",
|
|
35
|
+
// Webhook paths
|
|
36
|
+
WEBHOOK_PATHS: {
|
|
37
|
+
SMS: "/webhooks/twilio/sms",
|
|
38
|
+
VOICE: "/webhooks/twilio/voice",
|
|
39
|
+
STATUS: "/webhooks/twilio/status",
|
|
40
|
+
VOICE_STREAM: "/webhooks/twilio/voice-stream"
|
|
41
|
+
},
|
|
42
|
+
// TwiML templates
|
|
43
|
+
TWIML: {
|
|
44
|
+
DEFAULT_VOICE_RESPONSE: `<?xml version="1.0" encoding="UTF-8"?>
|
|
45
|
+
<Response>
|
|
46
|
+
<Say voice="alice">Hello from Eliza AI assistant. How can I help you today?</Say>
|
|
47
|
+
<Pause length="1"/>
|
|
48
|
+
</Response>`,
|
|
49
|
+
STREAM_RESPONSE: (streamUrl) => `<?xml version="1.0" encoding="UTF-8"?>
|
|
50
|
+
<Response>
|
|
51
|
+
<Start>
|
|
52
|
+
<Stream url="${streamUrl}" />
|
|
53
|
+
</Start>
|
|
54
|
+
<Say>Please wait while I connect you to the AI assistant.</Say>
|
|
55
|
+
<Pause length="60"/>
|
|
56
|
+
</Response>`
|
|
57
|
+
},
|
|
58
|
+
// Rate limits
|
|
59
|
+
RATE_LIMITS: {
|
|
60
|
+
SMS_PER_SECOND: 1,
|
|
61
|
+
CALLS_PER_SECOND: 1,
|
|
62
|
+
WEBHOOK_TIMEOUT: 15e3
|
|
63
|
+
// 15 seconds
|
|
64
|
+
},
|
|
65
|
+
// Cache TTL
|
|
66
|
+
CACHE_TTL: {
|
|
67
|
+
CONVERSATION: 3600,
|
|
68
|
+
// 1 hour
|
|
69
|
+
MEDIA: 86400,
|
|
70
|
+
// 24 hours
|
|
71
|
+
CALL_STATE: 1800
|
|
72
|
+
// 30 minutes
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var ERROR_MESSAGES = {
|
|
76
|
+
INVALID_PHONE_NUMBER: "Invalid phone number format. Please use E.164 format (e.g., +18885551212)",
|
|
77
|
+
MISSING_CREDENTIALS: "Twilio credentials not configured",
|
|
78
|
+
WEBHOOK_VALIDATION_FAILED: "Failed to validate Twilio webhook signature",
|
|
79
|
+
RATE_LIMIT_EXCEEDED: "Rate limit exceeded. Please try again later.",
|
|
80
|
+
MEDIA_TOO_LARGE: "Media file too large. Maximum size is 5MB",
|
|
81
|
+
UNSUPPORTED_MEDIA_TYPE: "Unsupported media type"
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/utils.ts
|
|
85
|
+
import { logger } from "@elizaos/core";
|
|
86
|
+
function validatePhoneNumber(phoneNumber) {
|
|
87
|
+
const e164Regex = /^\+[1-9]\d{1,14}$/;
|
|
88
|
+
return e164Regex.test(phoneNumber);
|
|
89
|
+
}
|
|
90
|
+
function validateMessagingAddress(address) {
|
|
91
|
+
const messagingRegex = /^(whatsapp:)?\+[1-9]\d{1,14}$/;
|
|
92
|
+
return messagingRegex.test(address);
|
|
93
|
+
}
|
|
94
|
+
function isWhatsAppAddress(address) {
|
|
95
|
+
return address.startsWith("whatsapp:");
|
|
96
|
+
}
|
|
97
|
+
function stripWhatsAppPrefix(address) {
|
|
98
|
+
return isWhatsAppAddress(address) ? address.slice("whatsapp:".length) : address;
|
|
99
|
+
}
|
|
100
|
+
function formatMessagingAddress(address, defaultCountryCode = "+1") {
|
|
101
|
+
const hasPrefix = isWhatsAppAddress(address);
|
|
102
|
+
const formatted = formatPhoneNumber(
|
|
103
|
+
stripWhatsAppPrefix(address),
|
|
104
|
+
defaultCountryCode
|
|
105
|
+
);
|
|
106
|
+
if (!formatted) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return hasPrefix ? `whatsapp:${formatted}` : formatted;
|
|
110
|
+
}
|
|
111
|
+
function formatPhoneNumber(phoneNumber, defaultCountryCode = "+1") {
|
|
112
|
+
let cleaned = phoneNumber.replace(/[^\d+]/g, "");
|
|
113
|
+
if (!cleaned.startsWith("+")) {
|
|
114
|
+
if (cleaned.startsWith("1") && cleaned.length === 11) {
|
|
115
|
+
cleaned = "+" + cleaned;
|
|
116
|
+
} else if (cleaned.length === 10) {
|
|
117
|
+
cleaned = defaultCountryCode + cleaned;
|
|
118
|
+
} else {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (validatePhoneNumber(cleaned)) {
|
|
123
|
+
return cleaned;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
var generateTwiML = {
|
|
128
|
+
/**
|
|
129
|
+
* Generates TwiML for a simple voice response
|
|
130
|
+
* @param message The message to say
|
|
131
|
+
* @param voice The voice to use (default: alice)
|
|
132
|
+
* @returns TwiML string
|
|
133
|
+
*/
|
|
134
|
+
say: (message, voice = "alice") => {
|
|
135
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
136
|
+
<Response>
|
|
137
|
+
<Say voice="${voice}">${escapeXml(message)}</Say>
|
|
138
|
+
</Response>`;
|
|
139
|
+
},
|
|
140
|
+
/**
|
|
141
|
+
* Generates TwiML for gathering input
|
|
142
|
+
* @param prompt The prompt message
|
|
143
|
+
* @param options Gather options
|
|
144
|
+
* @returns TwiML string
|
|
145
|
+
*/
|
|
146
|
+
gather: (prompt, options = {}) => {
|
|
147
|
+
const {
|
|
148
|
+
numDigits = 1,
|
|
149
|
+
timeout = 5,
|
|
150
|
+
action = "",
|
|
151
|
+
method = "POST"
|
|
152
|
+
} = options;
|
|
153
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
154
|
+
<Response>
|
|
155
|
+
<Gather numDigits="${numDigits}" timeout="${timeout}" action="${action}" method="${method}">
|
|
156
|
+
<Say>${escapeXml(prompt)}</Say>
|
|
157
|
+
</Gather>
|
|
158
|
+
</Response>`;
|
|
159
|
+
},
|
|
160
|
+
/**
|
|
161
|
+
* Generates TwiML for starting a media stream
|
|
162
|
+
* @param streamUrl The WebSocket URL for streaming
|
|
163
|
+
* @param customParameters Optional custom parameters
|
|
164
|
+
* @returns TwiML string
|
|
165
|
+
*/
|
|
166
|
+
stream: (streamUrl, customParameters) => {
|
|
167
|
+
let params = "";
|
|
168
|
+
if (customParameters) {
|
|
169
|
+
params = Object.entries(customParameters).map(
|
|
170
|
+
([key, value]) => `<Parameter name="${key}" value="${escapeXml(value)}" />`
|
|
171
|
+
).join("\n ");
|
|
172
|
+
}
|
|
173
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
174
|
+
<Response>
|
|
175
|
+
<Start>
|
|
176
|
+
<Stream url="${streamUrl}">
|
|
177
|
+
${params}
|
|
178
|
+
</Stream>
|
|
179
|
+
</Start>
|
|
180
|
+
<Say>Please wait while I connect you to the AI assistant.</Say>
|
|
181
|
+
<Pause length="60"/>
|
|
182
|
+
</Response>`;
|
|
183
|
+
},
|
|
184
|
+
/**
|
|
185
|
+
* Generates TwiML for recording a call
|
|
186
|
+
* @param options Recording options
|
|
187
|
+
* @returns TwiML string
|
|
188
|
+
*/
|
|
189
|
+
record: (options = {}) => {
|
|
190
|
+
const {
|
|
191
|
+
maxLength = 3600,
|
|
192
|
+
timeout = 5,
|
|
193
|
+
transcribe = false,
|
|
194
|
+
action = ""
|
|
195
|
+
} = options;
|
|
196
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
197
|
+
<Response>
|
|
198
|
+
<Record maxLength="${maxLength}" timeout="${timeout}" transcribe="${transcribe}" action="${action}" />
|
|
199
|
+
</Response>`;
|
|
200
|
+
},
|
|
201
|
+
/**
|
|
202
|
+
* Generates TwiML to hang up
|
|
203
|
+
* @returns TwiML string
|
|
204
|
+
*/
|
|
205
|
+
hangup: () => {
|
|
206
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
207
|
+
<Response>
|
|
208
|
+
<Hangup />
|
|
209
|
+
</Response>`;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
function escapeXml(str) {
|
|
213
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
214
|
+
}
|
|
215
|
+
function extractPhoneNumber(input) {
|
|
216
|
+
const wantsWhatsApp = input.toLowerCase().includes("whatsapp:");
|
|
217
|
+
const patterns = [
|
|
218
|
+
/\+\d{1,15}/,
|
|
219
|
+
// E.164 format
|
|
220
|
+
/\(\d{3}\)\s*\d{3}-\d{4}/,
|
|
221
|
+
// (555) 555-5555
|
|
222
|
+
/\d{3}-\d{3}-\d{4}/,
|
|
223
|
+
// 555-555-5555
|
|
224
|
+
/\d{10,15}/
|
|
225
|
+
// Just digits
|
|
226
|
+
];
|
|
227
|
+
for (const pattern of patterns) {
|
|
228
|
+
const match = input.match(pattern);
|
|
229
|
+
if (match) {
|
|
230
|
+
const formatted = formatPhoneNumber(match[0]);
|
|
231
|
+
if (formatted) {
|
|
232
|
+
return wantsWhatsApp ? `whatsapp:${formatted}` : formatted;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
function chunkTextForSms(text, maxLength = 160) {
|
|
239
|
+
if (text.length <= maxLength) {
|
|
240
|
+
return [text];
|
|
241
|
+
}
|
|
242
|
+
const chunks = [];
|
|
243
|
+
const words = text.split(" ");
|
|
244
|
+
let currentChunk = "";
|
|
245
|
+
for (const word of words) {
|
|
246
|
+
if ((currentChunk + " " + word).trim().length <= maxLength) {
|
|
247
|
+
currentChunk = (currentChunk + " " + word).trim();
|
|
248
|
+
} else {
|
|
249
|
+
if (currentChunk) {
|
|
250
|
+
chunks.push(currentChunk);
|
|
251
|
+
}
|
|
252
|
+
currentChunk = word;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (currentChunk) {
|
|
256
|
+
chunks.push(currentChunk);
|
|
257
|
+
}
|
|
258
|
+
return chunks;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/actions/sendSms.ts
|
|
262
|
+
var sendSmsAction = {
|
|
263
|
+
name: "SEND_SMS",
|
|
264
|
+
description: "Send an SMS message to a phone number via Twilio",
|
|
265
|
+
validate: async (runtime, message, state) => {
|
|
266
|
+
const twilioService = runtime.getService(TWILIO_SERVICE_NAME);
|
|
267
|
+
if (!twilioService) {
|
|
268
|
+
logger2.error("Twilio service not found");
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
const text = message.content.text || "";
|
|
272
|
+
const phoneNumber = extractPhoneNumber(text);
|
|
273
|
+
const hasSmsIntent = text.toLowerCase().includes("sms") || text.toLowerCase().includes("text") || text.toLowerCase().includes("message");
|
|
274
|
+
return !!phoneNumber && hasSmsIntent;
|
|
275
|
+
},
|
|
276
|
+
handler: async (runtime, message, state, options, callback) => {
|
|
277
|
+
try {
|
|
278
|
+
const twilioService = runtime.getService(
|
|
279
|
+
TWILIO_SERVICE_NAME
|
|
280
|
+
);
|
|
281
|
+
if (!twilioService) {
|
|
282
|
+
throw new Error("Twilio service not available");
|
|
283
|
+
}
|
|
284
|
+
const text = message.content.text || "";
|
|
285
|
+
const phoneNumber = extractPhoneNumber(text);
|
|
286
|
+
if (!phoneNumber) {
|
|
287
|
+
throw new Error("No phone number found in message");
|
|
288
|
+
}
|
|
289
|
+
let messageContent = text.replace(phoneNumber, "").replace(
|
|
290
|
+
/^(send\s*(an?\s*)?(sms|text|message)\s*(to\s*)?|text\s+)/gi,
|
|
291
|
+
""
|
|
292
|
+
).replace(/\b(saying|with\s*(the\s*)?(message|text))\b/gi, "").trim();
|
|
293
|
+
messageContent = messageContent.replace(/^["']|["']$/g, "").trim();
|
|
294
|
+
if (!messageContent) {
|
|
295
|
+
messageContent = "Hello from your AI assistant!";
|
|
296
|
+
}
|
|
297
|
+
if (!validateMessagingAddress(phoneNumber)) {
|
|
298
|
+
throw new Error("Invalid phone number format");
|
|
299
|
+
}
|
|
300
|
+
const messageChunks = chunkTextForSms(messageContent);
|
|
301
|
+
for (const chunk of messageChunks) {
|
|
302
|
+
await twilioService.sendSms(phoneNumber, chunk);
|
|
303
|
+
logger2.info(`SMS sent to ${phoneNumber}: ${chunk.substring(0, 50)}...`);
|
|
304
|
+
}
|
|
305
|
+
if (callback) {
|
|
306
|
+
callback({
|
|
307
|
+
text: `SMS message sent successfully to ${phoneNumber}`,
|
|
308
|
+
success: true
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
logger2.error({ error: String(error) }, "Error sending SMS");
|
|
313
|
+
if (callback) {
|
|
314
|
+
callback({
|
|
315
|
+
text: `Failed to send SMS: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
316
|
+
success: false
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
examples: [
|
|
322
|
+
[
|
|
323
|
+
{
|
|
324
|
+
name: "user",
|
|
325
|
+
content: {
|
|
326
|
+
text: "Send an SMS to +18885551234 saying 'Hello from AI assistant!'"
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "assistant",
|
|
331
|
+
content: {
|
|
332
|
+
text: "I'll send that SMS message for you.",
|
|
333
|
+
action: "SEND_SMS"
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
],
|
|
337
|
+
[
|
|
338
|
+
{
|
|
339
|
+
name: "user",
|
|
340
|
+
content: {
|
|
341
|
+
text: "Text +15555551234 with the message 'Meeting confirmed for 3pm tomorrow'"
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "assistant",
|
|
346
|
+
content: {
|
|
347
|
+
text: "Sending the meeting confirmation SMS now.",
|
|
348
|
+
action: "SEND_SMS"
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
]
|
|
352
|
+
],
|
|
353
|
+
similes: [
|
|
354
|
+
"send sms",
|
|
355
|
+
"send text",
|
|
356
|
+
"text message",
|
|
357
|
+
"sms to",
|
|
358
|
+
"text to",
|
|
359
|
+
"message phone"
|
|
360
|
+
]
|
|
361
|
+
};
|
|
362
|
+
var sendSms_default = sendSmsAction;
|
|
363
|
+
|
|
364
|
+
// src/actions/makeCall.ts
|
|
365
|
+
import {
|
|
366
|
+
logger as logger3
|
|
367
|
+
} from "@elizaos/core";
|
|
368
|
+
var makeCallAction = {
|
|
369
|
+
name: "MAKE_CALL",
|
|
370
|
+
description: "Make a phone call via Twilio with a message or custom TwiML",
|
|
371
|
+
validate: async (runtime, message, state) => {
|
|
372
|
+
const twilioService = runtime.getService(TWILIO_SERVICE_NAME);
|
|
373
|
+
if (!twilioService) {
|
|
374
|
+
logger3.error("Twilio service not found");
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
const text = message.content.text || "";
|
|
378
|
+
const phoneNumber = extractPhoneNumber(text);
|
|
379
|
+
const callIntent = text.toLowerCase().includes("call") || text.toLowerCase().includes("phone") || text.toLowerCase().includes("dial");
|
|
380
|
+
return !!phoneNumber && callIntent;
|
|
381
|
+
},
|
|
382
|
+
handler: async (runtime, message, state, options, callback) => {
|
|
383
|
+
try {
|
|
384
|
+
const twilioService = runtime.getService(
|
|
385
|
+
TWILIO_SERVICE_NAME
|
|
386
|
+
);
|
|
387
|
+
if (!twilioService) {
|
|
388
|
+
throw new Error("Twilio service not available");
|
|
389
|
+
}
|
|
390
|
+
const text = message.content.text || "";
|
|
391
|
+
const phoneNumber = extractPhoneNumber(text);
|
|
392
|
+
if (!phoneNumber) {
|
|
393
|
+
throw new Error("No phone number found in message");
|
|
394
|
+
}
|
|
395
|
+
let callMessage = text.replace(phoneNumber, "").replace(/call|phone|dial/gi, "").replace(/and\s*say/gi, "").replace(/with\s*(the\s*)?(message|saying)/gi, "").trim();
|
|
396
|
+
callMessage = callMessage.replace(/^["']|["']$/g, "").trim();
|
|
397
|
+
if (!callMessage) {
|
|
398
|
+
callMessage = "Hello, this is an automated call from your AI assistant.";
|
|
399
|
+
}
|
|
400
|
+
if (!validatePhoneNumber(phoneNumber)) {
|
|
401
|
+
throw new Error("Invalid phone number format");
|
|
402
|
+
}
|
|
403
|
+
const twiml = generateTwiML.say(callMessage);
|
|
404
|
+
const call = await twilioService.makeCall(phoneNumber, twiml);
|
|
405
|
+
logger3.info(`Call initiated to ${phoneNumber}, Call SID: ${call.sid}`);
|
|
406
|
+
if (callback) {
|
|
407
|
+
callback({
|
|
408
|
+
text: `Call initiated successfully to ${phoneNumber}. Call ID: ${call.sid}`,
|
|
409
|
+
success: true
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
} catch (error) {
|
|
413
|
+
logger3.error({ error: String(error) }, "Error making call");
|
|
414
|
+
if (callback) {
|
|
415
|
+
callback({
|
|
416
|
+
text: `Failed to make call: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
417
|
+
success: false
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
examples: [
|
|
423
|
+
[
|
|
424
|
+
{
|
|
425
|
+
name: "user",
|
|
426
|
+
content: {
|
|
427
|
+
text: "Call +18885551234 and say 'This is an important reminder about your appointment tomorrow at 3pm'"
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
name: "assistant",
|
|
432
|
+
content: {
|
|
433
|
+
text: "I'll place that call with your reminder message.",
|
|
434
|
+
action: "MAKE_CALL"
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
],
|
|
438
|
+
[
|
|
439
|
+
{
|
|
440
|
+
name: "user",
|
|
441
|
+
content: {
|
|
442
|
+
text: "Phone +15555551234 with an automated message about the meeting cancellation"
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: "assistant",
|
|
447
|
+
content: {
|
|
448
|
+
text: "Initiating the call with the cancellation message.",
|
|
449
|
+
action: "MAKE_CALL"
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
]
|
|
453
|
+
],
|
|
454
|
+
similes: [
|
|
455
|
+
"make call",
|
|
456
|
+
"phone call",
|
|
457
|
+
"call phone",
|
|
458
|
+
"dial number",
|
|
459
|
+
"voice call",
|
|
460
|
+
"ring phone"
|
|
461
|
+
]
|
|
462
|
+
};
|
|
463
|
+
var makeCall_default = makeCallAction;
|
|
464
|
+
|
|
465
|
+
// src/actions/sendMms.ts
|
|
466
|
+
import {
|
|
467
|
+
logger as logger4
|
|
468
|
+
} from "@elizaos/core";
|
|
469
|
+
var sendMmsAction = {
|
|
470
|
+
name: "SEND_MMS",
|
|
471
|
+
description: "Send an MMS (multimedia message) with images, audio, or video via Twilio",
|
|
472
|
+
validate: async (runtime, message, state) => {
|
|
473
|
+
const twilioService = runtime.getService(TWILIO_SERVICE_NAME);
|
|
474
|
+
if (!twilioService) {
|
|
475
|
+
logger4.error("Twilio service not found");
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const text = message.content.text || "";
|
|
479
|
+
const phoneNumber = extractPhoneNumber(text);
|
|
480
|
+
const mediaIntent = text.toLowerCase().includes("image") || text.toLowerCase().includes("photo") || text.toLowerCase().includes("picture") || text.toLowerCase().includes("video") || text.toLowerCase().includes("media") || text.toLowerCase().includes("mms");
|
|
481
|
+
const urlPattern = /https?:\/\/[^\s]+/g;
|
|
482
|
+
const hasUrls = urlPattern.test(text);
|
|
483
|
+
return !!phoneNumber && (mediaIntent || hasUrls);
|
|
484
|
+
},
|
|
485
|
+
handler: async (runtime, message, state, options, callback) => {
|
|
486
|
+
try {
|
|
487
|
+
const twilioService = runtime.getService(
|
|
488
|
+
TWILIO_SERVICE_NAME
|
|
489
|
+
);
|
|
490
|
+
if (!twilioService) {
|
|
491
|
+
throw new Error("Twilio service not available");
|
|
492
|
+
}
|
|
493
|
+
const text = message.content.text || "";
|
|
494
|
+
const phoneNumber = extractPhoneNumber(text);
|
|
495
|
+
if (!phoneNumber) {
|
|
496
|
+
throw new Error("No phone number found in message");
|
|
497
|
+
}
|
|
498
|
+
const urlPattern = /https?:\/\/[^\s]+/g;
|
|
499
|
+
const mediaUrls = text.match(urlPattern) || [];
|
|
500
|
+
if (mediaUrls.length === 0) {
|
|
501
|
+
mediaUrls.push("https://demo.twilio.com/owl.png");
|
|
502
|
+
}
|
|
503
|
+
let messageContent = text.replace(phoneNumber, "").replace(
|
|
504
|
+
/send\s*(an?\s*)?(mms|picture|photo|image|video|media)\s*(to\s*)?/gi,
|
|
505
|
+
""
|
|
506
|
+
).replace(/with\s*(the\s*)?(image|photo|picture|video|media|mms)/gi, "").replace(/at|from/gi, "").replace(urlPattern, "").replace(/saying|with\s*(the\s*)?(message|text)/gi, "").trim();
|
|
507
|
+
messageContent = messageContent.replace(/^["']|["']$/g, "").trim();
|
|
508
|
+
if (!messageContent) {
|
|
509
|
+
messageContent = "Here's the media you requested";
|
|
510
|
+
}
|
|
511
|
+
if (!validateMessagingAddress(phoneNumber)) {
|
|
512
|
+
throw new Error("Invalid phone number format");
|
|
513
|
+
}
|
|
514
|
+
const sentMessage = await twilioService.sendSms(
|
|
515
|
+
phoneNumber,
|
|
516
|
+
messageContent,
|
|
517
|
+
mediaUrls
|
|
518
|
+
);
|
|
519
|
+
logger4.info(
|
|
520
|
+
`MMS sent to ${phoneNumber} with ${mediaUrls.length} media attachment(s)`
|
|
521
|
+
);
|
|
522
|
+
if (callback) {
|
|
523
|
+
callback({
|
|
524
|
+
text: `MMS sent successfully to ${phoneNumber} with ${mediaUrls.length} media attachment(s)`,
|
|
525
|
+
success: true
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
} catch (error) {
|
|
529
|
+
logger4.error({ error: String(error) }, "Error sending MMS");
|
|
530
|
+
if (callback) {
|
|
531
|
+
callback({
|
|
532
|
+
text: `Failed to send MMS: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
533
|
+
success: false
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
examples: [
|
|
539
|
+
[
|
|
540
|
+
{
|
|
541
|
+
name: "user",
|
|
542
|
+
content: {
|
|
543
|
+
text: "Send a picture to +18885551234 with the image from https://example.com/photo.jpg saying 'Check out this photo!'"
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
name: "assistant",
|
|
548
|
+
content: {
|
|
549
|
+
text: "I'll send that photo with your message.",
|
|
550
|
+
action: "SEND_MMS"
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
],
|
|
554
|
+
[
|
|
555
|
+
{
|
|
556
|
+
name: "user",
|
|
557
|
+
content: {
|
|
558
|
+
text: "Send an MMS to +15555551234 with the video at https://example.com/video.mp4"
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
name: "assistant",
|
|
563
|
+
content: {
|
|
564
|
+
text: "Sending the video message now.",
|
|
565
|
+
action: "SEND_MMS"
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
]
|
|
569
|
+
],
|
|
570
|
+
similes: [
|
|
571
|
+
"send mms",
|
|
572
|
+
"send picture",
|
|
573
|
+
"send photo",
|
|
574
|
+
"send image",
|
|
575
|
+
"send video",
|
|
576
|
+
"send media",
|
|
577
|
+
"picture message",
|
|
578
|
+
"photo message"
|
|
579
|
+
]
|
|
580
|
+
};
|
|
581
|
+
var sendMms_default = sendMmsAction;
|
|
582
|
+
|
|
583
|
+
// src/providers/conversationHistory.ts
|
|
584
|
+
var conversationHistoryProvider = {
|
|
585
|
+
name: "twilioConversationHistory",
|
|
586
|
+
description: "Provides recent SMS/MMS conversation history with a phone number",
|
|
587
|
+
get: async (runtime, message, state) => {
|
|
588
|
+
try {
|
|
589
|
+
const twilioService = runtime.getService(
|
|
590
|
+
TWILIO_SERVICE_NAME
|
|
591
|
+
);
|
|
592
|
+
if (!twilioService) {
|
|
593
|
+
return {
|
|
594
|
+
text: "No Twilio conversation history available - service not initialized"
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
if (typeof message.content === "string") {
|
|
598
|
+
return {
|
|
599
|
+
text: "No phone number found in context"
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const phoneNumber = message.content.phoneNumber || message.content.text?.match(/\+?\d{10,15}/)?.[0];
|
|
603
|
+
if (!phoneNumber || typeof phoneNumber !== "string") {
|
|
604
|
+
return {
|
|
605
|
+
text: "No phone number found in context"
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
const conversationHistory = twilioService.getConversationHistory(
|
|
609
|
+
phoneNumber,
|
|
610
|
+
10
|
|
611
|
+
);
|
|
612
|
+
if (!conversationHistory || conversationHistory.length === 0) {
|
|
613
|
+
return {
|
|
614
|
+
text: `No recent conversation history with ${phoneNumber}`
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const history = conversationHistory.map((msg) => {
|
|
618
|
+
const direction = msg.direction === "inbound" ? "From" : "To";
|
|
619
|
+
const time = new Date(msg.dateCreated).toLocaleString();
|
|
620
|
+
return `[${time}] ${direction} ${phoneNumber}: ${msg.body}`;
|
|
621
|
+
}).join("\n");
|
|
622
|
+
return {
|
|
623
|
+
text: `Recent SMS conversation with ${phoneNumber}:
|
|
624
|
+
${history}`,
|
|
625
|
+
data: {
|
|
626
|
+
phoneNumber,
|
|
627
|
+
messageCount: conversationHistory.length,
|
|
628
|
+
lastMessage: conversationHistory[conversationHistory.length - 1]
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
} catch (error) {
|
|
632
|
+
console.error("Error in conversationHistoryProvider:", error);
|
|
633
|
+
return {
|
|
634
|
+
text: "Error retrieving conversation history"
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
var conversationHistory_default = conversationHistoryProvider;
|
|
640
|
+
|
|
641
|
+
// src/providers/callState.ts
|
|
642
|
+
var callStateProvider = {
|
|
643
|
+
name: "twilioCallState",
|
|
644
|
+
description: "Provides information about active voice calls and streams",
|
|
645
|
+
get: async (runtime, message, state) => {
|
|
646
|
+
try {
|
|
647
|
+
const twilioService = runtime.getService(
|
|
648
|
+
TWILIO_SERVICE_NAME
|
|
649
|
+
);
|
|
650
|
+
if (!twilioService) {
|
|
651
|
+
return {
|
|
652
|
+
text: "No Twilio call state available - service not initialized"
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
const activeStreams = twilioService.voiceStreams;
|
|
656
|
+
if (!activeStreams || activeStreams.size === 0) {
|
|
657
|
+
return {
|
|
658
|
+
text: "No active voice calls",
|
|
659
|
+
data: {
|
|
660
|
+
activeCallCount: 0
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
const callInfo = [];
|
|
665
|
+
const callData = [];
|
|
666
|
+
activeStreams.forEach((stream, callSid) => {
|
|
667
|
+
callInfo.push(`Call ${callSid}: ${stream.from} \u2192 ${stream.to}`);
|
|
668
|
+
callData.push({
|
|
669
|
+
callSid,
|
|
670
|
+
streamSid: stream.streamSid,
|
|
671
|
+
from: stream.from,
|
|
672
|
+
to: stream.to
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
return {
|
|
676
|
+
text: `Active voice calls (${activeStreams.size}):
|
|
677
|
+
${callInfo.join("\n")}`,
|
|
678
|
+
data: {
|
|
679
|
+
activeCallCount: activeStreams.size,
|
|
680
|
+
calls: callData
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
} catch (error) {
|
|
684
|
+
console.error("Error in callStateProvider:", error);
|
|
685
|
+
return {
|
|
686
|
+
text: "Error retrieving call state"
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
var callState_default = callStateProvider;
|
|
692
|
+
|
|
693
|
+
// src/service.ts
|
|
694
|
+
import {
|
|
695
|
+
ChannelType,
|
|
696
|
+
ContentType,
|
|
697
|
+
EventType,
|
|
698
|
+
Service,
|
|
699
|
+
createMessageMemory,
|
|
700
|
+
createUniqueUuid,
|
|
701
|
+
logger as logger5,
|
|
702
|
+
stringToUuid
|
|
703
|
+
} from "@elizaos/core";
|
|
704
|
+
import bodyParser from "body-parser";
|
|
705
|
+
import express from "express";
|
|
706
|
+
import NodeCache from "node-cache";
|
|
707
|
+
import twilio from "twilio";
|
|
708
|
+
import { WebSocketServer } from "ws";
|
|
709
|
+
|
|
710
|
+
// src/types.ts
|
|
711
|
+
import { z } from "zod";
|
|
712
|
+
var E164_REGEX = /^\+\d{1,15}$/;
|
|
713
|
+
var MESSAGING_REGEX = /^(whatsapp:)?\+\d{1,15}$/;
|
|
714
|
+
var TwilioMessageSchema = z.object({
|
|
715
|
+
to: z.string().regex(MESSAGING_REGEX),
|
|
716
|
+
body: z.string().min(1).max(1600),
|
|
717
|
+
mediaUrl: z.array(z.string().url()).optional()
|
|
718
|
+
});
|
|
719
|
+
var TwilioCallSchema = z.object({
|
|
720
|
+
to: z.string().regex(E164_REGEX),
|
|
721
|
+
twiml: z.string().optional(),
|
|
722
|
+
url: z.string().url().optional()
|
|
723
|
+
});
|
|
724
|
+
var SendSmsSchema = z.object({
|
|
725
|
+
to: z.string().regex(MESSAGING_REGEX),
|
|
726
|
+
message: z.string().min(1).max(1600),
|
|
727
|
+
mediaUrl: z.array(z.string().url()).optional()
|
|
728
|
+
});
|
|
729
|
+
var MakeCallSchema = z.object({
|
|
730
|
+
to: z.string().regex(E164_REGEX),
|
|
731
|
+
message: z.string().optional(),
|
|
732
|
+
url: z.string().url().optional()
|
|
733
|
+
});
|
|
734
|
+
var SendMmsSchema = z.object({
|
|
735
|
+
to: z.string().regex(MESSAGING_REGEX),
|
|
736
|
+
message: z.string().min(1).max(1600),
|
|
737
|
+
mediaUrl: z.array(z.string().url()).min(1)
|
|
738
|
+
});
|
|
739
|
+
var CACHE_KEYS = {
|
|
740
|
+
CONVERSATION: (phoneNumber) => `twilio:conversation:${phoneNumber}`,
|
|
741
|
+
CALL_STATE: (callSid) => `twilio:call:${callSid}`,
|
|
742
|
+
MEDIA: (mediaSid) => `twilio:media:${mediaSid}`
|
|
743
|
+
};
|
|
744
|
+
var TwilioError = class extends Error {
|
|
745
|
+
constructor(message, code, twilioCode) {
|
|
746
|
+
super(message);
|
|
747
|
+
this.code = code;
|
|
748
|
+
this.twilioCode = twilioCode;
|
|
749
|
+
this.name = "TwilioError";
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// src/service.ts
|
|
754
|
+
var getMessageService = (runtime) => {
|
|
755
|
+
if ("messageService" in runtime) {
|
|
756
|
+
const withMessageService = runtime;
|
|
757
|
+
return withMessageService.messageService ?? null;
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
};
|
|
761
|
+
var TwilioService = class _TwilioService extends Service {
|
|
762
|
+
static serviceType = TWILIO_SERVICE_NAME;
|
|
763
|
+
// Required static methods for Service type
|
|
764
|
+
static async start(runtime) {
|
|
765
|
+
const service = new _TwilioService();
|
|
766
|
+
await service.initialize(runtime);
|
|
767
|
+
return service;
|
|
768
|
+
}
|
|
769
|
+
static async stop(_runtime) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
twilioConfig;
|
|
773
|
+
client;
|
|
774
|
+
// Twilio client
|
|
775
|
+
app;
|
|
776
|
+
server;
|
|
777
|
+
wss;
|
|
778
|
+
cache;
|
|
779
|
+
voiceStreams;
|
|
780
|
+
isInitialized = false;
|
|
781
|
+
constructor() {
|
|
782
|
+
super();
|
|
783
|
+
this.voiceStreams = /* @__PURE__ */ new Map();
|
|
784
|
+
this.cache = new NodeCache({ stdTTL: 600 });
|
|
785
|
+
}
|
|
786
|
+
async initialize(runtime) {
|
|
787
|
+
if (this.isInitialized) {
|
|
788
|
+
logger5.warn("TwilioService already initialized");
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
this.runtime = runtime;
|
|
792
|
+
const configuredPhoneNumber = runtime.getSetting(
|
|
793
|
+
"TWILIO_PHONE_NUMBER"
|
|
794
|
+
);
|
|
795
|
+
const normalizedPhoneNumber = stripWhatsAppPrefix(
|
|
796
|
+
configuredPhoneNumber || ""
|
|
797
|
+
);
|
|
798
|
+
this.twilioConfig = {
|
|
799
|
+
accountSid: runtime.getSetting("TWILIO_ACCOUNT_SID"),
|
|
800
|
+
authToken: runtime.getSetting("TWILIO_AUTH_TOKEN"),
|
|
801
|
+
phoneNumber: normalizedPhoneNumber,
|
|
802
|
+
webhookUrl: runtime.getSetting("TWILIO_WEBHOOK_URL"),
|
|
803
|
+
webhookPort: parseInt(
|
|
804
|
+
runtime.getSetting("TWILIO_WEBHOOK_PORT") || "3000"
|
|
805
|
+
)
|
|
806
|
+
};
|
|
807
|
+
if (!this.twilioConfig.accountSid || !this.twilioConfig.authToken || !this.twilioConfig.phoneNumber) {
|
|
808
|
+
throw new TwilioError(ERROR_MESSAGES.MISSING_CREDENTIALS);
|
|
809
|
+
}
|
|
810
|
+
this.client = twilio(
|
|
811
|
+
this.twilioConfig.accountSid,
|
|
812
|
+
this.twilioConfig.authToken
|
|
813
|
+
);
|
|
814
|
+
await this.setupWebhookServer();
|
|
815
|
+
await this.updatePhoneNumberWebhooks();
|
|
816
|
+
this.isInitialized = true;
|
|
817
|
+
logger5.info("TwilioService initialized successfully");
|
|
818
|
+
}
|
|
819
|
+
// Implement stop method required by Service interface
|
|
820
|
+
async stop() {
|
|
821
|
+
await this.cleanup();
|
|
822
|
+
}
|
|
823
|
+
// Add capability description getter
|
|
824
|
+
get capabilityDescription() {
|
|
825
|
+
return "Twilio voice and SMS integration service for bidirectional communication";
|
|
826
|
+
}
|
|
827
|
+
async setupWebhookServer() {
|
|
828
|
+
this.app = express();
|
|
829
|
+
this.app.use(bodyParser.urlencoded({ extended: false }));
|
|
830
|
+
this.app.use(bodyParser.json());
|
|
831
|
+
this.app.post(TWILIO_CONSTANTS.WEBHOOK_PATHS.SMS, async (req, res) => {
|
|
832
|
+
try {
|
|
833
|
+
const webhook = req.body;
|
|
834
|
+
await this.handleIncomingSms(webhook);
|
|
835
|
+
res.type("text/xml").send("<Response></Response>");
|
|
836
|
+
} catch (error) {
|
|
837
|
+
logger5.error({ error: String(error) }, "Error handling SMS webhook");
|
|
838
|
+
res.status(500).send("<Response></Response>");
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
this.app.post(TWILIO_CONSTANTS.WEBHOOK_PATHS.VOICE, async (req, res) => {
|
|
842
|
+
try {
|
|
843
|
+
const webhook = req.body;
|
|
844
|
+
const twiml = await this.handleIncomingCall(webhook);
|
|
845
|
+
res.type("text/xml").send(twiml);
|
|
846
|
+
} catch (error) {
|
|
847
|
+
logger5.error({ error: String(error) }, "Error handling voice webhook");
|
|
848
|
+
res.type("text/xml").send(TWILIO_CONSTANTS.TWIML.DEFAULT_VOICE_RESPONSE);
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
this.app.post(TWILIO_CONSTANTS.WEBHOOK_PATHS.STATUS, (req, res) => {
|
|
852
|
+
const webhook = req.body;
|
|
853
|
+
const {
|
|
854
|
+
MessageSid,
|
|
855
|
+
MessageStatus,
|
|
856
|
+
SmsStatus,
|
|
857
|
+
CallSid,
|
|
858
|
+
CallStatus,
|
|
859
|
+
ErrorCode,
|
|
860
|
+
ErrorMessage,
|
|
861
|
+
To,
|
|
862
|
+
From,
|
|
863
|
+
MessagingServiceSid,
|
|
864
|
+
AccountSid,
|
|
865
|
+
ApiVersion
|
|
866
|
+
} = webhook;
|
|
867
|
+
logger5.info(
|
|
868
|
+
{
|
|
869
|
+
MessageSid,
|
|
870
|
+
MessageStatus,
|
|
871
|
+
SmsStatus,
|
|
872
|
+
CallSid,
|
|
873
|
+
CallStatus,
|
|
874
|
+
ErrorCode,
|
|
875
|
+
ErrorMessage,
|
|
876
|
+
To,
|
|
877
|
+
From,
|
|
878
|
+
MessagingServiceSid,
|
|
879
|
+
AccountSid,
|
|
880
|
+
ApiVersion
|
|
881
|
+
},
|
|
882
|
+
"Status callback received"
|
|
883
|
+
);
|
|
884
|
+
res.sendStatus(200);
|
|
885
|
+
});
|
|
886
|
+
this.server = this.app.listen(this.twilioConfig.webhookPort, () => {
|
|
887
|
+
logger5.info(
|
|
888
|
+
`Twilio webhook server listening on port ${this.twilioConfig.webhookPort}`
|
|
889
|
+
);
|
|
890
|
+
});
|
|
891
|
+
this.wss = new WebSocketServer({ server: this.server });
|
|
892
|
+
this.setupVoiceStreamingWebSocket();
|
|
893
|
+
}
|
|
894
|
+
setupVoiceStreamingWebSocket() {
|
|
895
|
+
this.wss.on("connection", (ws, req) => {
|
|
896
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
897
|
+
const callSid = url.searchParams.get("callSid");
|
|
898
|
+
if (!callSid) {
|
|
899
|
+
logger5.error("No callSid provided for voice stream");
|
|
900
|
+
ws.close();
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
logger5.info(`Voice stream connected for call ${callSid}`);
|
|
904
|
+
ws.on("message", async (data) => {
|
|
905
|
+
try {
|
|
906
|
+
const message = JSON.parse(data.toString());
|
|
907
|
+
switch (message.event) {
|
|
908
|
+
case "start":
|
|
909
|
+
this.handleStreamStart(callSid, message, ws);
|
|
910
|
+
break;
|
|
911
|
+
case "media":
|
|
912
|
+
this.handleStreamMedia(callSid, message);
|
|
913
|
+
break;
|
|
914
|
+
case "stop":
|
|
915
|
+
this.handleStreamStop(callSid);
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
} catch (error) {
|
|
919
|
+
logger5.error(
|
|
920
|
+
{ error: String(error) },
|
|
921
|
+
"Error processing voice stream message"
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
ws.on("close", () => {
|
|
926
|
+
logger5.info(`Voice stream closed for call ${callSid}`);
|
|
927
|
+
this.voiceStreams.delete(callSid);
|
|
928
|
+
if (this.runtime) {
|
|
929
|
+
this.runtime.emitEvent("voice:stream:ended" /* VOICE_STREAM_ENDED */, {
|
|
930
|
+
callSid
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
handleStreamStart(callSid, message, ws) {
|
|
937
|
+
const stream = {
|
|
938
|
+
streamSid: message.streamSid,
|
|
939
|
+
callSid,
|
|
940
|
+
from: message.start.customParameters.from,
|
|
941
|
+
to: message.start.customParameters.to,
|
|
942
|
+
socket: ws
|
|
943
|
+
};
|
|
944
|
+
this.voiceStreams.set(callSid, stream);
|
|
945
|
+
if (this.runtime) {
|
|
946
|
+
this.runtime.emitEvent("voice:stream:started" /* VOICE_STREAM_STARTED */, { stream });
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
handleStreamMedia(callSid, message) {
|
|
950
|
+
const stream = this.voiceStreams.get(callSid);
|
|
951
|
+
if (!stream) return;
|
|
952
|
+
const audioBuffer = Buffer.from(message.media.payload, "base64");
|
|
953
|
+
if (this.runtime) {
|
|
954
|
+
this.runtime.emitEvent("audio:received", {
|
|
955
|
+
callSid,
|
|
956
|
+
audio: audioBuffer,
|
|
957
|
+
timestamp: message.media.timestamp
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
handleStreamStop(callSid) {
|
|
962
|
+
this.voiceStreams.delete(callSid);
|
|
963
|
+
if (this.runtime) {
|
|
964
|
+
this.runtime.emitEvent("voice:stream:ended" /* VOICE_STREAM_ENDED */, { callSid });
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
async updatePhoneNumberWebhooks() {
|
|
968
|
+
try {
|
|
969
|
+
const phoneNumbers = await this.client.incomingPhoneNumbers.list({
|
|
970
|
+
phoneNumber: this.twilioConfig.phoneNumber
|
|
971
|
+
});
|
|
972
|
+
if (phoneNumbers.length === 0) {
|
|
973
|
+
throw new TwilioError(
|
|
974
|
+
`Phone number ${this.twilioConfig.phoneNumber} not found`
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
const phoneNumber = phoneNumbers[0];
|
|
978
|
+
await this.client.incomingPhoneNumbers(phoneNumber.sid).update({
|
|
979
|
+
smsUrl: `${this.twilioConfig.webhookUrl}${TWILIO_CONSTANTS.WEBHOOK_PATHS.SMS}`,
|
|
980
|
+
smsMethod: "POST",
|
|
981
|
+
voiceUrl: `${this.twilioConfig.webhookUrl}${TWILIO_CONSTANTS.WEBHOOK_PATHS.VOICE}`,
|
|
982
|
+
voiceMethod: "POST",
|
|
983
|
+
statusCallback: `${this.twilioConfig.webhookUrl}${TWILIO_CONSTANTS.WEBHOOK_PATHS.STATUS}`,
|
|
984
|
+
statusCallbackMethod: "POST"
|
|
985
|
+
});
|
|
986
|
+
logger5.info(
|
|
987
|
+
`Updated webhooks for phone number ${this.twilioConfig.phoneNumber}`
|
|
988
|
+
);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
logger5.error(
|
|
991
|
+
{ error: String(error) },
|
|
992
|
+
"Error updating phone number webhooks"
|
|
993
|
+
);
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
async sendSms(to, body, mediaUrl, fromOverride) {
|
|
998
|
+
const normalizedTo = formatMessagingAddress(to);
|
|
999
|
+
if (!normalizedTo || !validateMessagingAddress(normalizedTo)) {
|
|
1000
|
+
throw new TwilioError(ERROR_MESSAGES.INVALID_PHONE_NUMBER);
|
|
1001
|
+
}
|
|
1002
|
+
const normalizedFrom = formatMessagingAddress(
|
|
1003
|
+
fromOverride || this.twilioConfig.phoneNumber
|
|
1004
|
+
);
|
|
1005
|
+
if (!normalizedFrom) {
|
|
1006
|
+
throw new TwilioError(ERROR_MESSAGES.INVALID_PHONE_NUMBER);
|
|
1007
|
+
}
|
|
1008
|
+
const fromNumber = isWhatsAppAddress(normalizedTo) ? `whatsapp:${stripWhatsAppPrefix(normalizedFrom)}` : stripWhatsAppPrefix(normalizedFrom);
|
|
1009
|
+
try {
|
|
1010
|
+
const message = await this.client.messages.create({
|
|
1011
|
+
from: fromNumber,
|
|
1012
|
+
to: normalizedTo,
|
|
1013
|
+
body,
|
|
1014
|
+
mediaUrl,
|
|
1015
|
+
statusCallback: `${this.twilioConfig.webhookUrl}${TWILIO_CONSTANTS.WEBHOOK_PATHS.STATUS}`
|
|
1016
|
+
});
|
|
1017
|
+
const twilioMessage = {
|
|
1018
|
+
sid: message.sid,
|
|
1019
|
+
from: message.from,
|
|
1020
|
+
to: message.to,
|
|
1021
|
+
body: message.body,
|
|
1022
|
+
direction: "outbound",
|
|
1023
|
+
status: message.status,
|
|
1024
|
+
dateCreated: message.dateCreated,
|
|
1025
|
+
dateSent: message.dateSent || void 0
|
|
1026
|
+
};
|
|
1027
|
+
const conversationKey = CACHE_KEYS.CONVERSATION(normalizedTo);
|
|
1028
|
+
let conversationHistory = this.cache.get(conversationKey) || [];
|
|
1029
|
+
conversationHistory.push(twilioMessage);
|
|
1030
|
+
if (conversationHistory.length > 50) {
|
|
1031
|
+
conversationHistory = conversationHistory.slice(-50);
|
|
1032
|
+
}
|
|
1033
|
+
this.cache.set(
|
|
1034
|
+
conversationKey,
|
|
1035
|
+
conversationHistory,
|
|
1036
|
+
TWILIO_CONSTANTS.CACHE_TTL.CONVERSATION
|
|
1037
|
+
);
|
|
1038
|
+
if (this.runtime) {
|
|
1039
|
+
this.runtime.emitEvent("sms:sent" /* SMS_SENT */, twilioMessage);
|
|
1040
|
+
}
|
|
1041
|
+
return twilioMessage;
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
throw new TwilioError(
|
|
1044
|
+
`Failed to send SMS: ${error.message}`,
|
|
1045
|
+
error.code,
|
|
1046
|
+
error.moreInfo
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async makeCall(to, twiml, url) {
|
|
1051
|
+
if (!validatePhoneNumber(to)) {
|
|
1052
|
+
throw new TwilioError(ERROR_MESSAGES.INVALID_PHONE_NUMBER);
|
|
1053
|
+
}
|
|
1054
|
+
try {
|
|
1055
|
+
const callParams = {
|
|
1056
|
+
from: this.twilioConfig.phoneNumber,
|
|
1057
|
+
to,
|
|
1058
|
+
statusCallback: `${this.twilioConfig.webhookUrl}${TWILIO_CONSTANTS.WEBHOOK_PATHS.STATUS}`,
|
|
1059
|
+
statusCallbackEvent: ["initiated", "ringing", "answered", "completed"]
|
|
1060
|
+
};
|
|
1061
|
+
if (twiml) {
|
|
1062
|
+
callParams.twiml = twiml;
|
|
1063
|
+
} else if (url) {
|
|
1064
|
+
callParams.url = url;
|
|
1065
|
+
} else {
|
|
1066
|
+
callParams.twiml = TWILIO_CONSTANTS.TWIML.DEFAULT_VOICE_RESPONSE;
|
|
1067
|
+
}
|
|
1068
|
+
const call = await this.client.calls.create(callParams);
|
|
1069
|
+
const twilioCall = {
|
|
1070
|
+
sid: call.sid,
|
|
1071
|
+
from: call.from,
|
|
1072
|
+
to: call.to,
|
|
1073
|
+
status: call.status,
|
|
1074
|
+
direction: "outbound",
|
|
1075
|
+
dateCreated: call.dateCreated
|
|
1076
|
+
};
|
|
1077
|
+
this.cache.set(
|
|
1078
|
+
CACHE_KEYS.CALL_STATE(call.sid),
|
|
1079
|
+
twilioCall,
|
|
1080
|
+
TWILIO_CONSTANTS.CACHE_TTL.CALL_STATE
|
|
1081
|
+
);
|
|
1082
|
+
return twilioCall;
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
throw new TwilioError(
|
|
1085
|
+
`Failed to make call: ${error.message}`,
|
|
1086
|
+
error.code,
|
|
1087
|
+
error.moreInfo
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
async handleIncomingSms(webhook) {
|
|
1092
|
+
const message = {
|
|
1093
|
+
sid: webhook.MessageSid,
|
|
1094
|
+
from: webhook.From,
|
|
1095
|
+
to: webhook.To,
|
|
1096
|
+
body: webhook.Body,
|
|
1097
|
+
direction: "inbound",
|
|
1098
|
+
status: "received",
|
|
1099
|
+
dateCreated: /* @__PURE__ */ new Date()
|
|
1100
|
+
};
|
|
1101
|
+
if (webhook.NumMedia && parseInt(webhook.NumMedia) > 0) {
|
|
1102
|
+
message.media = [];
|
|
1103
|
+
for (let i = 0; i < parseInt(webhook.NumMedia); i++) {
|
|
1104
|
+
const mediaUrl = webhook[`MediaUrl${i}`];
|
|
1105
|
+
const contentType = webhook[`MediaContentType${i}`];
|
|
1106
|
+
if (mediaUrl) {
|
|
1107
|
+
message.media.push({
|
|
1108
|
+
url: mediaUrl,
|
|
1109
|
+
contentType: contentType || "unknown",
|
|
1110
|
+
sid: `media_${i}`
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
const conversationKey = CACHE_KEYS.CONVERSATION(webhook.From);
|
|
1116
|
+
let conversationHistory = this.cache.get(conversationKey) || [];
|
|
1117
|
+
conversationHistory.push(message);
|
|
1118
|
+
if (conversationHistory.length > 50) {
|
|
1119
|
+
conversationHistory = conversationHistory.slice(-50);
|
|
1120
|
+
}
|
|
1121
|
+
this.cache.set(
|
|
1122
|
+
conversationKey,
|
|
1123
|
+
conversationHistory,
|
|
1124
|
+
TWILIO_CONSTANTS.CACHE_TTL.CONVERSATION
|
|
1125
|
+
);
|
|
1126
|
+
if (this.runtime) {
|
|
1127
|
+
this.runtime.emitEvent("sms:received" /* SMS_RECEIVED */, message);
|
|
1128
|
+
}
|
|
1129
|
+
await this.processIncomingMessage(message);
|
|
1130
|
+
}
|
|
1131
|
+
async handleIncomingCall(webhook) {
|
|
1132
|
+
const call = {
|
|
1133
|
+
sid: webhook.CallSid,
|
|
1134
|
+
from: webhook.From,
|
|
1135
|
+
to: webhook.To,
|
|
1136
|
+
status: webhook.CallStatus,
|
|
1137
|
+
direction: "inbound",
|
|
1138
|
+
dateCreated: /* @__PURE__ */ new Date()
|
|
1139
|
+
};
|
|
1140
|
+
this.cache.set(
|
|
1141
|
+
CACHE_KEYS.CALL_STATE(webhook.CallSid),
|
|
1142
|
+
call,
|
|
1143
|
+
TWILIO_CONSTANTS.CACHE_TTL.CALL_STATE
|
|
1144
|
+
);
|
|
1145
|
+
if (this.runtime) {
|
|
1146
|
+
this.runtime.emitEvent("call:received" /* CALL_RECEIVED */, call);
|
|
1147
|
+
}
|
|
1148
|
+
const streamUrl = `wss://${new URL(this.twilioConfig.webhookUrl).host}${TWILIO_CONSTANTS.WEBHOOK_PATHS.VOICE_STREAM}?callSid=${webhook.CallSid}`;
|
|
1149
|
+
return TWILIO_CONSTANTS.TWIML.STREAM_RESPONSE(streamUrl);
|
|
1150
|
+
}
|
|
1151
|
+
async startVoiceStream(callSid) {
|
|
1152
|
+
const stream = this.voiceStreams.get(callSid);
|
|
1153
|
+
if (!stream) {
|
|
1154
|
+
throw new TwilioError(`No voice stream found for call ${callSid}`);
|
|
1155
|
+
}
|
|
1156
|
+
logger5.info(`Starting voice stream processing for call ${callSid}`);
|
|
1157
|
+
}
|
|
1158
|
+
async endVoiceStream(callSid) {
|
|
1159
|
+
const stream = this.voiceStreams.get(callSid);
|
|
1160
|
+
if (stream && stream.socket) {
|
|
1161
|
+
stream.socket.close();
|
|
1162
|
+
}
|
|
1163
|
+
this.voiceStreams.delete(callSid);
|
|
1164
|
+
}
|
|
1165
|
+
async processIncomingMessage(message) {
|
|
1166
|
+
try {
|
|
1167
|
+
const text = message.body?.trim();
|
|
1168
|
+
if (!text) {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const source = isWhatsAppAddress(message.from) ? "whatsapp" : "twilio";
|
|
1172
|
+
const entityId = createUniqueUuid(this.runtime, message.from);
|
|
1173
|
+
const roomId = createUniqueUuid(this.runtime, `twilio:${message.from}`);
|
|
1174
|
+
const worldId = createUniqueUuid(this.runtime, `twilio:${message.to}`);
|
|
1175
|
+
await this.runtime.ensureConnection({
|
|
1176
|
+
entityId,
|
|
1177
|
+
roomId,
|
|
1178
|
+
worldId,
|
|
1179
|
+
userName: message.from,
|
|
1180
|
+
source,
|
|
1181
|
+
channelId: message.from,
|
|
1182
|
+
type: ChannelType.DM
|
|
1183
|
+
});
|
|
1184
|
+
const attachments = this.buildMediaAttachments(message.media);
|
|
1185
|
+
const memory = createMessageMemory({
|
|
1186
|
+
id: stringToUuid(message.sid),
|
|
1187
|
+
entityId,
|
|
1188
|
+
roomId,
|
|
1189
|
+
content: {
|
|
1190
|
+
text,
|
|
1191
|
+
source,
|
|
1192
|
+
channelType: ChannelType.DM,
|
|
1193
|
+
phoneNumber: message.from,
|
|
1194
|
+
attachments: attachments.length > 0 ? attachments : void 0
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
const callback = async (content) => {
|
|
1198
|
+
const responseText = typeof content.text === "string" ? content.text.trim() : "";
|
|
1199
|
+
if (!responseText) {
|
|
1200
|
+
return [];
|
|
1201
|
+
}
|
|
1202
|
+
await this.sendSms(message.from, responseText, void 0, message.to);
|
|
1203
|
+
return [];
|
|
1204
|
+
};
|
|
1205
|
+
const messageService = getMessageService(this.runtime);
|
|
1206
|
+
if (messageService) {
|
|
1207
|
+
await messageService.handleMessage(this.runtime, memory, callback);
|
|
1208
|
+
} else {
|
|
1209
|
+
logger5.warn("messageService unavailable; falling back to event emit");
|
|
1210
|
+
await this.runtime.emitEvent(EventType.MESSAGE_RECEIVED, {
|
|
1211
|
+
message: memory,
|
|
1212
|
+
callback,
|
|
1213
|
+
source
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
logger5.error(
|
|
1218
|
+
{ error: String(error) },
|
|
1219
|
+
"Error processing incoming message"
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
buildMediaAttachments(media) {
|
|
1224
|
+
if (!media || media.length === 0) {
|
|
1225
|
+
return [];
|
|
1226
|
+
}
|
|
1227
|
+
return media.map((item) => ({
|
|
1228
|
+
id: item.sid,
|
|
1229
|
+
url: item.url,
|
|
1230
|
+
contentType: this.resolveContentType(item.contentType)
|
|
1231
|
+
}));
|
|
1232
|
+
}
|
|
1233
|
+
resolveContentType(contentType) {
|
|
1234
|
+
if (contentType.startsWith("image/")) {
|
|
1235
|
+
return ContentType.IMAGE;
|
|
1236
|
+
}
|
|
1237
|
+
if (contentType.startsWith("video/")) {
|
|
1238
|
+
return ContentType.VIDEO;
|
|
1239
|
+
}
|
|
1240
|
+
if (contentType.startsWith("audio/")) {
|
|
1241
|
+
return ContentType.AUDIO;
|
|
1242
|
+
}
|
|
1243
|
+
return ContentType.DOCUMENT;
|
|
1244
|
+
}
|
|
1245
|
+
sendStreamAudio(callSid, audio) {
|
|
1246
|
+
const stream = this.voiceStreams.get(callSid);
|
|
1247
|
+
if (!stream || !stream.socket) return;
|
|
1248
|
+
const payload = {
|
|
1249
|
+
event: "media",
|
|
1250
|
+
streamSid: stream.streamSid,
|
|
1251
|
+
media: {
|
|
1252
|
+
payload: audio.toString("base64")
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
stream.socket.send(JSON.stringify(payload));
|
|
1256
|
+
}
|
|
1257
|
+
async cleanup() {
|
|
1258
|
+
for (const [callSid, stream] of this.voiceStreams) {
|
|
1259
|
+
if (stream.socket) {
|
|
1260
|
+
stream.socket.close();
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
this.voiceStreams.clear();
|
|
1264
|
+
if (this.wss) {
|
|
1265
|
+
this.wss.close();
|
|
1266
|
+
}
|
|
1267
|
+
if (this.server) {
|
|
1268
|
+
this.server.close();
|
|
1269
|
+
}
|
|
1270
|
+
this.cache.flushAll();
|
|
1271
|
+
this.isInitialized = false;
|
|
1272
|
+
logger5.info("TwilioService cleaned up");
|
|
1273
|
+
}
|
|
1274
|
+
// Getters for service information
|
|
1275
|
+
get serviceType() {
|
|
1276
|
+
return TWILIO_SERVICE_NAME;
|
|
1277
|
+
}
|
|
1278
|
+
get serviceName() {
|
|
1279
|
+
return "twilio";
|
|
1280
|
+
}
|
|
1281
|
+
get phoneNumber() {
|
|
1282
|
+
return this.twilioConfig.phoneNumber;
|
|
1283
|
+
}
|
|
1284
|
+
get isConnected() {
|
|
1285
|
+
return this.isInitialized;
|
|
1286
|
+
}
|
|
1287
|
+
// Add public method to get conversation history
|
|
1288
|
+
getConversationHistory(phoneNumber, limit = 10) {
|
|
1289
|
+
const cacheKey = CACHE_KEYS.CONVERSATION(phoneNumber);
|
|
1290
|
+
const messages = this.cache.get(cacheKey);
|
|
1291
|
+
if (!messages || !Array.isArray(messages)) {
|
|
1292
|
+
return [];
|
|
1293
|
+
}
|
|
1294
|
+
return messages.slice(-limit);
|
|
1295
|
+
}
|
|
1296
|
+
// Add method to get call state
|
|
1297
|
+
getCallState(callSid) {
|
|
1298
|
+
const cacheKey = CACHE_KEYS.CALL_STATE(callSid);
|
|
1299
|
+
return this.cache.get(cacheKey);
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
// src/tests.ts
|
|
1304
|
+
import {
|
|
1305
|
+
logger as logger6
|
|
1306
|
+
} from "@elizaos/core";
|
|
1307
|
+
import * as readline from "readline";
|
|
1308
|
+
import axios from "axios";
|
|
1309
|
+
var TwilioTestSuite = class {
|
|
1310
|
+
name = "Twilio Plugin Test Suite";
|
|
1311
|
+
description = "Tests for Twilio voice and SMS functionality";
|
|
1312
|
+
tests = [
|
|
1313
|
+
{
|
|
1314
|
+
name: "Service Initialization Test",
|
|
1315
|
+
fn: async (runtime) => {
|
|
1316
|
+
const twilioService = runtime.getService(
|
|
1317
|
+
TWILIO_SERVICE_NAME
|
|
1318
|
+
);
|
|
1319
|
+
if (!twilioService) {
|
|
1320
|
+
throw new Error("Twilio service not initialized");
|
|
1321
|
+
}
|
|
1322
|
+
if (!twilioService.isConnected) {
|
|
1323
|
+
throw new Error("Twilio service is not connected");
|
|
1324
|
+
}
|
|
1325
|
+
if (!twilioService.phoneNumber) {
|
|
1326
|
+
throw new Error("Twilio phone number not configured");
|
|
1327
|
+
}
|
|
1328
|
+
logger6.info(
|
|
1329
|
+
`\u2705 Service initialized with phone number: ${twilioService.phoneNumber}`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
name: "Send SMS Test",
|
|
1335
|
+
fn: async (runtime) => {
|
|
1336
|
+
const twilioService = runtime.getService(
|
|
1337
|
+
TWILIO_SERVICE_NAME
|
|
1338
|
+
);
|
|
1339
|
+
if (!twilioService) {
|
|
1340
|
+
throw new Error("Twilio service not initialized");
|
|
1341
|
+
}
|
|
1342
|
+
const testNumber = runtime.getSetting("TWILIO_TEST_PHONE_NUMBER");
|
|
1343
|
+
if (!testNumber) {
|
|
1344
|
+
logger6.warn("TWILIO_TEST_PHONE_NUMBER not set, skipping SMS test");
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
const result = await twilioService.sendSms(
|
|
1348
|
+
testNumber,
|
|
1349
|
+
"Test SMS from Eliza Twilio plugin"
|
|
1350
|
+
);
|
|
1351
|
+
logger6.info(`\u2705 SMS test successful. Message SID: ${result.sid}`);
|
|
1352
|
+
}
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
name: "Send MMS Test",
|
|
1356
|
+
fn: async (runtime) => {
|
|
1357
|
+
const twilioService = runtime.getService(
|
|
1358
|
+
TWILIO_SERVICE_NAME
|
|
1359
|
+
);
|
|
1360
|
+
if (!twilioService) {
|
|
1361
|
+
throw new Error("Twilio service not initialized");
|
|
1362
|
+
}
|
|
1363
|
+
const testNumber = runtime.getSetting("TWILIO_TEST_PHONE_NUMBER");
|
|
1364
|
+
if (!testNumber) {
|
|
1365
|
+
logger6.warn("TWILIO_TEST_PHONE_NUMBER not set, skipping MMS test");
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
const result = await twilioService.sendSms(
|
|
1369
|
+
testNumber,
|
|
1370
|
+
"Test MMS from Eliza Twilio plugin",
|
|
1371
|
+
["https://demo.twilio.com/owl.png"]
|
|
1372
|
+
// Twilio's demo image
|
|
1373
|
+
);
|
|
1374
|
+
logger6.info(`\u2705 MMS test successful. Message SID: ${result.sid}`);
|
|
1375
|
+
}
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
name: "Make Call Test",
|
|
1379
|
+
fn: async (runtime) => {
|
|
1380
|
+
const twilioService = runtime.getService(
|
|
1381
|
+
TWILIO_SERVICE_NAME
|
|
1382
|
+
);
|
|
1383
|
+
if (!twilioService) {
|
|
1384
|
+
throw new Error("Twilio service not initialized");
|
|
1385
|
+
}
|
|
1386
|
+
const testNumber = runtime.getSetting("TWILIO_TEST_PHONE_NUMBER");
|
|
1387
|
+
if (!testNumber) {
|
|
1388
|
+
logger6.warn("TWILIO_TEST_PHONE_NUMBER not set, skipping call test");
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
const twiml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1392
|
+
<Response>
|
|
1393
|
+
<Say voice="alice">This is a test call from Eliza Twilio plugin. Goodbye!</Say>
|
|
1394
|
+
<Hangup/>
|
|
1395
|
+
</Response>`;
|
|
1396
|
+
const result = await twilioService.makeCall(testNumber, twiml);
|
|
1397
|
+
logger6.info(`\u2705 Call test successful. Call SID: ${result.sid}`);
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
{
|
|
1401
|
+
name: "Webhook Server Test",
|
|
1402
|
+
fn: async (runtime) => {
|
|
1403
|
+
const twilioService = runtime.getService(
|
|
1404
|
+
TWILIO_SERVICE_NAME
|
|
1405
|
+
);
|
|
1406
|
+
if (!twilioService) {
|
|
1407
|
+
throw new Error("Twilio service not initialized");
|
|
1408
|
+
}
|
|
1409
|
+
if (!twilioService.isConnected) {
|
|
1410
|
+
throw new Error("Webhook server is not running");
|
|
1411
|
+
}
|
|
1412
|
+
const webhookPort = runtime.getSetting("TWILIO_WEBHOOK_PORT") || "3000";
|
|
1413
|
+
logger6.info(`\u2705 Webhook server is running on port ${webhookPort}`);
|
|
1414
|
+
const webhookUrl = runtime.getSetting("TWILIO_WEBHOOK_URL");
|
|
1415
|
+
if (webhookUrl && webhookUrl.includes("localhost")) {
|
|
1416
|
+
try {
|
|
1417
|
+
const smsResponse = await axios.post(
|
|
1418
|
+
`http://localhost:${webhookPort}/webhooks/twilio/sms`,
|
|
1419
|
+
{
|
|
1420
|
+
MessageSid: "TEST123",
|
|
1421
|
+
From: "+18885551234",
|
|
1422
|
+
To: twilioService.phoneNumber,
|
|
1423
|
+
Body: "Test webhook message"
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1426
|
+
headers: {
|
|
1427
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
);
|
|
1431
|
+
if (smsResponse.status === 200) {
|
|
1432
|
+
logger6.info("\u2705 SMS webhook endpoint is responding");
|
|
1433
|
+
}
|
|
1434
|
+
const voiceResponse = await axios.post(
|
|
1435
|
+
`http://localhost:${webhookPort}/webhooks/twilio/voice`,
|
|
1436
|
+
{
|
|
1437
|
+
CallSid: "CATEST123",
|
|
1438
|
+
From: "+18885551234",
|
|
1439
|
+
To: twilioService.phoneNumber,
|
|
1440
|
+
CallStatus: "ringing"
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
headers: {
|
|
1444
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
);
|
|
1448
|
+
if (voiceResponse.status === 200) {
|
|
1449
|
+
logger6.info("\u2705 Voice webhook endpoint is responding");
|
|
1450
|
+
}
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
logger6.warn(
|
|
1453
|
+
{ error: String(error) },
|
|
1454
|
+
"Could not test webhook endpoints locally"
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
name: "Conversation History Test",
|
|
1462
|
+
fn: async (runtime) => {
|
|
1463
|
+
const twilioService = runtime.getService(
|
|
1464
|
+
TWILIO_SERVICE_NAME
|
|
1465
|
+
);
|
|
1466
|
+
if (!twilioService) {
|
|
1467
|
+
throw new Error("Twilio service not initialized");
|
|
1468
|
+
}
|
|
1469
|
+
const testNumber = runtime.getSetting("TWILIO_TEST_PHONE_NUMBER");
|
|
1470
|
+
if (!testNumber) {
|
|
1471
|
+
logger6.warn(
|
|
1472
|
+
"TWILIO_TEST_PHONE_NUMBER not set, skipping conversation history test"
|
|
1473
|
+
);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
await twilioService.sendSms(testNumber, "History test message");
|
|
1477
|
+
const history = twilioService.getConversationHistory(testNumber, 5);
|
|
1478
|
+
if (history.length > 0) {
|
|
1479
|
+
logger6.info(
|
|
1480
|
+
`\u2705 Conversation history retrieved: ${history.length} messages`
|
|
1481
|
+
);
|
|
1482
|
+
logger6.info(` Latest message: ${history[history.length - 1].body}`);
|
|
1483
|
+
} else {
|
|
1484
|
+
logger6.info(
|
|
1485
|
+
"\u2705 Conversation history is empty (expected for new number)"
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
name: "Error Handling Test",
|
|
1492
|
+
fn: async (runtime) => {
|
|
1493
|
+
const twilioService = runtime.getService(
|
|
1494
|
+
TWILIO_SERVICE_NAME
|
|
1495
|
+
);
|
|
1496
|
+
if (!twilioService) {
|
|
1497
|
+
throw new Error("Twilio service not initialized");
|
|
1498
|
+
}
|
|
1499
|
+
try {
|
|
1500
|
+
await twilioService.sendSms("invalid-number", "Test");
|
|
1501
|
+
throw new Error("Expected error for invalid phone number");
|
|
1502
|
+
} catch (error) {
|
|
1503
|
+
if (error.message.includes("Invalid phone number")) {
|
|
1504
|
+
logger6.info("\u2705 Invalid phone number error handled correctly");
|
|
1505
|
+
} else {
|
|
1506
|
+
throw error;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
try {
|
|
1510
|
+
await twilioService.sendSms("+18885551234", "");
|
|
1511
|
+
logger6.info("\u2705 Empty message handled");
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
logger6.info("\u2705 Empty message error handled correctly");
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
name: "Interactive Test Mode",
|
|
1519
|
+
fn: async (runtime) => {
|
|
1520
|
+
const twilioService = runtime.getService(
|
|
1521
|
+
TWILIO_SERVICE_NAME
|
|
1522
|
+
);
|
|
1523
|
+
if (!twilioService) {
|
|
1524
|
+
throw new Error("Twilio service not initialized");
|
|
1525
|
+
}
|
|
1526
|
+
const phoneNumber = runtime.getSetting("TWILIO_PHONE_NUMBER");
|
|
1527
|
+
const testNumber = runtime.getSetting("TWILIO_TEST_PHONE_NUMBER");
|
|
1528
|
+
if (!phoneNumber || !testNumber) {
|
|
1529
|
+
throw new Error(
|
|
1530
|
+
"TWILIO_PHONE_NUMBER and TWILIO_TEST_PHONE_NUMBER must be set for interactive testing"
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
logger6.info("\n\u{1F3AE} INTERACTIVE TWILIO TEST MODE");
|
|
1534
|
+
logger6.info("================================");
|
|
1535
|
+
logger6.info(`\u{1F4F1} Your Twilio Number: ${phoneNumber}`);
|
|
1536
|
+
logger6.info(`\u{1F4F1} Test Target Number: ${testNumber}`);
|
|
1537
|
+
logger6.info("\n\u{1F4CB} Instructions:");
|
|
1538
|
+
logger6.info(
|
|
1539
|
+
"1. The webhook server is running and listening for incoming messages"
|
|
1540
|
+
);
|
|
1541
|
+
logger6.info(
|
|
1542
|
+
"2. Text or call your Twilio number to test incoming messages"
|
|
1543
|
+
);
|
|
1544
|
+
logger6.info("3. The test will send a test SMS and make a test call");
|
|
1545
|
+
logger6.info("4. Watch the console for incoming message logs");
|
|
1546
|
+
logger6.info("\nPress Enter to start the interactive test...");
|
|
1547
|
+
await new Promise((resolve) => {
|
|
1548
|
+
const rl = readline.createInterface({
|
|
1549
|
+
input: process.stdin,
|
|
1550
|
+
output: process.stdout
|
|
1551
|
+
});
|
|
1552
|
+
rl.question("", () => {
|
|
1553
|
+
rl.close();
|
|
1554
|
+
resolve();
|
|
1555
|
+
});
|
|
1556
|
+
});
|
|
1557
|
+
logger6.info("\n\u{1F4E4} Test 1: Sending SMS...");
|
|
1558
|
+
try {
|
|
1559
|
+
const smsResult = await twilioService.sendSms(
|
|
1560
|
+
testNumber,
|
|
1561
|
+
"\u{1F389} Interactive test SMS from ElizaOS! Reply to test two-way messaging."
|
|
1562
|
+
);
|
|
1563
|
+
logger6.info(`\u2705 SMS sent! SID: ${smsResult.sid}`);
|
|
1564
|
+
logger6.info(` Status: ${smsResult.status}`);
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
logger6.error(`\u274C SMS failed: ${error}`);
|
|
1567
|
+
}
|
|
1568
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
1569
|
+
logger6.info("\n\u{1F4E4} Test 2: Making call...");
|
|
1570
|
+
try {
|
|
1571
|
+
const twiml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1572
|
+
<Response>
|
|
1573
|
+
<Say voice="alice">Hello from ElizaOS interactive test! This call confirms your Twilio integration is working.</Say>
|
|
1574
|
+
<Play>https://api.twilio.com/cowbell.mp3</Play>
|
|
1575
|
+
<Say voice="alice">Thank you for testing. Goodbye!</Say>
|
|
1576
|
+
</Response>`;
|
|
1577
|
+
const callResult = await twilioService.makeCall(testNumber, twiml);
|
|
1578
|
+
logger6.info(`\u2705 Call initiated! SID: ${callResult.sid}`);
|
|
1579
|
+
logger6.info(` Status: ${callResult.status}`);
|
|
1580
|
+
} catch (error) {
|
|
1581
|
+
logger6.error(`\u274C Call failed: ${error}`);
|
|
1582
|
+
}
|
|
1583
|
+
logger6.info("\n\u{1F4E5} Test 3: Waiting for incoming messages...");
|
|
1584
|
+
logger6.info(" Text your Twilio number now!");
|
|
1585
|
+
logger6.info(" The server will log any incoming SMS");
|
|
1586
|
+
logger6.info("\n\u23F1\uFE0F Test will continue for 30 seconds...");
|
|
1587
|
+
await new Promise((resolve) => setTimeout(resolve, 3e4));
|
|
1588
|
+
logger6.info("\n\u2728 Interactive test complete!");
|
|
1589
|
+
logger6.info("Check logs for detailed results.");
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
];
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1595
|
+
// src/index.ts
|
|
1596
|
+
var twilioPlugin = {
|
|
1597
|
+
name: "twilio",
|
|
1598
|
+
description: "Twilio plugin for bidirectional voice and text messaging integration",
|
|
1599
|
+
services: [TwilioService],
|
|
1600
|
+
actions: [sendSms_default, makeCall_default, sendMms_default],
|
|
1601
|
+
providers: [conversationHistory_default, callState_default],
|
|
1602
|
+
tests: [new TwilioTestSuite()],
|
|
1603
|
+
init: async (config, runtime) => {
|
|
1604
|
+
const accountSid = runtime.getSetting("TWILIO_ACCOUNT_SID");
|
|
1605
|
+
const authToken = runtime.getSetting("TWILIO_AUTH_TOKEN");
|
|
1606
|
+
const phoneNumber = runtime.getSetting("TWILIO_PHONE_NUMBER");
|
|
1607
|
+
const webhookUrl = runtime.getSetting("TWILIO_WEBHOOK_URL");
|
|
1608
|
+
if (!accountSid || accountSid.trim() === "") {
|
|
1609
|
+
logger7.warn(
|
|
1610
|
+
"Twilio Account SID not provided - Twilio plugin is loaded but will not be functional"
|
|
1611
|
+
);
|
|
1612
|
+
logger7.warn(
|
|
1613
|
+
"To enable Twilio functionality, please provide TWILIO_ACCOUNT_SID in your .env file"
|
|
1614
|
+
);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
if (!authToken || authToken.trim() === "") {
|
|
1618
|
+
logger7.warn(
|
|
1619
|
+
"Twilio Auth Token not provided - Twilio plugin is loaded but will not be functional"
|
|
1620
|
+
);
|
|
1621
|
+
logger7.warn(
|
|
1622
|
+
"To enable Twilio functionality, please provide TWILIO_AUTH_TOKEN in your .env file"
|
|
1623
|
+
);
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
if (!phoneNumber || phoneNumber.trim() === "") {
|
|
1627
|
+
logger7.warn(
|
|
1628
|
+
"Twilio Phone Number not provided - Twilio plugin is loaded but will not be functional"
|
|
1629
|
+
);
|
|
1630
|
+
logger7.warn(
|
|
1631
|
+
"To enable Twilio functionality, please provide TWILIO_PHONE_NUMBER in your .env file"
|
|
1632
|
+
);
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
if (!webhookUrl || webhookUrl.trim() === "") {
|
|
1636
|
+
logger7.warn(
|
|
1637
|
+
"Twilio Webhook URL not provided - Twilio will not be able to receive incoming messages or calls"
|
|
1638
|
+
);
|
|
1639
|
+
logger7.warn(
|
|
1640
|
+
"To enable incoming communication, please provide TWILIO_WEBHOOK_URL in your .env file"
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
logger7.info("Twilio plugin initialized successfully");
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
var index_default = twilioPlugin;
|
|
1647
|
+
export {
|
|
1648
|
+
index_default as default
|
|
1649
|
+
};
|
|
1650
|
+
//# sourceMappingURL=index.js.map
|