@openclaw/bluebubbles 2026.2.21 → 2026.2.23
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 +7 -1
- package/src/actions.test.ts +29 -46
- package/src/actions.ts +2 -13
- package/src/attachments.test.ts +156 -32
- package/src/attachments.ts +49 -16
- package/src/chat.test.ts +257 -57
- package/src/chat.ts +74 -124
- package/src/config-schema.ts +1 -0
- package/src/history.ts +177 -0
- package/src/monitor-normalize.test.ts +78 -0
- package/src/monitor-normalize.ts +41 -12
- package/src/monitor-processing.ts +383 -127
- package/src/monitor-shared.ts +3 -13
- package/src/monitor.test.ts +396 -4
- package/src/onboarding.ts +24 -37
- package/src/probe.ts +8 -0
- package/src/reactions.test.ts +27 -47
- package/src/request-url.ts +12 -0
- package/src/runtime.ts +20 -0
- package/src/send.test.ts +72 -31
- package/src/send.ts +49 -7
- package/src/targets.test.ts +19 -0
- package/src/targets.ts +46 -37
- package/src/test-harness.ts +31 -2
- package/src/types.ts +2 -0
package/src/send.test.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
1
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
3
|
import "./test-mocks.js";
|
|
3
4
|
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
|
5
|
+
import { clearBlueBubblesRuntime, setBlueBubblesRuntime } from "./runtime.js";
|
|
4
6
|
import { sendMessageBlueBubbles, resolveChatGuidForTarget } from "./send.js";
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
BLUE_BUBBLES_PRIVATE_API_STATUS,
|
|
9
|
+
installBlueBubblesFetchTestHooks,
|
|
10
|
+
mockBlueBubblesPrivateApiStatusOnce,
|
|
11
|
+
} from "./test-harness.js";
|
|
6
12
|
import type { BlueBubblesSendTarget } from "./types.js";
|
|
7
13
|
|
|
8
14
|
const mockFetch = vi.fn();
|
|
15
|
+
const privateApiStatusMock = vi.mocked(getCachedBlueBubblesPrivateApiStatus);
|
|
9
16
|
|
|
10
17
|
installBlueBubblesFetchTestHooks({
|
|
11
18
|
mockFetch,
|
|
12
|
-
privateApiStatusMock
|
|
19
|
+
privateApiStatusMock,
|
|
13
20
|
});
|
|
14
21
|
|
|
15
22
|
function mockResolvedHandleTarget(
|
|
@@ -37,6 +44,23 @@ function mockSendResponse(body: unknown) {
|
|
|
37
44
|
});
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
function mockNewChatSendResponse(guid: string) {
|
|
48
|
+
mockFetch
|
|
49
|
+
.mockResolvedValueOnce({
|
|
50
|
+
ok: true,
|
|
51
|
+
json: () => Promise.resolve({ data: [] }),
|
|
52
|
+
})
|
|
53
|
+
.mockResolvedValueOnce({
|
|
54
|
+
ok: true,
|
|
55
|
+
text: () =>
|
|
56
|
+
Promise.resolve(
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
data: { guid },
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
40
64
|
describe("send", () => {
|
|
41
65
|
describe("resolveChatGuidForTarget", () => {
|
|
42
66
|
const resolveHandleTargetGuid = async (data: Array<Record<string, unknown>>) => {
|
|
@@ -446,20 +470,7 @@ describe("send", () => {
|
|
|
446
470
|
});
|
|
447
471
|
|
|
448
472
|
it("strips markdown when creating a new chat", async () => {
|
|
449
|
-
|
|
450
|
-
.mockResolvedValueOnce({
|
|
451
|
-
ok: true,
|
|
452
|
-
json: () => Promise.resolve({ data: [] }),
|
|
453
|
-
})
|
|
454
|
-
.mockResolvedValueOnce({
|
|
455
|
-
ok: true,
|
|
456
|
-
text: () =>
|
|
457
|
-
Promise.resolve(
|
|
458
|
-
JSON.stringify({
|
|
459
|
-
data: { guid: "new-msg-stripped" },
|
|
460
|
-
}),
|
|
461
|
-
),
|
|
462
|
-
});
|
|
473
|
+
mockNewChatSendResponse("new-msg-stripped");
|
|
463
474
|
|
|
464
475
|
const result = await sendMessageBlueBubbles("+15550009999", "**Welcome** to the _chat_!", {
|
|
465
476
|
serverUrl: "http://localhost:1234",
|
|
@@ -476,20 +487,7 @@ describe("send", () => {
|
|
|
476
487
|
});
|
|
477
488
|
|
|
478
489
|
it("creates a new chat when handle target is missing", async () => {
|
|
479
|
-
|
|
480
|
-
.mockResolvedValueOnce({
|
|
481
|
-
ok: true,
|
|
482
|
-
json: () => Promise.resolve({ data: [] }),
|
|
483
|
-
})
|
|
484
|
-
.mockResolvedValueOnce({
|
|
485
|
-
ok: true,
|
|
486
|
-
text: () =>
|
|
487
|
-
Promise.resolve(
|
|
488
|
-
JSON.stringify({
|
|
489
|
-
data: { guid: "new-msg-guid" },
|
|
490
|
-
}),
|
|
491
|
-
),
|
|
492
|
-
});
|
|
490
|
+
mockNewChatSendResponse("new-msg-guid");
|
|
493
491
|
|
|
494
492
|
const result = await sendMessageBlueBubbles("+15550009999", "Hello new chat", {
|
|
495
493
|
serverUrl: "http://localhost:1234",
|
|
@@ -527,6 +525,10 @@ describe("send", () => {
|
|
|
527
525
|
});
|
|
528
526
|
|
|
529
527
|
it("uses private-api when reply metadata is present", async () => {
|
|
528
|
+
mockBlueBubblesPrivateApiStatusOnce(
|
|
529
|
+
privateApiStatusMock,
|
|
530
|
+
BLUE_BUBBLES_PRIVATE_API_STATUS.enabled,
|
|
531
|
+
);
|
|
530
532
|
mockResolvedHandleTarget();
|
|
531
533
|
mockSendResponse({ data: { guid: "msg-uuid-124" } });
|
|
532
534
|
|
|
@@ -548,7 +550,10 @@ describe("send", () => {
|
|
|
548
550
|
});
|
|
549
551
|
|
|
550
552
|
it("downgrades threaded reply to plain send when private API is disabled", async () => {
|
|
551
|
-
|
|
553
|
+
mockBlueBubblesPrivateApiStatusOnce(
|
|
554
|
+
privateApiStatusMock,
|
|
555
|
+
BLUE_BUBBLES_PRIVATE_API_STATUS.disabled,
|
|
556
|
+
);
|
|
552
557
|
mockResolvedHandleTarget();
|
|
553
558
|
mockSendResponse({ data: { guid: "msg-uuid-plain" } });
|
|
554
559
|
|
|
@@ -568,6 +573,10 @@ describe("send", () => {
|
|
|
568
573
|
});
|
|
569
574
|
|
|
570
575
|
it("normalizes effect names and uses private-api for effects", async () => {
|
|
576
|
+
mockBlueBubblesPrivateApiStatusOnce(
|
|
577
|
+
privateApiStatusMock,
|
|
578
|
+
BLUE_BUBBLES_PRIVATE_API_STATUS.enabled,
|
|
579
|
+
);
|
|
571
580
|
mockResolvedHandleTarget();
|
|
572
581
|
mockSendResponse({ data: { guid: "msg-uuid-125" } });
|
|
573
582
|
|
|
@@ -586,6 +595,38 @@ describe("send", () => {
|
|
|
586
595
|
expect(body.effectId).toBe("com.apple.MobileSMS.expressivesend.invisibleink");
|
|
587
596
|
});
|
|
588
597
|
|
|
598
|
+
it("warns and downgrades private-api features when status is unknown", async () => {
|
|
599
|
+
const runtimeLog = vi.fn();
|
|
600
|
+
setBlueBubblesRuntime({ log: runtimeLog } as unknown as PluginRuntime);
|
|
601
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
602
|
+
mockResolvedHandleTarget();
|
|
603
|
+
mockSendResponse({ data: { guid: "msg-uuid-unknown" } });
|
|
604
|
+
|
|
605
|
+
try {
|
|
606
|
+
const result = await sendMessageBlueBubbles("+15551234567", "Reply fallback", {
|
|
607
|
+
serverUrl: "http://localhost:1234",
|
|
608
|
+
password: "test",
|
|
609
|
+
replyToMessageGuid: "reply-guid-123",
|
|
610
|
+
effectId: "invisible ink",
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
expect(result.messageId).toBe("msg-uuid-unknown");
|
|
614
|
+
expect(runtimeLog).toHaveBeenCalledTimes(1);
|
|
615
|
+
expect(runtimeLog.mock.calls[0]?.[0]).toContain("Private API status unknown");
|
|
616
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
617
|
+
|
|
618
|
+
const sendCall = mockFetch.mock.calls[1];
|
|
619
|
+
const body = JSON.parse(sendCall[1].body);
|
|
620
|
+
expect(body.method).toBeUndefined();
|
|
621
|
+
expect(body.selectedMessageGuid).toBeUndefined();
|
|
622
|
+
expect(body.partIndex).toBeUndefined();
|
|
623
|
+
expect(body.effectId).toBeUndefined();
|
|
624
|
+
} finally {
|
|
625
|
+
clearBlueBubblesRuntime();
|
|
626
|
+
warnSpy.mockRestore();
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
589
630
|
it("sends message with chat_guid target directly", async () => {
|
|
590
631
|
mockFetch.mockResolvedValueOnce({
|
|
591
632
|
ok: true,
|
package/src/send.ts
CHANGED
|
@@ -2,7 +2,11 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
3
3
|
import { stripMarkdown } from "openclaw/plugin-sdk";
|
|
4
4
|
import { resolveBlueBubblesAccount } from "./accounts.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getCachedBlueBubblesPrivateApiStatus,
|
|
7
|
+
isBlueBubblesPrivateApiStatusEnabled,
|
|
8
|
+
} from "./probe.js";
|
|
9
|
+
import { warnBlueBubbles } from "./runtime.js";
|
|
6
10
|
import { extractBlueBubblesMessageId, resolveBlueBubblesSendTarget } from "./send-helpers.js";
|
|
7
11
|
import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js";
|
|
8
12
|
import {
|
|
@@ -71,6 +75,38 @@ function resolveEffectId(raw?: string): string | undefined {
|
|
|
71
75
|
return raw;
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
type PrivateApiDecision = {
|
|
79
|
+
canUsePrivateApi: boolean;
|
|
80
|
+
throwEffectDisabledError: boolean;
|
|
81
|
+
warningMessage?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function resolvePrivateApiDecision(params: {
|
|
85
|
+
privateApiStatus: boolean | null;
|
|
86
|
+
wantsReplyThread: boolean;
|
|
87
|
+
wantsEffect: boolean;
|
|
88
|
+
}): PrivateApiDecision {
|
|
89
|
+
const { privateApiStatus, wantsReplyThread, wantsEffect } = params;
|
|
90
|
+
const needsPrivateApi = wantsReplyThread || wantsEffect;
|
|
91
|
+
const canUsePrivateApi =
|
|
92
|
+
needsPrivateApi && isBlueBubblesPrivateApiStatusEnabled(privateApiStatus);
|
|
93
|
+
const throwEffectDisabledError = wantsEffect && privateApiStatus === false;
|
|
94
|
+
if (!needsPrivateApi || privateApiStatus !== null) {
|
|
95
|
+
return { canUsePrivateApi, throwEffectDisabledError };
|
|
96
|
+
}
|
|
97
|
+
const requested = [
|
|
98
|
+
wantsReplyThread ? "reply threading" : null,
|
|
99
|
+
wantsEffect ? "message effects" : null,
|
|
100
|
+
]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join(" + ");
|
|
103
|
+
return {
|
|
104
|
+
canUsePrivateApi,
|
|
105
|
+
throwEffectDisabledError,
|
|
106
|
+
warningMessage: `Private API status unknown; sending without ${requested}. Run a status probe to restore private-api features.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
74
110
|
type BlueBubblesChatRecord = Record<string, unknown>;
|
|
75
111
|
|
|
76
112
|
function extractChatGuid(chat: BlueBubblesChatRecord): string | null {
|
|
@@ -372,30 +408,36 @@ export async function sendMessageBlueBubbles(
|
|
|
372
408
|
const effectId = resolveEffectId(opts.effectId);
|
|
373
409
|
const wantsReplyThread = Boolean(opts.replyToMessageGuid?.trim());
|
|
374
410
|
const wantsEffect = Boolean(effectId);
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
411
|
+
const privateApiDecision = resolvePrivateApiDecision({
|
|
412
|
+
privateApiStatus,
|
|
413
|
+
wantsReplyThread,
|
|
414
|
+
wantsEffect,
|
|
415
|
+
});
|
|
416
|
+
if (privateApiDecision.throwEffectDisabledError) {
|
|
378
417
|
throw new Error(
|
|
379
418
|
"BlueBubbles send failed: reply/effect requires Private API, but it is disabled on the BlueBubbles server.",
|
|
380
419
|
);
|
|
381
420
|
}
|
|
421
|
+
if (privateApiDecision.warningMessage) {
|
|
422
|
+
warnBlueBubbles(privateApiDecision.warningMessage);
|
|
423
|
+
}
|
|
382
424
|
const payload: Record<string, unknown> = {
|
|
383
425
|
chatGuid,
|
|
384
426
|
tempGuid: crypto.randomUUID(),
|
|
385
427
|
message: strippedText,
|
|
386
428
|
};
|
|
387
|
-
if (canUsePrivateApi) {
|
|
429
|
+
if (privateApiDecision.canUsePrivateApi) {
|
|
388
430
|
payload.method = "private-api";
|
|
389
431
|
}
|
|
390
432
|
|
|
391
433
|
// Add reply threading support
|
|
392
|
-
if (wantsReplyThread && canUsePrivateApi) {
|
|
434
|
+
if (wantsReplyThread && privateApiDecision.canUsePrivateApi) {
|
|
393
435
|
payload.selectedMessageGuid = opts.replyToMessageGuid;
|
|
394
436
|
payload.partIndex = typeof opts.replyToPartIndex === "number" ? opts.replyToPartIndex : 0;
|
|
395
437
|
}
|
|
396
438
|
|
|
397
439
|
// Add message effects support
|
|
398
|
-
if (effectId) {
|
|
440
|
+
if (effectId && privateApiDecision.canUsePrivateApi) {
|
|
399
441
|
payload.effectId = effectId;
|
|
400
442
|
}
|
|
401
443
|
|
package/src/targets.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
isAllowedBlueBubblesSender,
|
|
3
4
|
looksLikeBlueBubblesTargetId,
|
|
4
5
|
normalizeBlueBubblesMessagingTarget,
|
|
5
6
|
parseBlueBubblesTarget,
|
|
@@ -181,3 +182,21 @@ describe("parseBlueBubblesAllowTarget", () => {
|
|
|
181
182
|
});
|
|
182
183
|
});
|
|
183
184
|
});
|
|
185
|
+
|
|
186
|
+
describe("isAllowedBlueBubblesSender", () => {
|
|
187
|
+
it("denies when allowFrom is empty", () => {
|
|
188
|
+
const allowed = isAllowedBlueBubblesSender({
|
|
189
|
+
allowFrom: [],
|
|
190
|
+
sender: "+15551234567",
|
|
191
|
+
});
|
|
192
|
+
expect(allowed).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("allows wildcard entries", () => {
|
|
196
|
+
const allowed = isAllowedBlueBubblesSender({
|
|
197
|
+
allowFrom: ["*"],
|
|
198
|
+
sender: "+15551234567",
|
|
199
|
+
});
|
|
200
|
+
expect(allowed).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
});
|
package/src/targets.ts
CHANGED
|
@@ -78,6 +78,40 @@ function looksLikeRawChatIdentifier(value: string): boolean {
|
|
|
78
78
|
return CHAT_IDENTIFIER_UUID_RE.test(trimmed) || CHAT_IDENTIFIER_HEX_RE.test(trimmed);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function parseGroupTarget(params: {
|
|
82
|
+
trimmed: string;
|
|
83
|
+
lower: string;
|
|
84
|
+
requireValue: boolean;
|
|
85
|
+
}): { kind: "chat_id"; chatId: number } | { kind: "chat_guid"; chatGuid: string } | null {
|
|
86
|
+
if (!params.lower.startsWith("group:")) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const value = stripPrefix(params.trimmed, "group:");
|
|
90
|
+
const chatId = Number.parseInt(value, 10);
|
|
91
|
+
if (Number.isFinite(chatId)) {
|
|
92
|
+
return { kind: "chat_id", chatId };
|
|
93
|
+
}
|
|
94
|
+
if (value) {
|
|
95
|
+
return { kind: "chat_guid", chatGuid: value };
|
|
96
|
+
}
|
|
97
|
+
if (params.requireValue) {
|
|
98
|
+
throw new Error("group target is required");
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseRawChatIdentifierTarget(
|
|
104
|
+
trimmed: string,
|
|
105
|
+
): { kind: "chat_identifier"; chatIdentifier: string } | null {
|
|
106
|
+
if (/^chat\d+$/i.test(trimmed)) {
|
|
107
|
+
return { kind: "chat_identifier", chatIdentifier: trimmed };
|
|
108
|
+
}
|
|
109
|
+
if (looksLikeRawChatIdentifier(trimmed)) {
|
|
110
|
+
return { kind: "chat_identifier", chatIdentifier: trimmed };
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
81
115
|
export function normalizeBlueBubblesHandle(raw: string): string {
|
|
82
116
|
const trimmed = raw.trim();
|
|
83
117
|
if (!trimmed) {
|
|
@@ -239,16 +273,9 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget {
|
|
|
239
273
|
return chatTarget;
|
|
240
274
|
}
|
|
241
275
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (Number.isFinite(chatId)) {
|
|
246
|
-
return { kind: "chat_id", chatId };
|
|
247
|
-
}
|
|
248
|
-
if (!value) {
|
|
249
|
-
throw new Error("group target is required");
|
|
250
|
-
}
|
|
251
|
-
return { kind: "chat_guid", chatGuid: value };
|
|
276
|
+
const groupTarget = parseGroupTarget({ trimmed, lower, requireValue: true });
|
|
277
|
+
if (groupTarget) {
|
|
278
|
+
return groupTarget;
|
|
252
279
|
}
|
|
253
280
|
|
|
254
281
|
const rawChatGuid = parseRawChatGuid(trimmed);
|
|
@@ -256,15 +283,9 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget {
|
|
|
256
283
|
return { kind: "chat_guid", chatGuid: rawChatGuid };
|
|
257
284
|
}
|
|
258
285
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return { kind: "chat_identifier", chatIdentifier: trimmed };
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Handle UUID/hex chat identifiers (e.g., "8b9c1a10536d4d86a336ea03ab7151cc")
|
|
266
|
-
if (looksLikeRawChatIdentifier(trimmed)) {
|
|
267
|
-
return { kind: "chat_identifier", chatIdentifier: trimmed };
|
|
286
|
+
const rawChatIdentifierTarget = parseRawChatIdentifierTarget(trimmed);
|
|
287
|
+
if (rawChatIdentifierTarget) {
|
|
288
|
+
return rawChatIdentifierTarget;
|
|
268
289
|
}
|
|
269
290
|
|
|
270
291
|
return { kind: "handle", to: trimmed, service: "auto" };
|
|
@@ -298,26 +319,14 @@ export function parseBlueBubblesAllowTarget(raw: string): BlueBubblesAllowTarget
|
|
|
298
319
|
return chatTarget;
|
|
299
320
|
}
|
|
300
321
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (Number.isFinite(chatId)) {
|
|
305
|
-
return { kind: "chat_id", chatId };
|
|
306
|
-
}
|
|
307
|
-
if (value) {
|
|
308
|
-
return { kind: "chat_guid", chatGuid: value };
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Handle chat<digits> pattern (e.g., "chat660250192681427962") as chat_identifier
|
|
313
|
-
// These are BlueBubbles chat identifiers (the third part of a chat GUID), not numeric IDs
|
|
314
|
-
if (/^chat\d+$/i.test(trimmed)) {
|
|
315
|
-
return { kind: "chat_identifier", chatIdentifier: trimmed };
|
|
322
|
+
const groupTarget = parseGroupTarget({ trimmed, lower, requireValue: false });
|
|
323
|
+
if (groupTarget) {
|
|
324
|
+
return groupTarget;
|
|
316
325
|
}
|
|
317
326
|
|
|
318
|
-
|
|
319
|
-
if (
|
|
320
|
-
return
|
|
327
|
+
const rawChatIdentifierTarget = parseRawChatIdentifierTarget(trimmed);
|
|
328
|
+
if (rawChatIdentifierTarget) {
|
|
329
|
+
return rawChatIdentifierTarget;
|
|
321
330
|
}
|
|
322
331
|
|
|
323
332
|
return { kind: "handle", handle: normalizeBlueBubblesHandle(trimmed) };
|
package/src/test-harness.ts
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import type { Mock } from "vitest";
|
|
2
2
|
import { afterEach, beforeEach, vi } from "vitest";
|
|
3
3
|
|
|
4
|
+
export const BLUE_BUBBLES_PRIVATE_API_STATUS = {
|
|
5
|
+
enabled: true,
|
|
6
|
+
disabled: false,
|
|
7
|
+
unknown: null,
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
type BlueBubblesPrivateApiStatusMock = {
|
|
11
|
+
mockReturnValue: (value: boolean | null) => unknown;
|
|
12
|
+
mockReturnValueOnce: (value: boolean | null) => unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function mockBlueBubblesPrivateApiStatus(
|
|
16
|
+
mock: Pick<BlueBubblesPrivateApiStatusMock, "mockReturnValue">,
|
|
17
|
+
value: boolean | null,
|
|
18
|
+
) {
|
|
19
|
+
mock.mockReturnValue(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function mockBlueBubblesPrivateApiStatusOnce(
|
|
23
|
+
mock: Pick<BlueBubblesPrivateApiStatusMock, "mockReturnValueOnce">,
|
|
24
|
+
value: boolean | null,
|
|
25
|
+
) {
|
|
26
|
+
mock.mockReturnValueOnce(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
4
29
|
export function resolveBlueBubblesAccountFromConfig(params: {
|
|
5
30
|
cfg?: { channels?: { bluebubbles?: Record<string, unknown> } };
|
|
6
31
|
accountId?: string;
|
|
@@ -22,11 +47,15 @@ export function createBlueBubblesAccountsMockModule() {
|
|
|
22
47
|
|
|
23
48
|
type BlueBubblesProbeMockModule = {
|
|
24
49
|
getCachedBlueBubblesPrivateApiStatus: Mock<() => boolean | null>;
|
|
50
|
+
isBlueBubblesPrivateApiStatusEnabled: Mock<(status: boolean | null) => boolean>;
|
|
25
51
|
};
|
|
26
52
|
|
|
27
53
|
export function createBlueBubblesProbeMockModule(): BlueBubblesProbeMockModule {
|
|
28
54
|
return {
|
|
29
|
-
getCachedBlueBubblesPrivateApiStatus: vi
|
|
55
|
+
getCachedBlueBubblesPrivateApiStatus: vi
|
|
56
|
+
.fn()
|
|
57
|
+
.mockReturnValue(BLUE_BUBBLES_PRIVATE_API_STATUS.unknown),
|
|
58
|
+
isBlueBubblesPrivateApiStatusEnabled: vi.fn((status: boolean | null) => status === true),
|
|
30
59
|
};
|
|
31
60
|
}
|
|
32
61
|
|
|
@@ -41,7 +70,7 @@ export function installBlueBubblesFetchTestHooks(params: {
|
|
|
41
70
|
vi.stubGlobal("fetch", params.mockFetch);
|
|
42
71
|
params.mockFetch.mockReset();
|
|
43
72
|
params.privateApiStatusMock.mockReset();
|
|
44
|
-
params.privateApiStatusMock.mockReturnValue(
|
|
73
|
+
params.privateApiStatusMock.mockReturnValue(BLUE_BUBBLES_PRIVATE_API_STATUS.unknown);
|
|
45
74
|
});
|
|
46
75
|
|
|
47
76
|
afterEach(() => {
|
package/src/types.ts
CHANGED
|
@@ -53,6 +53,8 @@ export type BlueBubblesAccountConfig = {
|
|
|
53
53
|
mediaLocalRoots?: string[];
|
|
54
54
|
/** Send read receipts for incoming messages (default: true). */
|
|
55
55
|
sendReadReceipts?: boolean;
|
|
56
|
+
/** Allow fetching from private/internal IP addresses (e.g. localhost). Required for same-host BlueBubbles setups. */
|
|
57
|
+
allowPrivateNetwork?: boolean;
|
|
56
58
|
/** Per-group configuration keyed by chat GUID or identifier. */
|
|
57
59
|
groups?: Record<string, BlueBubblesGroupConfig>;
|
|
58
60
|
};
|