@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/bluebubbles",
3
- "version": "2026.2.15",
3
+ "version": "2026.2.19",
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,61 +1,32 @@
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
- it("does nothing when chatGuid is empty", async () => {
37
- await markBlueBubblesChatRead("", {
38
- serverUrl: "http://localhost:1234",
39
- password: "test",
40
- });
41
- expect(mockFetch).not.toHaveBeenCalled();
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 serverUrl is missing", async () => {
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
- await sendBlueBubblesTyping("", true, {
166
- serverUrl: "http://localhost:1234",
167
- password: "test",
168
- });
169
- expect(mockFetch).not.toHaveBeenCalled();
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 serverUrl is missing", async () => {
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("sends typing start with POST method", async () => {
195
- mockFetch.mockResolvedValueOnce({
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).toHaveBeenCalledWith(
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("does not send typing when private API is disabled", async () => {
212
- vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValueOnce(false);
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).toHaveBeenCalledWith(
234
- expect.stringContaining("/api/v1/chat/iMessage%3B-%3B%2B15551234567/typing"),
235
- expect.objectContaining({ method: "DELETE" }),
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 serverUrl is missing", async () => {
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 { 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 {