@openclaw/bluebubbles 2026.2.15 → 2026.2.19
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/package.json +1 -1
- package/src/account-resolve.ts +29 -0
- package/src/actions.test.ts +47 -40
- package/src/actions.ts +1 -1
- package/src/attachments.test.ts +9 -29
- package/src/attachments.ts +3 -15
- package/src/chat.test.ts +46 -115
- package/src/chat.ts +3 -15
- package/src/media-send.test.ts +1 -1
- package/src/monitor-normalize.ts +1 -1
- package/src/monitor-processing.ts +204 -19
- package/src/monitor-shared.ts +1 -1
- package/src/monitor.test.ts +200 -14
- package/src/monitor.ts +14 -21
- package/src/onboarding.ts +2 -1
- package/src/reactions.ts +2 -14
- package/src/send-helpers.ts +1 -1
- package/src/send.test.ts +103 -320
- package/src/send.ts +1 -1
- package/src/targets.ts +10 -37
- package/src/test-harness.ts +50 -0
- package/src/test-mocks.ts +11 -0
package/package.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { resolveBlueBubblesAccount } from "./accounts.js";
|
|
3
|
+
|
|
4
|
+
export type BlueBubblesAccountResolveOpts = {
|
|
5
|
+
serverUrl?: string;
|
|
6
|
+
password?: string;
|
|
7
|
+
accountId?: string;
|
|
8
|
+
cfg?: OpenClawConfig;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function resolveBlueBubblesServerAccount(params: BlueBubblesAccountResolveOpts): {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
password: string;
|
|
14
|
+
accountId: string;
|
|
15
|
+
} {
|
|
16
|
+
const account = resolveBlueBubblesAccount({
|
|
17
|
+
cfg: params.cfg ?? {},
|
|
18
|
+
accountId: params.accountId,
|
|
19
|
+
});
|
|
20
|
+
const baseUrl = params.serverUrl?.trim() || account.config.serverUrl?.trim();
|
|
21
|
+
const password = params.password?.trim() || account.config.password?.trim();
|
|
22
|
+
if (!baseUrl) {
|
|
23
|
+
throw new Error("BlueBubbles serverUrl is required");
|
|
24
|
+
}
|
|
25
|
+
if (!password) {
|
|
26
|
+
throw new Error("BlueBubbles password is required");
|
|
27
|
+
}
|
|
28
|
+
return { baseUrl, password, accountId: account.accountId };
|
|
29
|
+
}
|
package/src/actions.test.ts
CHANGED
|
@@ -48,6 +48,13 @@ vi.mock("./probe.js", () => ({
|
|
|
48
48
|
}));
|
|
49
49
|
|
|
50
50
|
describe("bluebubblesMessageActions", () => {
|
|
51
|
+
const listActions = bluebubblesMessageActions.listActions!;
|
|
52
|
+
const supportsAction = bluebubblesMessageActions.supportsAction!;
|
|
53
|
+
const extractToolSend = bluebubblesMessageActions.extractToolSend!;
|
|
54
|
+
const handleAction = bluebubblesMessageActions.handleAction!;
|
|
55
|
+
const callHandleAction = (ctx: Omit<Parameters<typeof handleAction>[0], "channel">) =>
|
|
56
|
+
handleAction({ channel: "bluebubbles", ...ctx });
|
|
57
|
+
|
|
51
58
|
beforeEach(() => {
|
|
52
59
|
vi.clearAllMocks();
|
|
53
60
|
vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValue(null);
|
|
@@ -58,7 +65,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
58
65
|
const cfg: OpenClawConfig = {
|
|
59
66
|
channels: { bluebubbles: { enabled: false } },
|
|
60
67
|
};
|
|
61
|
-
const actions =
|
|
68
|
+
const actions = listActions({ cfg });
|
|
62
69
|
expect(actions).toEqual([]);
|
|
63
70
|
});
|
|
64
71
|
|
|
@@ -66,7 +73,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
66
73
|
const cfg: OpenClawConfig = {
|
|
67
74
|
channels: { bluebubbles: { enabled: true } },
|
|
68
75
|
};
|
|
69
|
-
const actions =
|
|
76
|
+
const actions = listActions({ cfg });
|
|
70
77
|
expect(actions).toEqual([]);
|
|
71
78
|
});
|
|
72
79
|
|
|
@@ -80,7 +87,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
80
87
|
},
|
|
81
88
|
},
|
|
82
89
|
};
|
|
83
|
-
const actions =
|
|
90
|
+
const actions = listActions({ cfg });
|
|
84
91
|
expect(actions).toContain("react");
|
|
85
92
|
});
|
|
86
93
|
|
|
@@ -95,7 +102,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
95
102
|
},
|
|
96
103
|
},
|
|
97
104
|
};
|
|
98
|
-
const actions =
|
|
105
|
+
const actions = listActions({ cfg });
|
|
99
106
|
expect(actions).not.toContain("react");
|
|
100
107
|
// Other actions should still be present
|
|
101
108
|
expect(actions).toContain("edit");
|
|
@@ -113,7 +120,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
113
120
|
},
|
|
114
121
|
},
|
|
115
122
|
};
|
|
116
|
-
const actions =
|
|
123
|
+
const actions = listActions({ cfg });
|
|
117
124
|
expect(actions).toContain("sendAttachment");
|
|
118
125
|
expect(actions).not.toContain("react");
|
|
119
126
|
expect(actions).not.toContain("reply");
|
|
@@ -130,31 +137,31 @@ describe("bluebubblesMessageActions", () => {
|
|
|
130
137
|
|
|
131
138
|
describe("supportsAction", () => {
|
|
132
139
|
it("returns true for react action", () => {
|
|
133
|
-
expect(
|
|
140
|
+
expect(supportsAction({ action: "react" })).toBe(true);
|
|
134
141
|
});
|
|
135
142
|
|
|
136
143
|
it("returns true for all supported actions", () => {
|
|
137
|
-
expect(
|
|
138
|
-
expect(
|
|
139
|
-
expect(
|
|
140
|
-
expect(
|
|
141
|
-
expect(
|
|
142
|
-
expect(
|
|
143
|
-
expect(
|
|
144
|
-
expect(
|
|
145
|
-
expect(
|
|
146
|
-
expect(
|
|
144
|
+
expect(supportsAction({ action: "edit" })).toBe(true);
|
|
145
|
+
expect(supportsAction({ action: "unsend" })).toBe(true);
|
|
146
|
+
expect(supportsAction({ action: "reply" })).toBe(true);
|
|
147
|
+
expect(supportsAction({ action: "sendWithEffect" })).toBe(true);
|
|
148
|
+
expect(supportsAction({ action: "renameGroup" })).toBe(true);
|
|
149
|
+
expect(supportsAction({ action: "setGroupIcon" })).toBe(true);
|
|
150
|
+
expect(supportsAction({ action: "addParticipant" })).toBe(true);
|
|
151
|
+
expect(supportsAction({ action: "removeParticipant" })).toBe(true);
|
|
152
|
+
expect(supportsAction({ action: "leaveGroup" })).toBe(true);
|
|
153
|
+
expect(supportsAction({ action: "sendAttachment" })).toBe(true);
|
|
147
154
|
});
|
|
148
155
|
|
|
149
156
|
it("returns false for unsupported actions", () => {
|
|
150
|
-
expect(
|
|
151
|
-
expect(
|
|
157
|
+
expect(supportsAction({ action: "delete" as never })).toBe(false);
|
|
158
|
+
expect(supportsAction({ action: "unknown" as never })).toBe(false);
|
|
152
159
|
});
|
|
153
160
|
});
|
|
154
161
|
|
|
155
162
|
describe("extractToolSend", () => {
|
|
156
163
|
it("extracts send params from sendMessage action", () => {
|
|
157
|
-
const result =
|
|
164
|
+
const result = extractToolSend({
|
|
158
165
|
args: {
|
|
159
166
|
action: "sendMessage",
|
|
160
167
|
to: "+15551234567",
|
|
@@ -168,14 +175,14 @@ describe("bluebubblesMessageActions", () => {
|
|
|
168
175
|
});
|
|
169
176
|
|
|
170
177
|
it("returns null for non-sendMessage action", () => {
|
|
171
|
-
const result =
|
|
178
|
+
const result = extractToolSend({
|
|
172
179
|
args: { action: "react", to: "+15551234567" },
|
|
173
180
|
});
|
|
174
181
|
expect(result).toBeNull();
|
|
175
182
|
});
|
|
176
183
|
|
|
177
184
|
it("returns null when to is missing", () => {
|
|
178
|
-
const result =
|
|
185
|
+
const result = extractToolSend({
|
|
179
186
|
args: { action: "sendMessage" },
|
|
180
187
|
});
|
|
181
188
|
expect(result).toBeNull();
|
|
@@ -193,8 +200,8 @@ describe("bluebubblesMessageActions", () => {
|
|
|
193
200
|
},
|
|
194
201
|
};
|
|
195
202
|
await expect(
|
|
196
|
-
|
|
197
|
-
action: "unknownAction",
|
|
203
|
+
callHandleAction({
|
|
204
|
+
action: "unknownAction" as never,
|
|
198
205
|
params: {},
|
|
199
206
|
cfg,
|
|
200
207
|
accountId: null,
|
|
@@ -212,7 +219,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
212
219
|
},
|
|
213
220
|
};
|
|
214
221
|
await expect(
|
|
215
|
-
|
|
222
|
+
callHandleAction({
|
|
216
223
|
action: "react",
|
|
217
224
|
params: { messageId: "msg-123" },
|
|
218
225
|
cfg,
|
|
@@ -232,7 +239,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
232
239
|
},
|
|
233
240
|
};
|
|
234
241
|
await expect(
|
|
235
|
-
|
|
242
|
+
callHandleAction({
|
|
236
243
|
action: "react",
|
|
237
244
|
params: { emoji: "❤️", messageId: "msg-123", chatGuid: "iMessage;-;+15551234567" },
|
|
238
245
|
cfg,
|
|
@@ -251,7 +258,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
251
258
|
},
|
|
252
259
|
};
|
|
253
260
|
await expect(
|
|
254
|
-
|
|
261
|
+
callHandleAction({
|
|
255
262
|
action: "react",
|
|
256
263
|
params: { emoji: "❤️" },
|
|
257
264
|
cfg,
|
|
@@ -273,7 +280,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
273
280
|
},
|
|
274
281
|
};
|
|
275
282
|
await expect(
|
|
276
|
-
|
|
283
|
+
callHandleAction({
|
|
277
284
|
action: "react",
|
|
278
285
|
params: { emoji: "❤️", messageId: "msg-123", to: "+15551234567" },
|
|
279
286
|
cfg,
|
|
@@ -293,7 +300,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
293
300
|
},
|
|
294
301
|
},
|
|
295
302
|
};
|
|
296
|
-
const result = await
|
|
303
|
+
const result = await callHandleAction({
|
|
297
304
|
action: "react",
|
|
298
305
|
params: {
|
|
299
306
|
emoji: "❤️",
|
|
@@ -328,7 +335,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
328
335
|
},
|
|
329
336
|
},
|
|
330
337
|
};
|
|
331
|
-
const result = await
|
|
338
|
+
const result = await callHandleAction({
|
|
332
339
|
action: "react",
|
|
333
340
|
params: {
|
|
334
341
|
emoji: "❤️",
|
|
@@ -364,7 +371,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
364
371
|
},
|
|
365
372
|
},
|
|
366
373
|
};
|
|
367
|
-
await
|
|
374
|
+
await callHandleAction({
|
|
368
375
|
action: "react",
|
|
369
376
|
params: {
|
|
370
377
|
emoji: "👍",
|
|
@@ -394,7 +401,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
394
401
|
},
|
|
395
402
|
},
|
|
396
403
|
};
|
|
397
|
-
await
|
|
404
|
+
await callHandleAction({
|
|
398
405
|
action: "react",
|
|
399
406
|
params: {
|
|
400
407
|
emoji: "😂",
|
|
@@ -426,7 +433,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
426
433
|
},
|
|
427
434
|
},
|
|
428
435
|
};
|
|
429
|
-
await
|
|
436
|
+
await callHandleAction({
|
|
430
437
|
action: "react",
|
|
431
438
|
params: {
|
|
432
439
|
emoji: "👍",
|
|
@@ -465,7 +472,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
465
472
|
},
|
|
466
473
|
};
|
|
467
474
|
|
|
468
|
-
await
|
|
475
|
+
await callHandleAction({
|
|
469
476
|
action: "react",
|
|
470
477
|
params: {
|
|
471
478
|
emoji: "❤️",
|
|
@@ -500,7 +507,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
500
507
|
};
|
|
501
508
|
|
|
502
509
|
await expect(
|
|
503
|
-
|
|
510
|
+
callHandleAction({
|
|
504
511
|
action: "react",
|
|
505
512
|
params: {
|
|
506
513
|
emoji: "❤️",
|
|
@@ -525,7 +532,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
525
532
|
},
|
|
526
533
|
};
|
|
527
534
|
|
|
528
|
-
await
|
|
535
|
+
await callHandleAction({
|
|
529
536
|
action: "edit",
|
|
530
537
|
params: { messageId: "msg-123", message: "updated" },
|
|
531
538
|
cfg,
|
|
@@ -551,7 +558,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
551
558
|
},
|
|
552
559
|
};
|
|
553
560
|
|
|
554
|
-
const result = await
|
|
561
|
+
const result = await callHandleAction({
|
|
555
562
|
action: "sendWithEffect",
|
|
556
563
|
params: {
|
|
557
564
|
message: "peekaboo",
|
|
@@ -586,7 +593,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
586
593
|
|
|
587
594
|
const base64Buffer = Buffer.from("voice").toString("base64");
|
|
588
595
|
|
|
589
|
-
await
|
|
596
|
+
await callHandleAction({
|
|
590
597
|
action: "sendAttachment",
|
|
591
598
|
params: {
|
|
592
599
|
to: "+15551234567",
|
|
@@ -619,7 +626,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
619
626
|
};
|
|
620
627
|
|
|
621
628
|
await expect(
|
|
622
|
-
|
|
629
|
+
callHandleAction({
|
|
623
630
|
action: "setGroupIcon",
|
|
624
631
|
params: { chatGuid: "iMessage;-;chat-guid" },
|
|
625
632
|
cfg,
|
|
@@ -644,7 +651,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
644
651
|
const testBuffer = Buffer.from("fake-image-data");
|
|
645
652
|
const base64Buffer = testBuffer.toString("base64");
|
|
646
653
|
|
|
647
|
-
const result = await
|
|
654
|
+
const result = await callHandleAction({
|
|
648
655
|
action: "setGroupIcon",
|
|
649
656
|
params: {
|
|
650
657
|
chatGuid: "iMessage;-;chat-guid",
|
|
@@ -681,7 +688,7 @@ describe("bluebubblesMessageActions", () => {
|
|
|
681
688
|
|
|
682
689
|
const base64Buffer = Buffer.from("test").toString("base64");
|
|
683
690
|
|
|
684
|
-
await
|
|
691
|
+
await callHandleAction({
|
|
685
692
|
action: "setGroupIcon",
|
|
686
693
|
params: {
|
|
687
694
|
chatGuid: "iMessage;-;chat-guid",
|
package/src/actions.ts
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
type ChannelMessageActionName,
|
|
11
11
|
type ChannelToolSend,
|
|
12
12
|
} from "openclaw/plugin-sdk";
|
|
13
|
-
import type { BlueBubblesSendTarget } from "./types.js";
|
|
14
13
|
import { resolveBlueBubblesAccount } from "./accounts.js";
|
|
15
14
|
import { sendBlueBubblesAttachment } from "./attachments.js";
|
|
16
15
|
import {
|
|
@@ -27,6 +26,7 @@ import { getCachedBlueBubblesPrivateApiStatus, isMacOS26OrHigher } from "./probe
|
|
|
27
26
|
import { sendBlueBubblesReaction } from "./reactions.js";
|
|
28
27
|
import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
|
|
29
28
|
import { normalizeBlueBubblesHandle, parseBlueBubblesTarget } from "./targets.js";
|
|
29
|
+
import type { BlueBubblesSendTarget } from "./types.js";
|
|
30
30
|
|
|
31
31
|
const providerId = "bluebubbles";
|
|
32
32
|
|
package/src/attachments.test.ts
CHANGED
|
@@ -1,38 +1,18 @@
|
|
|
1
|
-
import { describe, expect, it, vi
|
|
2
|
-
import
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import "./test-mocks.js";
|
|
3
3
|
import { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js";
|
|
4
4
|
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
resolveBlueBubblesAccount: vi.fn(({ cfg, accountId }) => {
|
|
8
|
-
const config = cfg?.channels?.bluebubbles ?? {};
|
|
9
|
-
return {
|
|
10
|
-
accountId: accountId ?? "default",
|
|
11
|
-
enabled: config.enabled !== false,
|
|
12
|
-
configured: Boolean(config.serverUrl && config.password),
|
|
13
|
-
config,
|
|
14
|
-
};
|
|
15
|
-
}),
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
vi.mock("./probe.js", () => ({
|
|
19
|
-
getCachedBlueBubblesPrivateApiStatus: vi.fn().mockReturnValue(null),
|
|
20
|
-
}));
|
|
5
|
+
import { installBlueBubblesFetchTestHooks } from "./test-harness.js";
|
|
6
|
+
import type { BlueBubblesAttachment } from "./types.js";
|
|
21
7
|
|
|
22
8
|
const mockFetch = vi.fn();
|
|
23
9
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReset();
|
|
29
|
-
vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValue(null);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
afterEach(() => {
|
|
33
|
-
vi.unstubAllGlobals();
|
|
34
|
-
});
|
|
10
|
+
installBlueBubblesFetchTestHooks({
|
|
11
|
+
mockFetch,
|
|
12
|
+
privateApiStatusMock: vi.mocked(getCachedBlueBubblesPrivateApiStatus),
|
|
13
|
+
});
|
|
35
14
|
|
|
15
|
+
describe("downloadBlueBubblesAttachment", () => {
|
|
36
16
|
it("throws when guid is missing", async () => {
|
|
37
17
|
const attachment: BlueBubblesAttachment = {};
|
|
38
18
|
await expect(
|
package/src/attachments.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
1
|
import crypto from "node:crypto";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import {
|
|
3
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
4
|
+
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
|
|
5
5
|
import { postMultipartFormData } from "./multipart.js";
|
|
6
6
|
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
|
7
7
|
import { extractBlueBubblesMessageId, resolveBlueBubblesSendTarget } from "./send-helpers.js";
|
|
@@ -54,19 +54,7 @@ function resolveVoiceInfo(filename: string, contentType?: string) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function resolveAccount(params: BlueBubblesAttachmentOpts) {
|
|
57
|
-
|
|
58
|
-
cfg: params.cfg ?? {},
|
|
59
|
-
accountId: params.accountId,
|
|
60
|
-
});
|
|
61
|
-
const baseUrl = params.serverUrl?.trim() || account.config.serverUrl?.trim();
|
|
62
|
-
const password = params.password?.trim() || account.config.password?.trim();
|
|
63
|
-
if (!baseUrl) {
|
|
64
|
-
throw new Error("BlueBubbles serverUrl is required");
|
|
65
|
-
}
|
|
66
|
-
if (!password) {
|
|
67
|
-
throw new Error("BlueBubbles password is required");
|
|
68
|
-
}
|
|
69
|
-
return { baseUrl, password, accountId: account.accountId };
|
|
57
|
+
return resolveBlueBubblesServerAccount(params);
|
|
70
58
|
}
|
|
71
59
|
|
|
72
60
|
export async function downloadBlueBubblesAttachment(
|
package/src/chat.test.ts
CHANGED
|
@@ -1,61 +1,32 @@
|
|
|
1
|
-
import { describe, expect, it, vi
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import "./test-mocks.js";
|
|
2
3
|
import { markBlueBubblesChatRead, sendBlueBubblesTyping, setGroupIconBlueBubbles } from "./chat.js";
|
|
3
4
|
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
|
4
|
-
|
|
5
|
-
vi.mock("./accounts.js", () => ({
|
|
6
|
-
resolveBlueBubblesAccount: vi.fn(({ cfg, accountId }) => {
|
|
7
|
-
const config = cfg?.channels?.bluebubbles ?? {};
|
|
8
|
-
return {
|
|
9
|
-
accountId: accountId ?? "default",
|
|
10
|
-
enabled: config.enabled !== false,
|
|
11
|
-
configured: Boolean(config.serverUrl && config.password),
|
|
12
|
-
config,
|
|
13
|
-
};
|
|
14
|
-
}),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
vi.mock("./probe.js", () => ({
|
|
18
|
-
getCachedBlueBubblesPrivateApiStatus: vi.fn().mockReturnValue(null),
|
|
19
|
-
}));
|
|
5
|
+
import { installBlueBubblesFetchTestHooks } from "./test-harness.js";
|
|
20
6
|
|
|
21
7
|
const mockFetch = vi.fn();
|
|
22
8
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReset();
|
|
28
|
-
vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValue(null);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
afterEach(() => {
|
|
32
|
-
vi.unstubAllGlobals();
|
|
33
|
-
});
|
|
9
|
+
installBlueBubblesFetchTestHooks({
|
|
10
|
+
mockFetch,
|
|
11
|
+
privateApiStatusMock: vi.mocked(getCachedBlueBubblesPrivateApiStatus),
|
|
12
|
+
});
|
|
34
13
|
|
|
14
|
+
describe("chat", () => {
|
|
35
15
|
describe("markBlueBubblesChatRead", () => {
|
|
36
|
-
it("does nothing when chatGuid is empty", async () => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
it("does nothing when chatGuid is whitespace", async () => {
|
|
45
|
-
await markBlueBubblesChatRead(" ", {
|
|
46
|
-
serverUrl: "http://localhost:1234",
|
|
47
|
-
password: "test",
|
|
48
|
-
});
|
|
16
|
+
it("does nothing when chatGuid is empty or whitespace", async () => {
|
|
17
|
+
for (const chatGuid of ["", " "]) {
|
|
18
|
+
await markBlueBubblesChatRead(chatGuid, {
|
|
19
|
+
serverUrl: "http://localhost:1234",
|
|
20
|
+
password: "test",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
49
23
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
50
24
|
});
|
|
51
25
|
|
|
52
|
-
it("throws when
|
|
26
|
+
it("throws when required credentials are missing", async () => {
|
|
53
27
|
await expect(markBlueBubblesChatRead("chat-guid", {})).rejects.toThrow(
|
|
54
28
|
"serverUrl is required",
|
|
55
29
|
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("throws when password is missing", async () => {
|
|
59
30
|
await expect(
|
|
60
31
|
markBlueBubblesChatRead("chat-guid", {
|
|
61
32
|
serverUrl: "http://localhost:1234",
|
|
@@ -161,29 +132,20 @@ describe("chat", () => {
|
|
|
161
132
|
});
|
|
162
133
|
|
|
163
134
|
describe("sendBlueBubblesTyping", () => {
|
|
164
|
-
it("does nothing when chatGuid is empty", async () => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
it("does nothing when chatGuid is whitespace", async () => {
|
|
173
|
-
await sendBlueBubblesTyping(" ", false, {
|
|
174
|
-
serverUrl: "http://localhost:1234",
|
|
175
|
-
password: "test",
|
|
176
|
-
});
|
|
135
|
+
it("does nothing when chatGuid is empty or whitespace", async () => {
|
|
136
|
+
for (const chatGuid of ["", " "]) {
|
|
137
|
+
await sendBlueBubblesTyping(chatGuid, true, {
|
|
138
|
+
serverUrl: "http://localhost:1234",
|
|
139
|
+
password: "test",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
177
142
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
178
143
|
});
|
|
179
144
|
|
|
180
|
-
it("throws when
|
|
145
|
+
it("throws when required credentials are missing", async () => {
|
|
181
146
|
await expect(sendBlueBubblesTyping("chat-guid", true, {})).rejects.toThrow(
|
|
182
147
|
"serverUrl is required",
|
|
183
148
|
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("throws when password is missing", async () => {
|
|
187
149
|
await expect(
|
|
188
150
|
sendBlueBubblesTyping("chat-guid", true, {
|
|
189
151
|
serverUrl: "http://localhost:1234",
|
|
@@ -191,49 +153,46 @@ describe("chat", () => {
|
|
|
191
153
|
).rejects.toThrow("password is required");
|
|
192
154
|
});
|
|
193
155
|
|
|
194
|
-
it("
|
|
195
|
-
|
|
196
|
-
ok: true,
|
|
197
|
-
text: () => Promise.resolve(""),
|
|
198
|
-
});
|
|
156
|
+
it("does not send typing when private API is disabled", async () => {
|
|
157
|
+
vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValueOnce(false);
|
|
199
158
|
|
|
200
159
|
await sendBlueBubblesTyping("iMessage;-;+15551234567", true, {
|
|
201
160
|
serverUrl: "http://localhost:1234",
|
|
202
161
|
password: "test",
|
|
203
162
|
});
|
|
204
163
|
|
|
205
|
-
expect(mockFetch).
|
|
206
|
-
expect.stringContaining("/api/v1/chat/iMessage%3B-%3B%2B15551234567/typing"),
|
|
207
|
-
expect.objectContaining({ method: "POST" }),
|
|
208
|
-
);
|
|
164
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
209
165
|
});
|
|
210
166
|
|
|
211
|
-
it("
|
|
212
|
-
|
|
167
|
+
it("uses POST for start and DELETE for stop", async () => {
|
|
168
|
+
mockFetch
|
|
169
|
+
.mockResolvedValueOnce({
|
|
170
|
+
ok: true,
|
|
171
|
+
text: () => Promise.resolve(""),
|
|
172
|
+
})
|
|
173
|
+
.mockResolvedValueOnce({
|
|
174
|
+
ok: true,
|
|
175
|
+
text: () => Promise.resolve(""),
|
|
176
|
+
});
|
|
213
177
|
|
|
214
178
|
await sendBlueBubblesTyping("iMessage;-;+15551234567", true, {
|
|
215
179
|
serverUrl: "http://localhost:1234",
|
|
216
180
|
password: "test",
|
|
217
181
|
});
|
|
218
|
-
|
|
219
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("sends typing stop with DELETE method", async () => {
|
|
223
|
-
mockFetch.mockResolvedValueOnce({
|
|
224
|
-
ok: true,
|
|
225
|
-
text: () => Promise.resolve(""),
|
|
226
|
-
});
|
|
227
|
-
|
|
228
182
|
await sendBlueBubblesTyping("iMessage;-;+15551234567", false, {
|
|
229
183
|
serverUrl: "http://localhost:1234",
|
|
230
184
|
password: "test",
|
|
231
185
|
});
|
|
232
186
|
|
|
233
|
-
expect(mockFetch).
|
|
234
|
-
|
|
235
|
-
|
|
187
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
188
|
+
expect(mockFetch.mock.calls[0][0]).toContain(
|
|
189
|
+
"/api/v1/chat/iMessage%3B-%3B%2B15551234567/typing",
|
|
190
|
+
);
|
|
191
|
+
expect(mockFetch.mock.calls[0][1].method).toBe("POST");
|
|
192
|
+
expect(mockFetch.mock.calls[1][0]).toContain(
|
|
193
|
+
"/api/v1/chat/iMessage%3B-%3B%2B15551234567/typing",
|
|
236
194
|
);
|
|
195
|
+
expect(mockFetch.mock.calls[1][1].method).toBe("DELETE");
|
|
237
196
|
});
|
|
238
197
|
|
|
239
198
|
it("includes password in URL query", async () => {
|
|
@@ -317,31 +276,6 @@ describe("chat", () => {
|
|
|
317
276
|
expect(calledUrl).toContain("typing-server:8888");
|
|
318
277
|
expect(calledUrl).toContain("password=typing-pass");
|
|
319
278
|
});
|
|
320
|
-
|
|
321
|
-
it("can start and stop typing in sequence", async () => {
|
|
322
|
-
mockFetch
|
|
323
|
-
.mockResolvedValueOnce({
|
|
324
|
-
ok: true,
|
|
325
|
-
text: () => Promise.resolve(""),
|
|
326
|
-
})
|
|
327
|
-
.mockResolvedValueOnce({
|
|
328
|
-
ok: true,
|
|
329
|
-
text: () => Promise.resolve(""),
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
await sendBlueBubblesTyping("chat-123", true, {
|
|
333
|
-
serverUrl: "http://localhost:1234",
|
|
334
|
-
password: "test",
|
|
335
|
-
});
|
|
336
|
-
await sendBlueBubblesTyping("chat-123", false, {
|
|
337
|
-
serverUrl: "http://localhost:1234",
|
|
338
|
-
password: "test",
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
342
|
-
expect(mockFetch.mock.calls[0][1].method).toBe("POST");
|
|
343
|
-
expect(mockFetch.mock.calls[1][1].method).toBe("DELETE");
|
|
344
|
-
});
|
|
345
279
|
});
|
|
346
280
|
|
|
347
281
|
describe("setGroupIconBlueBubbles", () => {
|
|
@@ -363,13 +297,10 @@ describe("chat", () => {
|
|
|
363
297
|
).rejects.toThrow("image buffer");
|
|
364
298
|
});
|
|
365
299
|
|
|
366
|
-
it("throws when
|
|
300
|
+
it("throws when required credentials are missing", async () => {
|
|
367
301
|
await expect(
|
|
368
302
|
setGroupIconBlueBubbles("chat-guid", new Uint8Array([1, 2, 3]), "icon.png", {}),
|
|
369
303
|
).rejects.toThrow("serverUrl is required");
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it("throws when password is missing", async () => {
|
|
373
304
|
await expect(
|
|
374
305
|
setGroupIconBlueBubbles("chat-guid", new Uint8Array([1, 2, 3]), "icon.png", {
|
|
375
306
|
serverUrl: "http://localhost:1234",
|
package/src/chat.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
1
|
import crypto from "node:crypto";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import {
|
|
3
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
4
|
+
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
|
|
5
5
|
import { postMultipartFormData } from "./multipart.js";
|
|
6
6
|
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
|
7
7
|
import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js";
|
|
@@ -15,19 +15,7 @@ export type BlueBubblesChatOpts = {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
function resolveAccount(params: BlueBubblesChatOpts) {
|
|
18
|
-
|
|
19
|
-
cfg: params.cfg ?? {},
|
|
20
|
-
accountId: params.accountId,
|
|
21
|
-
});
|
|
22
|
-
const baseUrl = params.serverUrl?.trim() || account.config.serverUrl?.trim();
|
|
23
|
-
const password = params.password?.trim() || account.config.password?.trim();
|
|
24
|
-
if (!baseUrl) {
|
|
25
|
-
throw new Error("BlueBubbles serverUrl is required");
|
|
26
|
-
}
|
|
27
|
-
if (!password) {
|
|
28
|
-
throw new Error("BlueBubbles password is required");
|
|
29
|
-
}
|
|
30
|
-
return { baseUrl, password, accountId: account.accountId };
|
|
18
|
+
return resolveBlueBubblesServerAccount(params);
|
|
31
19
|
}
|
|
32
20
|
|
|
33
21
|
function assertPrivateApiEnabled(accountId: string, feature: string): void {
|