@elizaos/plugin-blooio 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/dist/index.js ADDED
@@ -0,0 +1,1019 @@
1
+ // src/index.ts
2
+ import { logger as logger5 } from "@elizaos/core";
3
+
4
+ // src/actions/sendMessage.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 BLOOIO_SERVICE_NAME = "blooio";
22
+ var BLOOIO_CONSTANTS = {
23
+ API_BASE_URL: "https://backend.blooio.com/v2/api",
24
+ WEBHOOK_PATHS: {
25
+ EVENTS: "/webhook"
26
+ },
27
+ SIGNATURE_TOLERANCE_SECONDS: 300,
28
+ CACHE_TTL: {
29
+ CONVERSATION: 3600
30
+ // 1 hour
31
+ }
32
+ };
33
+ var ERROR_MESSAGES = {
34
+ INVALID_CHAT_ID: "Invalid chat identifier. Use E.164 (+15551234567), email, or group id (grp_xxxx).",
35
+ MISSING_API_KEY: "Blooio API key not configured",
36
+ MISSING_WEBHOOK_URL: "Blooio webhook URL not configured",
37
+ WEBHOOK_VALIDATION_FAILED: "Failed to validate Blooio webhook signature"
38
+ };
39
+
40
+ // src/utils.ts
41
+ import crypto from "crypto";
42
+ import { logger } from "@elizaos/core";
43
+ var E164_REGEX = /^\+\d{1,15}$/;
44
+ var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
45
+ var GROUP_REGEX = /^grp_[A-Za-z0-9]+$/;
46
+ function isE164(value) {
47
+ return E164_REGEX.test(value);
48
+ }
49
+ function isEmail(value) {
50
+ return EMAIL_REGEX.test(value);
51
+ }
52
+ function isGroupId(value) {
53
+ return GROUP_REGEX.test(value);
54
+ }
55
+ function validateChatId(chatId) {
56
+ const parts = chatId.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
57
+ if (parts.length === 0) {
58
+ return false;
59
+ }
60
+ return parts.every(
61
+ (part) => isE164(part) || isEmail(part) || isGroupId(part)
62
+ );
63
+ }
64
+ function extractChatIdCandidates(text) {
65
+ const matches = [];
66
+ const capture = (regex) => {
67
+ let match = regex.exec(text);
68
+ while (match) {
69
+ matches.push({ value: match[0], index: match.index });
70
+ match = regex.exec(text);
71
+ }
72
+ };
73
+ capture(/\+\d{1,15}/g);
74
+ capture(/\bgrp_[A-Za-z0-9]+\b/g);
75
+ capture(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g);
76
+ matches.sort((a, b) => a.index - b.index);
77
+ const unique = [];
78
+ for (const match of matches) {
79
+ if (!unique.includes(match.value)) {
80
+ unique.push(match.value);
81
+ }
82
+ }
83
+ return unique;
84
+ }
85
+ function extractAttachmentUrls(text) {
86
+ const urls = text.match(/https?:\/\/[^\s)]+/g) ?? [];
87
+ return Array.from(new Set(urls));
88
+ }
89
+ function stripChatIdsFromText(text, chatIds) {
90
+ let cleaned = text;
91
+ for (const chatId of chatIds) {
92
+ const escaped = escapeRegExp(chatId);
93
+ cleaned = cleaned.replace(new RegExp(escaped, "g"), "");
94
+ }
95
+ return cleaned.trim();
96
+ }
97
+ function getWebhookPath(webhookUrl) {
98
+ try {
99
+ const parsed = new URL(webhookUrl);
100
+ return parsed.pathname || BLOOIO_CONSTANTS.WEBHOOK_PATHS.EVENTS;
101
+ } catch (error) {
102
+ logger.warn(
103
+ { error: String(error) },
104
+ "Invalid webhook URL, using default path"
105
+ );
106
+ return BLOOIO_CONSTANTS.WEBHOOK_PATHS.EVENTS;
107
+ }
108
+ }
109
+ function verifyWebhookSignature(secret, signatureHeader, rawBody, toleranceSeconds = BLOOIO_CONSTANTS.SIGNATURE_TOLERANCE_SECONDS) {
110
+ const parsed = parseSignatureHeader(signatureHeader);
111
+ if (!parsed) {
112
+ logger.warn(
113
+ { signatureHeader: signatureHeader.substring(0, 50) },
114
+ "Failed to parse signature header"
115
+ );
116
+ return false;
117
+ }
118
+ const { timestamp, signature } = parsed;
119
+ const timestampNumber = Number(timestamp);
120
+ if (!Number.isFinite(timestampNumber)) {
121
+ logger.warn({ timestamp }, "Invalid timestamp in signature");
122
+ return false;
123
+ }
124
+ const nowSeconds = Math.floor(Date.now() / 1e3);
125
+ const timeDiff = Math.abs(nowSeconds - timestampNumber);
126
+ if (timeDiff > toleranceSeconds) {
127
+ logger.warn(
128
+ { timestampNumber, nowSeconds, timeDiff, toleranceSeconds },
129
+ "Webhook signature timestamp out of tolerance"
130
+ );
131
+ return false;
132
+ }
133
+ const payload = `${timestamp}.${rawBody}`;
134
+ const expected = crypto.createHmac("sha256", secret).update(payload).digest("hex");
135
+ const isValid = timingSafeEqual(expected, signature);
136
+ if (!isValid) {
137
+ logger.warn(
138
+ {
139
+ expectedFirst8: expected.substring(0, 8),
140
+ actualFirst8: signature.substring(0, 8),
141
+ bodyLength: rawBody.length
142
+ },
143
+ "Webhook signature mismatch"
144
+ );
145
+ }
146
+ return isValid;
147
+ }
148
+ function parseSignatureHeader(header) {
149
+ const parts = header.split(",").map((part) => part.trim());
150
+ const timestampPart = parts.find((part) => part.startsWith("t="));
151
+ const signaturePart = parts.find((part) => part.startsWith("v1="));
152
+ if (!timestampPart || !signaturePart) {
153
+ return null;
154
+ }
155
+ const timestamp = timestampPart.split("=")[1];
156
+ const signature = signaturePart.split("=")[1];
157
+ if (!timestamp || !signature) {
158
+ return null;
159
+ }
160
+ return { timestamp, signature };
161
+ }
162
+ function timingSafeEqual(expected, actual) {
163
+ const expectedBuffer = Buffer.from(expected, "hex");
164
+ const actualBuffer = Buffer.from(actual, "hex");
165
+ if (expectedBuffer.length !== actualBuffer.length) {
166
+ return false;
167
+ }
168
+ return crypto.timingSafeEqual(expectedBuffer, actualBuffer);
169
+ }
170
+ function escapeRegExp(value) {
171
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
172
+ }
173
+
174
+ // src/actions/sendMessage.ts
175
+ var sendMessageAction = {
176
+ name: "SEND_MESSAGE",
177
+ description: "Send a message via Blooio to a chat (phone, email, or group)",
178
+ validate: async (runtime, message, _state) => {
179
+ const blooioService = runtime.getService(BLOOIO_SERVICE_NAME);
180
+ if (!blooioService) {
181
+ logger2.error("Blooio service not found");
182
+ return false;
183
+ }
184
+ const text = typeof message.content?.text === "string" ? message.content.text : "";
185
+ const candidates = extractChatIdCandidates(text);
186
+ return candidates.some((candidate) => validateChatId(candidate));
187
+ },
188
+ handler: async (runtime, message, _state, _options, callback) => {
189
+ try {
190
+ const blooioService = runtime.getService(
191
+ BLOOIO_SERVICE_NAME
192
+ );
193
+ if (!blooioService) {
194
+ throw new Error("Blooio service not available");
195
+ }
196
+ const text = typeof message.content?.text === "string" ? message.content.text : "";
197
+ const candidates = extractChatIdCandidates(text);
198
+ const validRecipients = candidates.filter(
199
+ (candidate) => validateChatId(candidate)
200
+ );
201
+ if (validRecipients.length === 0) {
202
+ throw new Error("No valid chat identifier found in message");
203
+ }
204
+ const chatId = validRecipients.map((recipient) => recipient.trim()).join(",");
205
+ const outboundAttachments = [];
206
+ const urlsFromText = extractAttachmentUrls(text);
207
+ for (const url of urlsFromText) {
208
+ outboundAttachments.push(url);
209
+ }
210
+ const contentAttachments = message.content?.attachments;
211
+ if (contentAttachments && Array.isArray(contentAttachments)) {
212
+ for (const attachment of contentAttachments) {
213
+ if (typeof attachment === "object" && attachment !== null) {
214
+ const media = attachment;
215
+ if (media.url) {
216
+ outboundAttachments.push({
217
+ url: media.url,
218
+ name: media.title ?? media.description ?? void 0
219
+ });
220
+ }
221
+ }
222
+ }
223
+ }
224
+ let messageContent = stripChatIdsFromText(text, validRecipients);
225
+ for (const url of urlsFromText) {
226
+ messageContent = messageContent.replace(url, "");
227
+ }
228
+ messageContent = messageContent.replace(/send\s+(a\s+)?(message|text|imessage|sms)?\s*(to)?\s*/gi, "").replace(/^\s*to\s+/i, "").replace(/^\s*(saying|with)\s*/gi, "").replace(/^\s*["']|["']\s*$/g, "").replace(/\s+/g, " ").trim();
229
+ if (/^(send|message|text|imessage|sms|saying|with|to)?$/i.test(
230
+ messageContent
231
+ )) {
232
+ messageContent = "";
233
+ }
234
+ if (!messageContent && outboundAttachments.length === 0) {
235
+ messageContent = "Hello from your assistant.";
236
+ }
237
+ await blooioService.sendMessage(chatId, {
238
+ text: messageContent || void 0,
239
+ attachments: outboundAttachments.length > 0 ? outboundAttachments : void 0
240
+ });
241
+ if (callback) {
242
+ await callback({
243
+ text: `Message sent successfully to ${chatId}`,
244
+ success: true
245
+ });
246
+ }
247
+ } catch (error) {
248
+ logger2.error(
249
+ { error: String(error) },
250
+ "Error sending message via Blooio"
251
+ );
252
+ if (callback) {
253
+ await callback({
254
+ text: `Failed to send message: ${error instanceof Error ? error.message : "Unknown error"}`,
255
+ success: false
256
+ });
257
+ }
258
+ }
259
+ },
260
+ examples: [
261
+ [
262
+ {
263
+ name: "user",
264
+ content: {
265
+ text: "Send a message to +17147023671 saying 'Hello from Blooio!'"
266
+ }
267
+ },
268
+ {
269
+ name: "assistant",
270
+ content: {
271
+ text: "I'll send that message.",
272
+ action: "SEND_MESSAGE"
273
+ }
274
+ }
275
+ ],
276
+ [
277
+ {
278
+ name: "user",
279
+ content: {
280
+ text: "Message jane@example.com with 'Your iMessage is ready.'"
281
+ }
282
+ },
283
+ {
284
+ name: "assistant",
285
+ content: {
286
+ text: "Sending that now.",
287
+ action: "SEND_MESSAGE"
288
+ }
289
+ }
290
+ ]
291
+ ],
292
+ similes: ["send message", "send imessage", "text", "message"]
293
+ };
294
+ var sendMessage_default = sendMessageAction;
295
+
296
+ // src/providers/conversationHistory.ts
297
+ var conversationHistoryProvider = {
298
+ name: "blooioConversationHistory",
299
+ description: "Provides recent Blooio conversation history with a chat",
300
+ get: async (runtime, message, _state) => {
301
+ try {
302
+ const blooioService = runtime.getService(
303
+ BLOOIO_SERVICE_NAME
304
+ );
305
+ if (!blooioService) {
306
+ return {
307
+ text: "No Blooio conversation history available - service not initialized"
308
+ };
309
+ }
310
+ if (typeof message.content === "string") {
311
+ return {
312
+ text: "No chat identifier found in context"
313
+ };
314
+ }
315
+ const chatId = typeof message.content?.chatId === "string" ? message.content.chatId : typeof message.content?.phoneNumber === "string" ? message.content.phoneNumber : message.content?.text?.match(
316
+ /(\+\d{1,15}|grp_[A-Za-z0-9]+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/
317
+ )?.[0];
318
+ if (!chatId || typeof chatId !== "string") {
319
+ return {
320
+ text: "No chat identifier found in context"
321
+ };
322
+ }
323
+ const conversationHistory = blooioService.getConversationHistory(
324
+ chatId,
325
+ 10
326
+ );
327
+ if (!conversationHistory || conversationHistory.length === 0) {
328
+ return {
329
+ text: `No recent conversation history with ${chatId}`
330
+ };
331
+ }
332
+ const history = conversationHistory.map((msg) => {
333
+ const direction = msg.direction === "inbound" ? "From" : "To";
334
+ const time = new Date(msg.timestamp).toLocaleString();
335
+ const text = msg.text ?? "(no text)";
336
+ return `[${time}] ${direction} ${chatId}: ${text}`;
337
+ }).join("\n");
338
+ return {
339
+ text: `Recent Blooio conversation with ${chatId}:
340
+ ${history}`,
341
+ data: {
342
+ chatId,
343
+ messageCount: conversationHistory.length,
344
+ lastMessage: conversationHistory[conversationHistory.length - 1]
345
+ }
346
+ };
347
+ } catch (error) {
348
+ console.error("Error in conversationHistoryProvider:", error);
349
+ return {
350
+ text: "Error retrieving conversation history"
351
+ };
352
+ }
353
+ }
354
+ };
355
+ var conversationHistory_default = conversationHistoryProvider;
356
+
357
+ // src/service.ts
358
+ import {
359
+ ChannelType,
360
+ ContentType,
361
+ EventType,
362
+ Service,
363
+ createMessageMemory,
364
+ createUniqueUuid,
365
+ logger as logger3,
366
+ stringToUuid
367
+ } from "@elizaos/core";
368
+ import bodyParser from "body-parser";
369
+ import express from "express";
370
+ import NodeCache from "node-cache";
371
+
372
+ // src/types.ts
373
+ import { z } from "zod";
374
+ var BlooioError = class extends Error {
375
+ constructor(message, statusCode, details) {
376
+ super(message);
377
+ this.statusCode = statusCode;
378
+ this.details = details;
379
+ this.name = "BlooioError";
380
+ }
381
+ };
382
+ var SendMessageSchema = z.object({
383
+ chatId: z.string().min(1),
384
+ text: z.string().min(1).optional(),
385
+ attachments: z.array(z.string().url()).optional()
386
+ });
387
+ var CACHE_KEYS = {
388
+ CONVERSATION: (chatId) => `blooio:conversation:${chatId}`
389
+ };
390
+
391
+ // src/service.ts
392
+ var getMessageService = (runtime) => {
393
+ if ("messageService" in runtime) {
394
+ const withMessageService = runtime;
395
+ return withMessageService.messageService ?? null;
396
+ }
397
+ return null;
398
+ };
399
+ var BlooioService = class _BlooioService extends Service {
400
+ static serviceType = BLOOIO_SERVICE_NAME;
401
+ static async start(runtime) {
402
+ const service = new _BlooioService();
403
+ await service.initialize(runtime);
404
+ return service;
405
+ }
406
+ static async stop(_runtime) {
407
+ return;
408
+ }
409
+ blooioConfig;
410
+ app;
411
+ server = null;
412
+ cache;
413
+ isInitialized = false;
414
+ constructor() {
415
+ super();
416
+ this.cache = new NodeCache({ stdTTL: 600 });
417
+ }
418
+ async initialize(runtime) {
419
+ if (this.isInitialized) {
420
+ logger3.warn("BlooioService already initialized");
421
+ return;
422
+ }
423
+ this.runtime = runtime;
424
+ const apiKey = runtime.getSetting("BLOOIO_API_KEY");
425
+ const webhookUrl = runtime.getSetting("BLOOIO_WEBHOOK_URL");
426
+ const webhookSecret = runtime.getSetting("BLOOIO_WEBHOOK_SECRET");
427
+ const baseUrlSetting = runtime.getSetting("BLOOIO_BASE_URL");
428
+ const webhookPortSetting = runtime.getSetting(
429
+ "BLOOIO_WEBHOOK_PORT"
430
+ );
431
+ const fromNumber = runtime.getSetting("BLOOIO_FROM_NUMBER");
432
+ const signatureToleranceSetting = runtime.getSetting(
433
+ "BLOOIO_SIGNATURE_TOLERANCE_SEC"
434
+ );
435
+ const webhookPathSetting = runtime.getSetting(
436
+ "BLOOIO_WEBHOOK_PATH"
437
+ );
438
+ const webhookPort = Number.parseInt(webhookPortSetting || "3001", 10);
439
+ const signatureToleranceSeconds = Number.parseInt(
440
+ signatureToleranceSetting || "",
441
+ 10
442
+ );
443
+ const resolvedSignatureTolerance = Number.isFinite(signatureToleranceSeconds) && signatureToleranceSeconds > 0 ? signatureToleranceSeconds : BLOOIO_CONSTANTS.SIGNATURE_TOLERANCE_SECONDS;
444
+ if (!apiKey || apiKey.trim() === "") {
445
+ throw new BlooioError(ERROR_MESSAGES.MISSING_API_KEY);
446
+ }
447
+ if (!webhookUrl || webhookUrl.trim() === "") {
448
+ throw new BlooioError(ERROR_MESSAGES.MISSING_WEBHOOK_URL);
449
+ }
450
+ const webhookPathRaw = webhookPathSetting && webhookPathSetting.trim() !== "" ? webhookPathSetting : getWebhookPath(webhookUrl);
451
+ const webhookPath = webhookPathRaw.startsWith("/") ? webhookPathRaw : `/${webhookPathRaw}`;
452
+ this.blooioConfig = {
453
+ apiKey,
454
+ webhookUrl,
455
+ webhookPath,
456
+ webhookPort: Number.isFinite(webhookPort) ? webhookPort : 3001,
457
+ webhookSecret: webhookSecret?.trim() ? webhookSecret : void 0,
458
+ baseUrl: baseUrlSetting?.trim() ? baseUrlSetting.trim() : BLOOIO_CONSTANTS.API_BASE_URL,
459
+ fromNumber: fromNumber?.trim() ? fromNumber.trim() : void 0,
460
+ signatureToleranceSeconds: resolvedSignatureTolerance
461
+ };
462
+ if (!this.blooioConfig.webhookSecret) {
463
+ logger3.warn(
464
+ "Blooio webhook secret not set; signature validation disabled"
465
+ );
466
+ }
467
+ await this.setupWebhookServer();
468
+ this.isInitialized = true;
469
+ logger3.info("BlooioService initialized successfully");
470
+ }
471
+ async stop() {
472
+ await this.cleanup();
473
+ }
474
+ get capabilityDescription() {
475
+ return "Blooio iMessage/SMS integration service for bidirectional messaging";
476
+ }
477
+ async setupWebhookServer() {
478
+ this.app = express();
479
+ this.app.use(
480
+ bodyParser.json({
481
+ verify: (req, _res, buf) => {
482
+ const typed = req;
483
+ typed.rawBody = buf.toString("utf8");
484
+ }
485
+ })
486
+ );
487
+ this.app.get("/health", (_req, res) => {
488
+ res.json({ status: "ok", service: "blooio" });
489
+ });
490
+ this.app.post(
491
+ this.blooioConfig.webhookPath,
492
+ async (req, res) => {
493
+ logger3.info(
494
+ {
495
+ path: req.path,
496
+ method: req.method,
497
+ headers: {
498
+ "X-Blooio-Event": req.header("X-Blooio-Event"),
499
+ "X-Blooio-Message-Id": req.header("X-Blooio-Message-Id"),
500
+ "Content-Type": req.header("Content-Type")
501
+ }
502
+ },
503
+ "Blooio webhook request received"
504
+ );
505
+ try {
506
+ const signatureHeader = req.header("X-Blooio-Signature") ?? "";
507
+ const eventHeader = req.header("X-Blooio-Event") ?? "";
508
+ const rawBody = typeof req.rawBody === "string" ? req.rawBody : "";
509
+ if (this.blooioConfig.webhookSecret) {
510
+ const valid = verifyWebhookSignature(
511
+ this.blooioConfig.webhookSecret,
512
+ signatureHeader,
513
+ rawBody,
514
+ this.blooioConfig.signatureToleranceSeconds
515
+ );
516
+ if (!valid) {
517
+ logger3.warn("Blooio webhook signature validation failed");
518
+ res.status(401).send(ERROR_MESSAGES.WEBHOOK_VALIDATION_FAILED);
519
+ return;
520
+ }
521
+ }
522
+ const payload = req.body;
523
+ if (!payload || typeof payload.event !== "string") {
524
+ logger3.warn({ body: req.body }, "Invalid webhook payload received");
525
+ res.status(400).send("Invalid webhook payload");
526
+ return;
527
+ }
528
+ logger3.info(
529
+ {
530
+ event: payload.event,
531
+ message_id: payload.message_id,
532
+ external_id: payload.external_id
533
+ },
534
+ "Processing Blooio webhook event"
535
+ );
536
+ if (eventHeader && payload.event !== eventHeader) {
537
+ logger3.warn(
538
+ { eventHeader, payloadEvent: payload.event },
539
+ "Blooio webhook event header mismatch"
540
+ );
541
+ }
542
+ await this.handleWebhookEvent(payload);
543
+ logger3.info(
544
+ { event: payload.event },
545
+ "Blooio webhook processed successfully"
546
+ );
547
+ res.sendStatus(200);
548
+ } catch (error) {
549
+ logger3.error(
550
+ { error: String(error) },
551
+ "Error handling Blooio webhook"
552
+ );
553
+ res.sendStatus(500);
554
+ }
555
+ }
556
+ );
557
+ this.server = this.app.listen(this.blooioConfig.webhookPort, () => {
558
+ logger3.info(
559
+ `Blooio webhook server listening on port ${this.blooioConfig.webhookPort} (${this.blooioConfig.webhookPath})`
560
+ );
561
+ });
562
+ }
563
+ async sendMessage(chatId, request) {
564
+ const normalizedChatId = chatId.split(",").map((part) => part.trim()).filter(Boolean).join(",");
565
+ if (!validateChatId(normalizedChatId)) {
566
+ throw new BlooioError(ERROR_MESSAGES.INVALID_CHAT_ID);
567
+ }
568
+ const url = `${this.blooioConfig.baseUrl}/chats/${encodeURIComponent(normalizedChatId)}/messages`;
569
+ const payload = {
570
+ text: request.text,
571
+ attachments: request.attachments,
572
+ metadata: request.metadata,
573
+ use_typing_indicator: request.use_typing_indicator,
574
+ fromNumber: request.fromNumber ?? this.blooioConfig.fromNumber
575
+ };
576
+ const cleanedPayload = Object.fromEntries(
577
+ Object.entries(payload).filter(([, value]) => value !== void 0)
578
+ );
579
+ const idempotencyKey = request.idempotencyKey?.trim();
580
+ const fromNumber = request.fromNumber ?? this.blooioConfig.fromNumber;
581
+ const response = await fetch(url, {
582
+ method: "POST",
583
+ headers: {
584
+ Authorization: `Bearer ${this.blooioConfig.apiKey}`,
585
+ "Content-Type": "application/json",
586
+ ...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {},
587
+ ...fromNumber ? { "X-From-Number": fromNumber } : {}
588
+ },
589
+ body: JSON.stringify(cleanedPayload)
590
+ });
591
+ const responseText = await response.text();
592
+ if (!response.ok) {
593
+ throw new BlooioError(
594
+ `Blooio API error (${response.status})`,
595
+ response.status,
596
+ responseText
597
+ );
598
+ }
599
+ let data;
600
+ try {
601
+ data = JSON.parse(responseText);
602
+ } catch (error) {
603
+ throw new BlooioError(
604
+ "Invalid JSON response from Blooio",
605
+ response.status
606
+ );
607
+ }
608
+ const messageId = data.message_id || (data.message_ids ? data.message_ids[0] : void 0);
609
+ const message = {
610
+ messageId: messageId ?? createUniqueUuid(this.runtime, `${normalizedChatId}:${Date.now()}`),
611
+ chatId: normalizedChatId,
612
+ sender: fromNumber ?? "blooio",
613
+ text: typeof request.text === "string" ? request.text : void 0,
614
+ attachments: Array.isArray(request.attachments) ? request.attachments.map(
615
+ (item) => typeof item === "string" ? item : item.url
616
+ ) : void 0,
617
+ direction: "outbound",
618
+ status: data.status,
619
+ timestamp: Date.now()
620
+ };
621
+ this.cacheMessage(normalizedChatId, message);
622
+ if (this.runtime) {
623
+ this.runtime.emitEvent("blooio:message:sent", message);
624
+ }
625
+ return data;
626
+ }
627
+ async handleWebhookEvent(event) {
628
+ switch (event.event) {
629
+ case "message.received":
630
+ await this.handleIncomingMessage(event);
631
+ break;
632
+ case "message.sent":
633
+ this.handleMessageSent(event);
634
+ break;
635
+ case "message.delivered":
636
+ this.handleMessageDelivered(event);
637
+ break;
638
+ case "message.failed":
639
+ this.handleMessageFailed(event);
640
+ break;
641
+ case "message.read":
642
+ this.handleMessageRead(event);
643
+ break;
644
+ case "group.name_changed":
645
+ case "group.icon_changed":
646
+ if (this.runtime) {
647
+ this.runtime.emitEvent(`blooio:${event.event}`, event);
648
+ }
649
+ break;
650
+ default:
651
+ break;
652
+ }
653
+ }
654
+ handleMessageSent(event) {
655
+ if (this.runtime) {
656
+ this.runtime.emitEvent("blooio:message:sent", event);
657
+ }
658
+ }
659
+ handleMessageDelivered(event) {
660
+ if (this.runtime) {
661
+ this.runtime.emitEvent("blooio:message:delivered", event);
662
+ }
663
+ }
664
+ handleMessageFailed(event) {
665
+ if (this.runtime) {
666
+ this.runtime.emitEvent("blooio:message:failed", event);
667
+ }
668
+ }
669
+ handleMessageRead(event) {
670
+ if (this.runtime) {
671
+ this.runtime.emitEvent("blooio:message:read", event);
672
+ }
673
+ }
674
+ async handleIncomingMessage(webhook) {
675
+ const chatId = webhook.external_id ?? webhook.sender;
676
+ if (!chatId) {
677
+ logger3.warn("Blooio webhook missing chat identifier");
678
+ return;
679
+ }
680
+ const inboundMessage = {
681
+ messageId: webhook.message_id ?? createUniqueUuid(this.runtime, `${chatId}:${Date.now()}`),
682
+ chatId,
683
+ sender: webhook.sender,
684
+ text: webhook.text,
685
+ attachments: this.normalizeAttachmentUrls(webhook.attachments),
686
+ direction: "inbound",
687
+ protocol: webhook.protocol,
688
+ timestamp: webhook.received_at ?? webhook.timestamp,
689
+ internalId: webhook.internal_id
690
+ };
691
+ this.cacheMessage(chatId, inboundMessage);
692
+ if (this.runtime) {
693
+ this.runtime.emitEvent("blooio:message:received", inboundMessage);
694
+ }
695
+ await this.processIncomingMessage(webhook, inboundMessage);
696
+ }
697
+ async processIncomingMessage(webhook, message) {
698
+ try {
699
+ const text = message.text?.trim();
700
+ const hasAttachments = message.attachments && message.attachments.length > 0;
701
+ if (!text && !hasAttachments) {
702
+ return;
703
+ }
704
+ const channelType = webhook.is_group ? ChannelType.GROUP : ChannelType.DM;
705
+ const entityId = createUniqueUuid(this.runtime, webhook.sender);
706
+ const roomId = createUniqueUuid(this.runtime, `blooio:${message.chatId}`);
707
+ const worldId = createUniqueUuid(
708
+ this.runtime,
709
+ `blooio:${webhook.internal_id ?? "unknown"}`
710
+ );
711
+ await this.runtime.ensureConnection({
712
+ entityId,
713
+ roomId,
714
+ worldId,
715
+ userName: webhook.sender,
716
+ source: "blooio",
717
+ channelId: message.chatId,
718
+ type: channelType
719
+ });
720
+ const attachments = this.buildMediaFromBlooioAttachments(
721
+ webhook.attachments
722
+ );
723
+ const memory = createMessageMemory({
724
+ id: stringToUuid(message.messageId),
725
+ entityId,
726
+ roomId,
727
+ content: {
728
+ text: text ?? "",
729
+ source: "blooio",
730
+ channelType,
731
+ chatId: message.chatId,
732
+ phoneNumber: isE164(message.chatId) ? message.chatId : void 0,
733
+ protocol: message.protocol,
734
+ attachments: attachments.length > 0 ? attachments : void 0
735
+ }
736
+ });
737
+ const callback = async (content) => {
738
+ const responseText = typeof content.text === "string" ? content.text.trim() : "";
739
+ const outboundAttachments = [];
740
+ if (content.attachments && Array.isArray(content.attachments)) {
741
+ for (const attachment of content.attachments) {
742
+ if (typeof attachment === "object" && attachment !== null) {
743
+ const media = attachment;
744
+ if (media.url) {
745
+ outboundAttachments.push({
746
+ url: media.url,
747
+ name: media.title ?? media.description ?? void 0
748
+ });
749
+ }
750
+ }
751
+ }
752
+ }
753
+ const urlsFromText = extractAttachmentUrls(responseText);
754
+ for (const url of urlsFromText) {
755
+ outboundAttachments.push(url);
756
+ }
757
+ if (!responseText && outboundAttachments.length === 0) {
758
+ return [];
759
+ }
760
+ await this.sendMessage(message.chatId, {
761
+ text: responseText || void 0,
762
+ attachments: outboundAttachments.length > 0 ? outboundAttachments : void 0,
763
+ fromNumber: this.blooioConfig.fromNumber ?? webhook.internal_id
764
+ });
765
+ return [];
766
+ };
767
+ const messageService = getMessageService(this.runtime);
768
+ if (messageService) {
769
+ await messageService.handleMessage(this.runtime, memory, callback);
770
+ } else {
771
+ logger3.warn("messageService unavailable; falling back to event emit");
772
+ await this.runtime.emitEvent(EventType.MESSAGE_RECEIVED, {
773
+ message: memory,
774
+ callback,
775
+ source: "blooio"
776
+ });
777
+ }
778
+ } catch (error) {
779
+ logger3.error({ error: String(error) }, "Error processing Blooio message");
780
+ }
781
+ }
782
+ buildMediaAttachments(attachments) {
783
+ if (!attachments || attachments.length === 0) {
784
+ return [];
785
+ }
786
+ return attachments.map((url) => ({
787
+ id: createUniqueUuid(this.runtime, url),
788
+ url,
789
+ contentType: this.resolveContentType(url)
790
+ }));
791
+ }
792
+ /**
793
+ * Build Media attachments from Blooio webhook attachments (preserves names)
794
+ */
795
+ buildMediaFromBlooioAttachments(attachments) {
796
+ if (!attachments || attachments.length === 0) {
797
+ return [];
798
+ }
799
+ return attachments.map((item) => {
800
+ const url = typeof item === "string" ? item : item.url;
801
+ const name = typeof item === "object" ? item.name : void 0;
802
+ return {
803
+ id: createUniqueUuid(this.runtime, url),
804
+ url,
805
+ title: name,
806
+ contentType: this.resolveContentType(url)
807
+ };
808
+ });
809
+ }
810
+ normalizeAttachmentUrls(attachments) {
811
+ if (!attachments || attachments.length === 0) {
812
+ return void 0;
813
+ }
814
+ return attachments.map((item) => typeof item === "string" ? item : item.url).filter((url) => typeof url === "string" && url.length > 0);
815
+ }
816
+ resolveContentType(url) {
817
+ const lower = url.toLowerCase();
818
+ if (lower.match(/\.(png|jpe?g|gif|webp|bmp|svg|ico|heic|heif|tiff?)$/)) {
819
+ return ContentType.IMAGE;
820
+ }
821
+ if (lower.match(/\.(mp4|mov|webm|avi|mkv|m4v|3gp|flv|wmv)$/)) {
822
+ return ContentType.VIDEO;
823
+ }
824
+ if (lower.match(/\.(mp3|wav|m4a|ogg|aac|flac|wma|aiff?)$/)) {
825
+ return ContentType.AUDIO;
826
+ }
827
+ return ContentType.DOCUMENT;
828
+ }
829
+ /**
830
+ * Resolve ContentType from a MIME type string (e.g., from webhook payload)
831
+ */
832
+ resolveContentTypeFromMime(mimeType) {
833
+ const lower = mimeType.toLowerCase();
834
+ if (lower.startsWith("image/")) {
835
+ return ContentType.IMAGE;
836
+ }
837
+ if (lower.startsWith("video/")) {
838
+ return ContentType.VIDEO;
839
+ }
840
+ if (lower.startsWith("audio/")) {
841
+ return ContentType.AUDIO;
842
+ }
843
+ return ContentType.DOCUMENT;
844
+ }
845
+ cacheMessage(chatId, message) {
846
+ const key = CACHE_KEYS.CONVERSATION(chatId);
847
+ const history = this.cache.get(key) ?? [];
848
+ history.push(message);
849
+ const trimmed = history.length > 50 ? history.slice(-50) : history;
850
+ this.cache.set(key, trimmed, BLOOIO_CONSTANTS.CACHE_TTL.CONVERSATION);
851
+ }
852
+ async cleanup() {
853
+ if (this.server) {
854
+ this.server.close();
855
+ }
856
+ this.cache.flushAll();
857
+ this.isInitialized = false;
858
+ logger3.info("BlooioService cleaned up");
859
+ }
860
+ get serviceType() {
861
+ return BLOOIO_SERVICE_NAME;
862
+ }
863
+ get serviceName() {
864
+ return "blooio";
865
+ }
866
+ get isConnected() {
867
+ return this.isInitialized;
868
+ }
869
+ getConversationHistory(chatId, limit = 10) {
870
+ const cacheKey = CACHE_KEYS.CONVERSATION(chatId);
871
+ const messages = this.cache.get(cacheKey);
872
+ if (!messages) {
873
+ return [];
874
+ }
875
+ return messages.slice(-limit);
876
+ }
877
+ get defaultFromNumber() {
878
+ return this.blooioConfig.fromNumber;
879
+ }
880
+ };
881
+
882
+ // src/tests.ts
883
+ import {
884
+ logger as logger4
885
+ } from "@elizaos/core";
886
+ var BlooioTestSuite = class {
887
+ name = "Blooio Plugin Test Suite";
888
+ description = "Tests for Blooio iMessage/SMS functionality";
889
+ tests = [
890
+ {
891
+ name: "Service Initialization Test",
892
+ fn: async (runtime) => {
893
+ const blooioService = runtime.getService(
894
+ BLOOIO_SERVICE_NAME
895
+ );
896
+ if (!blooioService) {
897
+ throw new Error("Blooio service not initialized");
898
+ }
899
+ if (!blooioService.isConnected) {
900
+ throw new Error("Blooio service is not connected");
901
+ }
902
+ logger4.info("\u2705 Blooio service initialized");
903
+ }
904
+ },
905
+ {
906
+ name: "Send Message Test",
907
+ fn: async (runtime) => {
908
+ const blooioService = runtime.getService(
909
+ BLOOIO_SERVICE_NAME
910
+ );
911
+ if (!blooioService) {
912
+ throw new Error("Blooio service not initialized");
913
+ }
914
+ const testChatId = runtime.getSetting("BLOOIO_TEST_CHAT_ID");
915
+ if (!testChatId) {
916
+ logger4.warn("BLOOIO_TEST_CHAT_ID not set, skipping send test");
917
+ return;
918
+ }
919
+ const result = await blooioService.sendMessage(testChatId, {
920
+ text: "Test message from Eliza Blooio plugin"
921
+ });
922
+ logger4.info(`\u2705 Message test queued. Status: ${result.status}`);
923
+ }
924
+ },
925
+ {
926
+ name: "Conversation History Test",
927
+ fn: async (runtime) => {
928
+ const blooioService = runtime.getService(
929
+ BLOOIO_SERVICE_NAME
930
+ );
931
+ if (!blooioService) {
932
+ throw new Error("Blooio service not initialized");
933
+ }
934
+ const testChatId = runtime.getSetting("BLOOIO_TEST_CHAT_ID");
935
+ if (!testChatId) {
936
+ logger4.warn("BLOOIO_TEST_CHAT_ID not set, skipping history test");
937
+ return;
938
+ }
939
+ await blooioService.sendMessage(testChatId, {
940
+ text: "History test message"
941
+ });
942
+ const history = blooioService.getConversationHistory(testChatId, 5);
943
+ if (history.length > 0) {
944
+ logger4.info(
945
+ `\u2705 Conversation history retrieved: ${history.length} messages`
946
+ );
947
+ logger4.info(
948
+ ` Latest message: ${history[history.length - 1].text ?? "(no text)"}`
949
+ );
950
+ } else {
951
+ logger4.info(
952
+ "\u2705 Conversation history is empty (expected for new chat)"
953
+ );
954
+ }
955
+ }
956
+ },
957
+ {
958
+ name: "Error Handling Test",
959
+ fn: async (runtime) => {
960
+ const blooioService = runtime.getService(
961
+ BLOOIO_SERVICE_NAME
962
+ );
963
+ if (!blooioService) {
964
+ throw new Error("Blooio service not initialized");
965
+ }
966
+ try {
967
+ await blooioService.sendMessage("invalid-chat-id", { text: "Test" });
968
+ throw new Error("Expected error for invalid chat id");
969
+ } catch (error) {
970
+ logger4.info("\u2705 Invalid chat id error handled correctly");
971
+ }
972
+ }
973
+ }
974
+ ];
975
+ };
976
+
977
+ // src/index.ts
978
+ var blooioPlugin = {
979
+ name: "blooio",
980
+ description: "Blooio plugin for iMessage/SMS messaging integration",
981
+ services: [BlooioService],
982
+ actions: [sendMessage_default],
983
+ providers: [conversationHistory_default],
984
+ tests: [new BlooioTestSuite()],
985
+ init: async (_config, runtime) => {
986
+ const apiKey = runtime.getSetting("BLOOIO_API_KEY");
987
+ const webhookUrl = runtime.getSetting("BLOOIO_WEBHOOK_URL");
988
+ const webhookSecret = runtime.getSetting("BLOOIO_WEBHOOK_SECRET");
989
+ if (!apiKey || apiKey.trim() === "") {
990
+ logger5.warn(
991
+ "Blooio API key not provided - Blooio plugin is loaded but will not be functional"
992
+ );
993
+ logger5.warn(
994
+ "To enable Blooio functionality, please provide BLOOIO_API_KEY in your .env file"
995
+ );
996
+ return;
997
+ }
998
+ if (!webhookUrl || webhookUrl.trim() === "") {
999
+ logger5.warn(
1000
+ "Blooio webhook URL not provided - Blooio will not receive incoming messages"
1001
+ );
1002
+ logger5.warn(
1003
+ "To enable incoming communication, please provide BLOOIO_WEBHOOK_URL in your .env file"
1004
+ );
1005
+ return;
1006
+ }
1007
+ if (!webhookSecret || webhookSecret.trim() === "") {
1008
+ logger5.warn(
1009
+ "Blooio webhook secret not provided - signature verification is disabled"
1010
+ );
1011
+ }
1012
+ logger5.info("Blooio plugin initialized successfully");
1013
+ }
1014
+ };
1015
+ var index_default = blooioPlugin;
1016
+ export {
1017
+ index_default as default
1018
+ };
1019
+ //# sourceMappingURL=index.js.map