@elizaos/plugin-bluebubbles 2.0.0-alpha.3
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/__tests__/integration.test.ts +260 -0
- package/build.ts +16 -0
- package/dist/index.js +46 -0
- package/package.json +33 -0
- package/src/actions/index.ts +5 -0
- package/src/actions/sendMessage.ts +175 -0
- package/src/actions/sendReaction.ts +186 -0
- package/src/client.ts +389 -0
- package/src/constants.ts +41 -0
- package/src/environment.ts +120 -0
- package/src/index.ts +68 -0
- package/src/providers/chatContext.ts +105 -0
- package/src/providers/chatState.ts +90 -0
- package/src/providers/index.ts +5 -0
- package/src/service.ts +502 -0
- package/src/types.ts +165 -0
- package/tsconfig.json +22 -0
package/src/service.ts
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlueBubbles service for ElizaOS
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
ChannelType,
|
|
6
|
+
type Content,
|
|
7
|
+
type ContentType,
|
|
8
|
+
createUniqueUuid,
|
|
9
|
+
type Entity,
|
|
10
|
+
type EventPayload,
|
|
11
|
+
EventType,
|
|
12
|
+
type IAgentRuntime,
|
|
13
|
+
logger,
|
|
14
|
+
Service,
|
|
15
|
+
type UUID,
|
|
16
|
+
} from "@elizaos/core";
|
|
17
|
+
import { BlueBubblesClient } from "./client";
|
|
18
|
+
import { BLUEBUBBLES_SERVICE_NAME, DEFAULT_WEBHOOK_PATH } from "./constants";
|
|
19
|
+
import {
|
|
20
|
+
getConfigFromRuntime,
|
|
21
|
+
isHandleAllowed,
|
|
22
|
+
normalizeHandle,
|
|
23
|
+
} from "./environment";
|
|
24
|
+
import type {
|
|
25
|
+
BlueBubblesChat,
|
|
26
|
+
BlueBubblesChatState,
|
|
27
|
+
BlueBubblesConfig,
|
|
28
|
+
BlueBubblesIncomingEvent,
|
|
29
|
+
BlueBubblesMessage,
|
|
30
|
+
BlueBubblesWebhookPayload,
|
|
31
|
+
} from "./types";
|
|
32
|
+
|
|
33
|
+
export class BlueBubblesService extends Service {
|
|
34
|
+
static serviceType = BLUEBUBBLES_SERVICE_NAME;
|
|
35
|
+
capabilityDescription =
|
|
36
|
+
"The agent is able to send and receive iMessages via BlueBubbles";
|
|
37
|
+
|
|
38
|
+
private client: BlueBubblesClient | null = null;
|
|
39
|
+
private blueBubblesConfig: BlueBubblesConfig | null = null;
|
|
40
|
+
private knownChats: Map<string, BlueBubblesChat> = new Map();
|
|
41
|
+
private entityCache: Map<string, UUID> = new Map();
|
|
42
|
+
private roomCache: Map<string, UUID> = new Map();
|
|
43
|
+
private webhookPath: string = DEFAULT_WEBHOOK_PATH;
|
|
44
|
+
private isRunning = false;
|
|
45
|
+
|
|
46
|
+
constructor(runtime?: IAgentRuntime) {
|
|
47
|
+
super(runtime);
|
|
48
|
+
if (!runtime) return;
|
|
49
|
+
this.blueBubblesConfig = getConfigFromRuntime(runtime);
|
|
50
|
+
|
|
51
|
+
if (!this.blueBubblesConfig) {
|
|
52
|
+
logger.warn(
|
|
53
|
+
"BlueBubbles configuration not provided - BlueBubbles functionality will be unavailable",
|
|
54
|
+
);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!this.blueBubblesConfig.enabled) {
|
|
59
|
+
logger.info("BlueBubbles plugin is disabled via configuration");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.webhookPath =
|
|
64
|
+
this.blueBubblesConfig.webhookPath ?? DEFAULT_WEBHOOK_PATH;
|
|
65
|
+
this.client = new BlueBubblesClient(this.blueBubblesConfig);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static async start(runtime: IAgentRuntime): Promise<BlueBubblesService> {
|
|
69
|
+
const service = new BlueBubblesService(runtime);
|
|
70
|
+
|
|
71
|
+
if (!service.client) {
|
|
72
|
+
logger.warn(
|
|
73
|
+
"BlueBubbles service started without client functionality - no configuration provided",
|
|
74
|
+
);
|
|
75
|
+
return service;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Probe the server to verify connectivity
|
|
80
|
+
const probeResult = await service.client.probe();
|
|
81
|
+
|
|
82
|
+
if (!probeResult.ok) {
|
|
83
|
+
logger.error(
|
|
84
|
+
`Failed to connect to BlueBubbles server: ${probeResult.error}`,
|
|
85
|
+
);
|
|
86
|
+
return service;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
logger.success(
|
|
90
|
+
`Connected to BlueBubbles server v${probeResult.serverVersion} on macOS ${probeResult.osVersion}`,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (probeResult.privateApiEnabled) {
|
|
94
|
+
logger.info(
|
|
95
|
+
"BlueBubbles Private API is enabled - edit and unsend features available",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Initialize known chats
|
|
100
|
+
await service.initializeChats();
|
|
101
|
+
|
|
102
|
+
service.isRunning = true;
|
|
103
|
+
logger.success(
|
|
104
|
+
`BlueBubbles service started for ${runtime.character.name}`,
|
|
105
|
+
);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
logger.error(
|
|
108
|
+
`Failed to start BlueBubbles service: ${error instanceof Error ? error.message : String(error)}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return service;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static async stopRuntime(runtime: IAgentRuntime): Promise<void> {
|
|
116
|
+
const service = runtime.getService<BlueBubblesService>(
|
|
117
|
+
BLUEBUBBLES_SERVICE_NAME,
|
|
118
|
+
);
|
|
119
|
+
if (service) {
|
|
120
|
+
await service.stop();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async stop(): Promise<void> {
|
|
125
|
+
this.isRunning = false;
|
|
126
|
+
logger.info("BlueBubbles service stopped");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Gets the BlueBubbles client
|
|
131
|
+
*/
|
|
132
|
+
getClient(): BlueBubblesClient | null {
|
|
133
|
+
return this.client;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Gets the current configuration
|
|
138
|
+
*/
|
|
139
|
+
getConfig(): BlueBubblesConfig | null {
|
|
140
|
+
return this.blueBubblesConfig;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Checks if the service is running
|
|
145
|
+
*/
|
|
146
|
+
getIsRunning(): boolean {
|
|
147
|
+
return this.isRunning;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Gets the webhook path for receiving messages
|
|
152
|
+
*/
|
|
153
|
+
getWebhookPath(): string {
|
|
154
|
+
return this.webhookPath;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Initializes known chats from the server
|
|
159
|
+
*/
|
|
160
|
+
private async initializeChats(): Promise<void> {
|
|
161
|
+
if (!this.client) return;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const chats = await this.client.listChats(100);
|
|
165
|
+
for (const chat of chats) {
|
|
166
|
+
this.knownChats.set(chat.guid, chat);
|
|
167
|
+
}
|
|
168
|
+
logger.info(`Loaded ${chats.length} BlueBubbles chats`);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
logger.error(
|
|
171
|
+
`Failed to load BlueBubbles chats: ${error instanceof Error ? error.message : String(error)}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Handles an incoming webhook payload
|
|
178
|
+
*/
|
|
179
|
+
async handleWebhook(payload: BlueBubblesWebhookPayload): Promise<void> {
|
|
180
|
+
if (!this.blueBubblesConfig || !this.client) {
|
|
181
|
+
logger.warn("Received webhook but BlueBubbles service is not configured");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const event: BlueBubblesIncomingEvent = {
|
|
186
|
+
type: payload.type as BlueBubblesIncomingEvent["type"],
|
|
187
|
+
data: payload.data,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
switch (event.type) {
|
|
191
|
+
case "new-message":
|
|
192
|
+
await this.handleIncomingMessage(event.data as BlueBubblesMessage);
|
|
193
|
+
break;
|
|
194
|
+
case "updated-message":
|
|
195
|
+
await this.handleMessageUpdate(event.data as BlueBubblesMessage);
|
|
196
|
+
break;
|
|
197
|
+
case "chat-updated":
|
|
198
|
+
await this.handleChatUpdate(event.data as BlueBubblesChat);
|
|
199
|
+
break;
|
|
200
|
+
case "typing-indicator":
|
|
201
|
+
case "read-receipt":
|
|
202
|
+
// These events can be logged but don't require action
|
|
203
|
+
logger.debug(
|
|
204
|
+
`BlueBubbles ${event.type}: ${JSON.stringify(event.data)}`,
|
|
205
|
+
);
|
|
206
|
+
break;
|
|
207
|
+
default:
|
|
208
|
+
logger.debug(`Unhandled BlueBubbles event: ${event.type}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Handles an incoming message
|
|
214
|
+
*/
|
|
215
|
+
private async handleIncomingMessage(
|
|
216
|
+
message: BlueBubblesMessage,
|
|
217
|
+
): Promise<void> {
|
|
218
|
+
// Skip outgoing messages
|
|
219
|
+
if (message.isFromMe) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Skip system messages
|
|
224
|
+
if (message.isSystemMessage) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!this.blueBubblesConfig) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const chat = message.chats[0];
|
|
233
|
+
if (!chat) {
|
|
234
|
+
logger.warn(`Received message without chat info: ${message.guid}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const isGroup = chat.participants.length > 1;
|
|
239
|
+
const senderHandle = message.handle?.address ?? "";
|
|
240
|
+
|
|
241
|
+
// Check access policies
|
|
242
|
+
if (isGroup) {
|
|
243
|
+
if (
|
|
244
|
+
!isHandleAllowed(
|
|
245
|
+
senderHandle,
|
|
246
|
+
this.blueBubblesConfig.groupAllowFrom ?? [],
|
|
247
|
+
this.blueBubblesConfig.groupPolicy ?? "allowlist",
|
|
248
|
+
)
|
|
249
|
+
) {
|
|
250
|
+
logger.debug(
|
|
251
|
+
`Ignoring message from ${senderHandle} - not in group allowlist`,
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
if (
|
|
257
|
+
!isHandleAllowed(
|
|
258
|
+
senderHandle,
|
|
259
|
+
this.blueBubblesConfig.allowFrom ?? [],
|
|
260
|
+
this.blueBubblesConfig.dmPolicy ?? "pairing",
|
|
261
|
+
)
|
|
262
|
+
) {
|
|
263
|
+
logger.debug(
|
|
264
|
+
`Ignoring message from ${senderHandle} - not in DM allowlist`,
|
|
265
|
+
);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Mark as read if configured
|
|
271
|
+
if (this.blueBubblesConfig.sendReadReceipts && this.client) {
|
|
272
|
+
try {
|
|
273
|
+
await this.client.markChatRead(chat.guid);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
logger.debug(`Failed to mark chat as read: ${error}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Create or get entity for sender
|
|
280
|
+
const entityId = await this.getOrCreateEntity(
|
|
281
|
+
senderHandle,
|
|
282
|
+
message.handle?.address,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Create or get room for chat
|
|
286
|
+
const roomId = await this.getOrCreateRoom(chat);
|
|
287
|
+
|
|
288
|
+
// Build content
|
|
289
|
+
const content: Content = {
|
|
290
|
+
text: message.text ?? "",
|
|
291
|
+
source: "bluebubbles",
|
|
292
|
+
inReplyTo: (message.threadOriginatorGuid ?? undefined) as
|
|
293
|
+
| UUID
|
|
294
|
+
| undefined,
|
|
295
|
+
attachments: message.attachments.map((att) => ({
|
|
296
|
+
id: att.guid,
|
|
297
|
+
url: `${this.blueBubblesConfig?.serverUrl}/api/v1/attachment/${encodeURIComponent(att.guid)}?password=${encodeURIComponent(this.blueBubblesConfig?.password ?? "")}`,
|
|
298
|
+
title: att.transferName,
|
|
299
|
+
description: att.mimeType ?? undefined,
|
|
300
|
+
contentType: (att.mimeType ??
|
|
301
|
+
"application/octet-stream") as ContentType,
|
|
302
|
+
})),
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Emit message event
|
|
306
|
+
if (this.runtime) {
|
|
307
|
+
this.runtime.emitEvent(EventType.MESSAGE_RECEIVED, {
|
|
308
|
+
runtime: this.runtime,
|
|
309
|
+
message: {
|
|
310
|
+
id: createUniqueUuid(this.runtime, message.guid) as UUID,
|
|
311
|
+
entityId,
|
|
312
|
+
roomId,
|
|
313
|
+
content,
|
|
314
|
+
createdAt: message.dateCreated,
|
|
315
|
+
},
|
|
316
|
+
source: "bluebubbles",
|
|
317
|
+
channelType: isGroup ? ChannelType.GROUP : ChannelType.DM,
|
|
318
|
+
} as EventPayload);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Handles a message update (edit, unsend, etc.)
|
|
324
|
+
*/
|
|
325
|
+
private async handleMessageUpdate(
|
|
326
|
+
message: BlueBubblesMessage,
|
|
327
|
+
): Promise<void> {
|
|
328
|
+
// Handle edited or unsent messages
|
|
329
|
+
if (message.dateEdited) {
|
|
330
|
+
logger.debug(`Message ${message.guid} was edited`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Handles a chat update
|
|
336
|
+
*/
|
|
337
|
+
private async handleChatUpdate(chat: BlueBubblesChat): Promise<void> {
|
|
338
|
+
this.knownChats.set(chat.guid, chat);
|
|
339
|
+
logger.debug(
|
|
340
|
+
`Chat ${chat.guid} updated: ${chat.displayName ?? chat.chatIdentifier}`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Gets or creates an entity for a BlueBubbles handle
|
|
346
|
+
*/
|
|
347
|
+
private async getOrCreateEntity(
|
|
348
|
+
handle: string,
|
|
349
|
+
displayName?: string,
|
|
350
|
+
): Promise<UUID> {
|
|
351
|
+
const normalized = normalizeHandle(handle);
|
|
352
|
+
const cached = this.entityCache.get(normalized);
|
|
353
|
+
if (cached) {
|
|
354
|
+
return cached;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const entityId = createUniqueUuid(
|
|
358
|
+
this.runtime,
|
|
359
|
+
`bluebubbles:${normalized}`,
|
|
360
|
+
) as UUID;
|
|
361
|
+
|
|
362
|
+
// Check if entity exists
|
|
363
|
+
const existing = await this.runtime.getEntityById(entityId);
|
|
364
|
+
if (!existing) {
|
|
365
|
+
const entity: Entity = {
|
|
366
|
+
id: entityId,
|
|
367
|
+
agentId: this.runtime.agentId,
|
|
368
|
+
names: displayName ? [displayName, normalized] : [normalized],
|
|
369
|
+
metadata: {
|
|
370
|
+
bluebubbles: {
|
|
371
|
+
handle: normalized,
|
|
372
|
+
displayName: displayName ?? normalized,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
await this.runtime.createEntity(entity);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this.entityCache.set(normalized, entityId);
|
|
380
|
+
return entityId;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Gets or creates a room for a BlueBubbles chat
|
|
385
|
+
*/
|
|
386
|
+
private async getOrCreateRoom(chat: BlueBubblesChat): Promise<UUID> {
|
|
387
|
+
const cached = this.roomCache.get(chat.guid);
|
|
388
|
+
if (cached) {
|
|
389
|
+
return cached;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const roomId = createUniqueUuid(
|
|
393
|
+
this.runtime,
|
|
394
|
+
`bluebubbles:${chat.guid}`,
|
|
395
|
+
) as UUID;
|
|
396
|
+
|
|
397
|
+
// Check if room exists
|
|
398
|
+
const existing = await this.runtime.getRoom(roomId);
|
|
399
|
+
if (!existing && this.runtime) {
|
|
400
|
+
const isGroup = chat.participants.length > 1;
|
|
401
|
+
await this.runtime.createRoom({
|
|
402
|
+
id: roomId,
|
|
403
|
+
name: chat.displayName ?? chat.chatIdentifier,
|
|
404
|
+
source: "bluebubbles",
|
|
405
|
+
type: isGroup ? ChannelType.GROUP : ChannelType.DM,
|
|
406
|
+
channelId: chat.guid,
|
|
407
|
+
worldId: this.runtime.agentId,
|
|
408
|
+
metadata: {
|
|
409
|
+
blueBubblesServerUrl: this.blueBubblesConfig?.serverUrl,
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
this.roomCache.set(chat.guid, roomId);
|
|
415
|
+
return roomId;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Sends a message to a target
|
|
420
|
+
*/
|
|
421
|
+
async sendMessage(
|
|
422
|
+
target: string,
|
|
423
|
+
text: string,
|
|
424
|
+
_replyToId?: string,
|
|
425
|
+
): Promise<{ guid: string }> {
|
|
426
|
+
if (!this.client) {
|
|
427
|
+
throw new Error("BlueBubbles client not initialized");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const chatGuid = await this.client.resolveTarget(target);
|
|
431
|
+
const result = await this.client.sendMessage(chatGuid, text, {
|
|
432
|
+
// If we have a replyToId, use it as threadOriginatorGuid
|
|
433
|
+
// BlueBubbles handles this through the message association
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
return { guid: result.guid };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Gets the state for a chat
|
|
441
|
+
*/
|
|
442
|
+
async getChatState(chatGuid: string): Promise<BlueBubblesChatState | null> {
|
|
443
|
+
const chat = this.knownChats.get(chatGuid);
|
|
444
|
+
if (!chat && this.client) {
|
|
445
|
+
try {
|
|
446
|
+
const fetchedChat = await this.client.getChat(chatGuid);
|
|
447
|
+
this.knownChats.set(chatGuid, fetchedChat);
|
|
448
|
+
return this.chatToState(fetchedChat);
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (!chat) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return this.chatToState(chat);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private chatToState(chat: BlueBubblesChat): BlueBubblesChatState {
|
|
462
|
+
return {
|
|
463
|
+
chatGuid: chat.guid,
|
|
464
|
+
chatIdentifier: chat.chatIdentifier,
|
|
465
|
+
isGroup: chat.participants.length > 1,
|
|
466
|
+
participants: chat.participants.map((p) => p.address),
|
|
467
|
+
displayName: chat.displayName,
|
|
468
|
+
lastMessageAt: chat.lastMessage?.dateCreated ?? null,
|
|
469
|
+
hasUnread: chat.hasUnreadMessages,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Checks if the service is connected
|
|
475
|
+
*/
|
|
476
|
+
isConnected(): boolean {
|
|
477
|
+
return this.isRunning && this.client !== null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Sends a reaction to a message
|
|
482
|
+
*/
|
|
483
|
+
async sendReaction(
|
|
484
|
+
chatGuid: string,
|
|
485
|
+
messageGuid: string,
|
|
486
|
+
reaction: string,
|
|
487
|
+
): Promise<{ success: boolean }> {
|
|
488
|
+
if (!this.client) {
|
|
489
|
+
throw new Error("BlueBubbles client not initialized");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
await this.client.reactToMessage(chatGuid, messageGuid, reaction);
|
|
494
|
+
return { success: true };
|
|
495
|
+
} catch (error) {
|
|
496
|
+
logger.error(
|
|
497
|
+
`Failed to send reaction: ${error instanceof Error ? error.message : String(error)}`,
|
|
498
|
+
);
|
|
499
|
+
return { success: false };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the BlueBubbles plugin
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type DmPolicy = "open" | "pairing" | "allowlist" | "disabled";
|
|
6
|
+
export type GroupPolicy = "open" | "allowlist" | "disabled";
|
|
7
|
+
|
|
8
|
+
export interface BlueBubblesConfig {
|
|
9
|
+
serverUrl: string;
|
|
10
|
+
password: string;
|
|
11
|
+
webhookPath?: string;
|
|
12
|
+
dmPolicy?: DmPolicy;
|
|
13
|
+
groupPolicy?: GroupPolicy;
|
|
14
|
+
allowFrom?: string[];
|
|
15
|
+
groupAllowFrom?: string[];
|
|
16
|
+
sendReadReceipts?: boolean;
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BlueBubblesMessage {
|
|
21
|
+
guid: string;
|
|
22
|
+
text: string | null;
|
|
23
|
+
subject: string | null;
|
|
24
|
+
country: string | null;
|
|
25
|
+
handle: BlueBubblesHandle | null;
|
|
26
|
+
handleId: number;
|
|
27
|
+
otherHandle: number;
|
|
28
|
+
chats: BlueBubblesChat[];
|
|
29
|
+
attachments: BlueBubblesAttachment[];
|
|
30
|
+
expressiveSendStyleId: string | null;
|
|
31
|
+
dateCreated: number;
|
|
32
|
+
dateRead: number | null;
|
|
33
|
+
dateDelivered: number | null;
|
|
34
|
+
isFromMe: boolean;
|
|
35
|
+
isDelayed: boolean;
|
|
36
|
+
isAutoReply: boolean;
|
|
37
|
+
isSystemMessage: boolean;
|
|
38
|
+
isServiceMessage: boolean;
|
|
39
|
+
isForward: boolean;
|
|
40
|
+
isArchived: boolean;
|
|
41
|
+
hasDdResults: boolean;
|
|
42
|
+
hasPayloadData: boolean;
|
|
43
|
+
threadOriginatorGuid: string | null;
|
|
44
|
+
threadOriginatorPart: string | null;
|
|
45
|
+
associatedMessageGuid: string | null;
|
|
46
|
+
associatedMessageType: string | null;
|
|
47
|
+
balloonBundleId: string | null;
|
|
48
|
+
dateEdited: number | null;
|
|
49
|
+
error: number;
|
|
50
|
+
itemType: number;
|
|
51
|
+
groupTitle: string | null;
|
|
52
|
+
groupActionType: number;
|
|
53
|
+
payloadData: Record<string, unknown> | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface BlueBubblesHandle {
|
|
57
|
+
address: string;
|
|
58
|
+
service: string;
|
|
59
|
+
country: string | null;
|
|
60
|
+
originalROWID: number;
|
|
61
|
+
uncanonicalizedId: string | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BlueBubblesChat {
|
|
65
|
+
guid: string;
|
|
66
|
+
chatIdentifier: string;
|
|
67
|
+
displayName: string | null;
|
|
68
|
+
participants: BlueBubblesHandle[];
|
|
69
|
+
lastMessage: BlueBubblesMessage | null;
|
|
70
|
+
style: number;
|
|
71
|
+
isArchived: boolean;
|
|
72
|
+
isFiltered: boolean;
|
|
73
|
+
isPinned: boolean;
|
|
74
|
+
hasUnreadMessages: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface BlueBubblesAttachment {
|
|
78
|
+
guid: string;
|
|
79
|
+
originalROWID: number;
|
|
80
|
+
uti: string;
|
|
81
|
+
mimeType: string | null;
|
|
82
|
+
transferName: string;
|
|
83
|
+
totalBytes: number;
|
|
84
|
+
isOutgoing: boolean;
|
|
85
|
+
hideAttachment: boolean;
|
|
86
|
+
isSticker: boolean;
|
|
87
|
+
hasLivePhoto: boolean;
|
|
88
|
+
height: number | null;
|
|
89
|
+
width: number | null;
|
|
90
|
+
metadata: Record<string, unknown> | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface BlueBubblesServerInfo {
|
|
94
|
+
os_version: string;
|
|
95
|
+
server_version: string;
|
|
96
|
+
private_api: boolean;
|
|
97
|
+
proxy_service: string | null;
|
|
98
|
+
helper_connected: boolean;
|
|
99
|
+
detected_icloud: string | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface BlueBubblesWebhookPayload {
|
|
103
|
+
type: string;
|
|
104
|
+
data: BlueBubblesMessage | BlueBubblesChat | Record<string, unknown>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface SendMessageOptions {
|
|
108
|
+
tempGuid?: string;
|
|
109
|
+
method?: "apple-script" | "private-api";
|
|
110
|
+
subject?: string;
|
|
111
|
+
effectId?: string;
|
|
112
|
+
partIndex?: number;
|
|
113
|
+
ddScan?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface SendMessageResult {
|
|
117
|
+
guid: string;
|
|
118
|
+
tempGuid?: string;
|
|
119
|
+
status: "sent" | "delivered" | "failed";
|
|
120
|
+
dateCreated: number;
|
|
121
|
+
text: string;
|
|
122
|
+
error?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface SendAttachmentOptions extends SendMessageOptions {
|
|
126
|
+
name?: string;
|
|
127
|
+
isAudioMessage?: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface BlueBubblesProbeResult {
|
|
131
|
+
ok: boolean;
|
|
132
|
+
serverVersion?: string;
|
|
133
|
+
osVersion?: string;
|
|
134
|
+
privateApiEnabled?: boolean;
|
|
135
|
+
helperConnected?: boolean;
|
|
136
|
+
error?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface BlueBubblesChatState {
|
|
140
|
+
chatGuid: string;
|
|
141
|
+
chatIdentifier: string;
|
|
142
|
+
isGroup: boolean;
|
|
143
|
+
participants: string[];
|
|
144
|
+
displayName: string | null;
|
|
145
|
+
lastMessageAt: number | null;
|
|
146
|
+
hasUnread: boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Event types for webhook processing
|
|
150
|
+
export type BlueBubblesEventType =
|
|
151
|
+
| "new-message"
|
|
152
|
+
| "updated-message"
|
|
153
|
+
| "typing-indicator"
|
|
154
|
+
| "read-receipt"
|
|
155
|
+
| "chat-updated"
|
|
156
|
+
| "participant-added"
|
|
157
|
+
| "participant-removed"
|
|
158
|
+
| "group-name-changed"
|
|
159
|
+
| "group-icon-changed"
|
|
160
|
+
| "group-icon-removed";
|
|
161
|
+
|
|
162
|
+
export interface BlueBubblesIncomingEvent {
|
|
163
|
+
type: BlueBubblesEventType;
|
|
164
|
+
data: BlueBubblesMessage | BlueBubblesChat | Record<string, unknown>;
|
|
165
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"outDir": "./dist",
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"declarationMap": true,
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"target": "ES2022",
|
|
9
|
+
"module": "ESNext",
|
|
10
|
+
"moduleResolution": "Bundler",
|
|
11
|
+
"lib": ["ES2022"],
|
|
12
|
+
"types": ["node", "bun"],
|
|
13
|
+
"strict": true,
|
|
14
|
+
"esModuleInterop": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"forceConsistentCasingInFileNames": true,
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"isolatedModules": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*"],
|
|
21
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
22
|
+
}
|