@openclaw/bluebubbles 2026.2.15 → 2026.2.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/bluebubbles",
3
- "version": "2026.2.15",
3
+ "version": "2026.2.17",
4
4
  "description": "OpenClaw BlueBubbles channel plugin",
5
5
  "type": "module",
6
6
  "devDependencies": {
@@ -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
+ }
@@ -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 = bluebubblesMessageActions.listActions({ cfg });
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 = bluebubblesMessageActions.listActions({ cfg });
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 = bluebubblesMessageActions.listActions({ cfg });
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 = bluebubblesMessageActions.listActions({ cfg });
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 = bluebubblesMessageActions.listActions({ cfg });
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(bluebubblesMessageActions.supportsAction({ action: "react" })).toBe(true);
140
+ expect(supportsAction({ action: "react" })).toBe(true);
134
141
  });
135
142
 
136
143
  it("returns true for all supported actions", () => {
137
- expect(bluebubblesMessageActions.supportsAction({ action: "edit" })).toBe(true);
138
- expect(bluebubblesMessageActions.supportsAction({ action: "unsend" })).toBe(true);
139
- expect(bluebubblesMessageActions.supportsAction({ action: "reply" })).toBe(true);
140
- expect(bluebubblesMessageActions.supportsAction({ action: "sendWithEffect" })).toBe(true);
141
- expect(bluebubblesMessageActions.supportsAction({ action: "renameGroup" })).toBe(true);
142
- expect(bluebubblesMessageActions.supportsAction({ action: "setGroupIcon" })).toBe(true);
143
- expect(bluebubblesMessageActions.supportsAction({ action: "addParticipant" })).toBe(true);
144
- expect(bluebubblesMessageActions.supportsAction({ action: "removeParticipant" })).toBe(true);
145
- expect(bluebubblesMessageActions.supportsAction({ action: "leaveGroup" })).toBe(true);
146
- expect(bluebubblesMessageActions.supportsAction({ action: "sendAttachment" })).toBe(true);
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(bluebubblesMessageActions.supportsAction({ action: "delete" })).toBe(false);
151
- expect(bluebubblesMessageActions.supportsAction({ action: "unknown" })).toBe(false);
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 = bluebubblesMessageActions.extractToolSend({
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 = bluebubblesMessageActions.extractToolSend({
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 = bluebubblesMessageActions.extractToolSend({
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
- bluebubblesMessageActions.handleAction({
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
- bluebubblesMessageActions.handleAction({
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
- bluebubblesMessageActions.handleAction({
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
- bluebubblesMessageActions.handleAction({
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
- bluebubblesMessageActions.handleAction({
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 bluebubblesMessageActions.handleAction({
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 bluebubblesMessageActions.handleAction({
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 bluebubblesMessageActions.handleAction({
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 bluebubblesMessageActions.handleAction({
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 bluebubblesMessageActions.handleAction({
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 bluebubblesMessageActions.handleAction({
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
- bluebubblesMessageActions.handleAction({
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 bluebubblesMessageActions.handleAction({
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 bluebubblesMessageActions.handleAction({
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 bluebubblesMessageActions.handleAction({
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
- bluebubblesMessageActions.handleAction({
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 bluebubblesMessageActions.handleAction({
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 bluebubblesMessageActions.handleAction({
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
 
@@ -1,38 +1,18 @@
1
- import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2
- import type { BlueBubblesAttachment } from "./types.js";
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
- vi.mock("./accounts.js", () => ({
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
- describe("downloadBlueBubblesAttachment", () => {
25
- beforeEach(() => {
26
- vi.stubGlobal("fetch", mockFetch);
27
- mockFetch.mockReset();
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(
@@ -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 { resolveBlueBubblesAccount } from "./accounts.js";
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
- const account = resolveBlueBubblesAccount({
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,37 +1,17 @@
1
- import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
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
- describe("chat", () => {
24
- beforeEach(() => {
25
- vi.stubGlobal("fetch", mockFetch);
26
- mockFetch.mockReset();
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
16
  it("does nothing when chatGuid is empty", async () => {
37
17
  await markBlueBubblesChatRead("", {
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 { resolveBlueBubblesAccount } from "./accounts.js";
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
- const account = resolveBlueBubblesAccount({
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 {
@@ -1,8 +1,8 @@
1
- import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
2
1
  import fs from "node:fs/promises";
3
2
  import os from "node:os";
4
3
  import path from "node:path";
5
4
  import { pathToFileURL } from "node:url";
5
+ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
6
6
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
7
  import { sendBlueBubblesMedia } from "./media-send.js";
8
8
  import { setBlueBubblesRuntime } from "./runtime.js";
@@ -1,5 +1,5 @@
1
- import type { BlueBubblesAttachment } from "./types.js";
2
1
  import { normalizeBlueBubblesHandle } from "./targets.js";
2
+ import type { BlueBubblesAttachment } from "./types.js";
3
3
 
4
4
  function asRecord(value: unknown): Record<string, unknown> | null {
5
5
  return value && typeof value === "object" && !Array.isArray(value)