@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/chat.test.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import "./test-mocks.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
addBlueBubblesParticipant,
|
|
5
|
+
editBlueBubblesMessage,
|
|
6
|
+
leaveBlueBubblesChat,
|
|
7
|
+
markBlueBubblesChatRead,
|
|
8
|
+
removeBlueBubblesParticipant,
|
|
9
|
+
renameBlueBubblesChat,
|
|
10
|
+
sendBlueBubblesTyping,
|
|
11
|
+
setGroupIconBlueBubbles,
|
|
12
|
+
unsendBlueBubblesMessage,
|
|
13
|
+
} from "./chat.js";
|
|
4
14
|
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
|
5
15
|
import { installBlueBubblesFetchTestHooks } from "./test-harness.js";
|
|
6
16
|
|
|
@@ -12,6 +22,44 @@ installBlueBubblesFetchTestHooks({
|
|
|
12
22
|
});
|
|
13
23
|
|
|
14
24
|
describe("chat", () => {
|
|
25
|
+
function mockOkTextResponse() {
|
|
26
|
+
mockFetch.mockResolvedValueOnce({
|
|
27
|
+
ok: true,
|
|
28
|
+
text: () => Promise.resolve(""),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function expectCalledUrlIncludesPassword(params: {
|
|
33
|
+
password: string;
|
|
34
|
+
invoke: () => Promise<void>;
|
|
35
|
+
}) {
|
|
36
|
+
mockOkTextResponse();
|
|
37
|
+
await params.invoke();
|
|
38
|
+
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
39
|
+
expect(calledUrl).toContain(`password=${params.password}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function expectCalledUrlUsesConfigCredentials(params: {
|
|
43
|
+
serverHost: string;
|
|
44
|
+
password: string;
|
|
45
|
+
invoke: (cfg: {
|
|
46
|
+
channels: { bluebubbles: { serverUrl: string; password: string } };
|
|
47
|
+
}) => Promise<void>;
|
|
48
|
+
}) {
|
|
49
|
+
mockOkTextResponse();
|
|
50
|
+
await params.invoke({
|
|
51
|
+
channels: {
|
|
52
|
+
bluebubbles: {
|
|
53
|
+
serverUrl: `http://${params.serverHost}`,
|
|
54
|
+
password: params.password,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
59
|
+
expect(calledUrl).toContain(params.serverHost);
|
|
60
|
+
expect(calledUrl).toContain(`password=${params.password}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
15
63
|
describe("markBlueBubblesChatRead", () => {
|
|
16
64
|
it("does nothing when chatGuid is empty or whitespace", async () => {
|
|
17
65
|
for (const chatGuid of ["", " "]) {
|
|
@@ -63,18 +111,14 @@ describe("chat", () => {
|
|
|
63
111
|
});
|
|
64
112
|
|
|
65
113
|
it("includes password in URL query", async () => {
|
|
66
|
-
|
|
67
|
-
ok: true,
|
|
68
|
-
text: () => Promise.resolve(""),
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
await markBlueBubblesChatRead("chat-123", {
|
|
72
|
-
serverUrl: "http://localhost:1234",
|
|
114
|
+
await expectCalledUrlIncludesPassword({
|
|
73
115
|
password: "my-secret",
|
|
116
|
+
invoke: () =>
|
|
117
|
+
markBlueBubblesChatRead("chat-123", {
|
|
118
|
+
serverUrl: "http://localhost:1234",
|
|
119
|
+
password: "my-secret",
|
|
120
|
+
}),
|
|
74
121
|
});
|
|
75
|
-
|
|
76
|
-
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
77
|
-
expect(calledUrl).toContain("password=my-secret");
|
|
78
122
|
});
|
|
79
123
|
|
|
80
124
|
it("throws on non-ok response", async () => {
|
|
@@ -109,25 +153,14 @@ describe("chat", () => {
|
|
|
109
153
|
});
|
|
110
154
|
|
|
111
155
|
it("resolves credentials from config", async () => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
channels: {
|
|
120
|
-
bluebubbles: {
|
|
121
|
-
serverUrl: "http://config-server:9999",
|
|
122
|
-
password: "config-pass",
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
},
|
|
156
|
+
await expectCalledUrlUsesConfigCredentials({
|
|
157
|
+
serverHost: "config-server:9999",
|
|
158
|
+
password: "config-pass",
|
|
159
|
+
invoke: (cfg) =>
|
|
160
|
+
markBlueBubblesChatRead("chat-123", {
|
|
161
|
+
cfg,
|
|
162
|
+
}),
|
|
126
163
|
});
|
|
127
|
-
|
|
128
|
-
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
129
|
-
expect(calledUrl).toContain("config-server:9999");
|
|
130
|
-
expect(calledUrl).toContain("password=config-pass");
|
|
131
164
|
});
|
|
132
165
|
});
|
|
133
166
|
|
|
@@ -278,6 +311,188 @@ describe("chat", () => {
|
|
|
278
311
|
});
|
|
279
312
|
});
|
|
280
313
|
|
|
314
|
+
describe("editBlueBubblesMessage", () => {
|
|
315
|
+
it("throws when required args are missing", async () => {
|
|
316
|
+
await expect(editBlueBubblesMessage("", "updated", {})).rejects.toThrow("messageGuid");
|
|
317
|
+
await expect(editBlueBubblesMessage("message-guid", " ", {})).rejects.toThrow("newText");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("sends edit request with default payload values", async () => {
|
|
321
|
+
mockFetch.mockResolvedValueOnce({
|
|
322
|
+
ok: true,
|
|
323
|
+
text: () => Promise.resolve(""),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await editBlueBubblesMessage(" message-guid ", " updated text ", {
|
|
327
|
+
serverUrl: "http://localhost:1234",
|
|
328
|
+
password: "test-password",
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
332
|
+
expect.stringContaining("/api/v1/message/message-guid/edit"),
|
|
333
|
+
expect.objectContaining({
|
|
334
|
+
method: "POST",
|
|
335
|
+
headers: { "Content-Type": "application/json" },
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
338
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
339
|
+
expect(body).toEqual({
|
|
340
|
+
editedMessage: "updated text",
|
|
341
|
+
backwardsCompatibilityMessage: "Edited to: updated text",
|
|
342
|
+
partIndex: 0,
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("supports custom part index and backwards compatibility message", async () => {
|
|
347
|
+
mockFetch.mockResolvedValueOnce({
|
|
348
|
+
ok: true,
|
|
349
|
+
text: () => Promise.resolve(""),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
await editBlueBubblesMessage("message-guid", "new text", {
|
|
353
|
+
serverUrl: "http://localhost:1234",
|
|
354
|
+
password: "test-password",
|
|
355
|
+
partIndex: 3,
|
|
356
|
+
backwardsCompatMessage: "custom-backwards-message",
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
360
|
+
expect(body.partIndex).toBe(3);
|
|
361
|
+
expect(body.backwardsCompatibilityMessage).toBe("custom-backwards-message");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("throws on non-ok response", async () => {
|
|
365
|
+
mockFetch.mockResolvedValueOnce({
|
|
366
|
+
ok: false,
|
|
367
|
+
status: 422,
|
|
368
|
+
text: () => Promise.resolve("Unprocessable"),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
await expect(
|
|
372
|
+
editBlueBubblesMessage("message-guid", "new text", {
|
|
373
|
+
serverUrl: "http://localhost:1234",
|
|
374
|
+
password: "test-password",
|
|
375
|
+
}),
|
|
376
|
+
).rejects.toThrow("edit failed (422): Unprocessable");
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("unsendBlueBubblesMessage", () => {
|
|
381
|
+
it("throws when messageGuid is missing", async () => {
|
|
382
|
+
await expect(unsendBlueBubblesMessage("", {})).rejects.toThrow("messageGuid");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("sends unsend request with default part index", async () => {
|
|
386
|
+
mockFetch.mockResolvedValueOnce({
|
|
387
|
+
ok: true,
|
|
388
|
+
text: () => Promise.resolve(""),
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
await unsendBlueBubblesMessage(" msg-123 ", {
|
|
392
|
+
serverUrl: "http://localhost:1234",
|
|
393
|
+
password: "test-password",
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
397
|
+
expect.stringContaining("/api/v1/message/msg-123/unsend"),
|
|
398
|
+
expect.objectContaining({
|
|
399
|
+
method: "POST",
|
|
400
|
+
headers: { "Content-Type": "application/json" },
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
403
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
404
|
+
expect(body.partIndex).toBe(0);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("uses custom part index", async () => {
|
|
408
|
+
mockFetch.mockResolvedValueOnce({
|
|
409
|
+
ok: true,
|
|
410
|
+
text: () => Promise.resolve(""),
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
await unsendBlueBubblesMessage("msg-123", {
|
|
414
|
+
serverUrl: "http://localhost:1234",
|
|
415
|
+
password: "test-password",
|
|
416
|
+
partIndex: 2,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
420
|
+
expect(body.partIndex).toBe(2);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe("group chat mutation actions", () => {
|
|
425
|
+
it("renames chat", async () => {
|
|
426
|
+
mockFetch.mockResolvedValueOnce({
|
|
427
|
+
ok: true,
|
|
428
|
+
text: () => Promise.resolve(""),
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
await renameBlueBubblesChat(" chat-guid ", "New Group Name", {
|
|
432
|
+
serverUrl: "http://localhost:1234",
|
|
433
|
+
password: "test-password",
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
437
|
+
expect.stringContaining("/api/v1/chat/chat-guid"),
|
|
438
|
+
expect.objectContaining({ method: "PUT" }),
|
|
439
|
+
);
|
|
440
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
441
|
+
expect(body.displayName).toBe("New Group Name");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("adds and removes participant using matching endpoint", async () => {
|
|
445
|
+
mockFetch
|
|
446
|
+
.mockResolvedValueOnce({
|
|
447
|
+
ok: true,
|
|
448
|
+
text: () => Promise.resolve(""),
|
|
449
|
+
})
|
|
450
|
+
.mockResolvedValueOnce({
|
|
451
|
+
ok: true,
|
|
452
|
+
text: () => Promise.resolve(""),
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
await addBlueBubblesParticipant("chat-guid", "+15551234567", {
|
|
456
|
+
serverUrl: "http://localhost:1234",
|
|
457
|
+
password: "test-password",
|
|
458
|
+
});
|
|
459
|
+
await removeBlueBubblesParticipant("chat-guid", "+15551234567", {
|
|
460
|
+
serverUrl: "http://localhost:1234",
|
|
461
|
+
password: "test-password",
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
465
|
+
expect(mockFetch.mock.calls[0][0]).toContain("/api/v1/chat/chat-guid/participant");
|
|
466
|
+
expect(mockFetch.mock.calls[0][1].method).toBe("POST");
|
|
467
|
+
expect(mockFetch.mock.calls[1][0]).toContain("/api/v1/chat/chat-guid/participant");
|
|
468
|
+
expect(mockFetch.mock.calls[1][1].method).toBe("DELETE");
|
|
469
|
+
|
|
470
|
+
const addBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
471
|
+
const removeBody = JSON.parse(mockFetch.mock.calls[1][1].body);
|
|
472
|
+
expect(addBody.address).toBe("+15551234567");
|
|
473
|
+
expect(removeBody.address).toBe("+15551234567");
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("leaves chat without JSON body", async () => {
|
|
477
|
+
mockFetch.mockResolvedValueOnce({
|
|
478
|
+
ok: true,
|
|
479
|
+
text: () => Promise.resolve(""),
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
await leaveBlueBubblesChat("chat-guid", {
|
|
483
|
+
serverUrl: "http://localhost:1234",
|
|
484
|
+
password: "test-password",
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
488
|
+
expect.stringContaining("/api/v1/chat/chat-guid/leave"),
|
|
489
|
+
expect.objectContaining({ method: "POST" }),
|
|
490
|
+
);
|
|
491
|
+
expect(mockFetch.mock.calls[0][1].body).toBeUndefined();
|
|
492
|
+
expect(mockFetch.mock.calls[0][1].headers).toBeUndefined();
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
281
496
|
describe("setGroupIconBlueBubbles", () => {
|
|
282
497
|
it("throws when chatGuid is empty", async () => {
|
|
283
498
|
await expect(
|
|
@@ -344,18 +559,14 @@ describe("chat", () => {
|
|
|
344
559
|
});
|
|
345
560
|
|
|
346
561
|
it("includes password in URL query", async () => {
|
|
347
|
-
|
|
348
|
-
ok: true,
|
|
349
|
-
text: () => Promise.resolve(""),
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
await setGroupIconBlueBubbles("chat-123", new Uint8Array([1, 2, 3]), "icon.png", {
|
|
353
|
-
serverUrl: "http://localhost:1234",
|
|
562
|
+
await expectCalledUrlIncludesPassword({
|
|
354
563
|
password: "my-secret",
|
|
564
|
+
invoke: () =>
|
|
565
|
+
setGroupIconBlueBubbles("chat-123", new Uint8Array([1, 2, 3]), "icon.png", {
|
|
566
|
+
serverUrl: "http://localhost:1234",
|
|
567
|
+
password: "my-secret",
|
|
568
|
+
}),
|
|
355
569
|
});
|
|
356
|
-
|
|
357
|
-
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
358
|
-
expect(calledUrl).toContain("password=my-secret");
|
|
359
570
|
});
|
|
360
571
|
|
|
361
572
|
it("throws on non-ok response", async () => {
|
|
@@ -390,25 +601,14 @@ describe("chat", () => {
|
|
|
390
601
|
});
|
|
391
602
|
|
|
392
603
|
it("resolves credentials from config", async () => {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
channels: {
|
|
401
|
-
bluebubbles: {
|
|
402
|
-
serverUrl: "http://config-server:9999",
|
|
403
|
-
password: "config-pass",
|
|
404
|
-
},
|
|
405
|
-
},
|
|
406
|
-
},
|
|
604
|
+
await expectCalledUrlUsesConfigCredentials({
|
|
605
|
+
serverHost: "config-server:9999",
|
|
606
|
+
password: "config-pass",
|
|
607
|
+
invoke: (cfg) =>
|
|
608
|
+
setGroupIconBlueBubbles("chat-123", new Uint8Array([1]), "icon.png", {
|
|
609
|
+
cfg,
|
|
610
|
+
}),
|
|
407
611
|
});
|
|
408
|
-
|
|
409
|
-
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
410
|
-
expect(calledUrl).toContain("config-server:9999");
|
|
411
|
-
expect(calledUrl).toContain("password=config-pass");
|
|
412
612
|
});
|
|
413
613
|
|
|
414
614
|
it("includes filename in multipart body", async () => {
|
package/src/chat.ts
CHANGED
|
@@ -26,6 +26,41 @@ function assertPrivateApiEnabled(accountId: string, feature: string): void {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function resolvePartIndex(partIndex: number | undefined): number {
|
|
30
|
+
return typeof partIndex === "number" ? partIndex : 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function sendPrivateApiJsonRequest(params: {
|
|
34
|
+
opts: BlueBubblesChatOpts;
|
|
35
|
+
feature: string;
|
|
36
|
+
action: string;
|
|
37
|
+
path: string;
|
|
38
|
+
method: "POST" | "PUT" | "DELETE";
|
|
39
|
+
payload?: unknown;
|
|
40
|
+
}): Promise<void> {
|
|
41
|
+
const { baseUrl, password, accountId } = resolveAccount(params.opts);
|
|
42
|
+
assertPrivateApiEnabled(accountId, params.feature);
|
|
43
|
+
const url = buildBlueBubblesApiUrl({
|
|
44
|
+
baseUrl,
|
|
45
|
+
path: params.path,
|
|
46
|
+
password,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const request: RequestInit = { method: params.method };
|
|
50
|
+
if (params.payload !== undefined) {
|
|
51
|
+
request.headers = { "Content-Type": "application/json" };
|
|
52
|
+
request.body = JSON.stringify(params.payload);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const res = await blueBubblesFetchWithTimeout(url, request, params.opts.timeoutMs);
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const errorText = await res.text().catch(() => "");
|
|
58
|
+
throw new Error(
|
|
59
|
+
`BlueBubbles ${params.action} failed (${res.status}): ${errorText || "unknown"}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
29
64
|
export async function markBlueBubblesChatRead(
|
|
30
65
|
chatGuid: string,
|
|
31
66
|
opts: BlueBubblesChatOpts = {},
|
|
@@ -97,34 +132,18 @@ export async function editBlueBubblesMessage(
|
|
|
97
132
|
throw new Error("BlueBubbles edit requires newText");
|
|
98
133
|
}
|
|
99
134
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
135
|
+
await sendPrivateApiJsonRequest({
|
|
136
|
+
opts,
|
|
137
|
+
feature: "edit",
|
|
138
|
+
action: "edit",
|
|
139
|
+
method: "POST",
|
|
104
140
|
path: `/api/v1/message/${encodeURIComponent(trimmedGuid)}/edit`,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
editedMessage: trimmedText,
|
|
110
|
-
backwardsCompatibilityMessage: opts.backwardsCompatMessage ?? `Edited to: ${trimmedText}`,
|
|
111
|
-
partIndex: typeof opts.partIndex === "number" ? opts.partIndex : 0,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const res = await blueBubblesFetchWithTimeout(
|
|
115
|
-
url,
|
|
116
|
-
{
|
|
117
|
-
method: "POST",
|
|
118
|
-
headers: { "Content-Type": "application/json" },
|
|
119
|
-
body: JSON.stringify(payload),
|
|
141
|
+
payload: {
|
|
142
|
+
editedMessage: trimmedText,
|
|
143
|
+
backwardsCompatibilityMessage: opts.backwardsCompatMessage ?? `Edited to: ${trimmedText}`,
|
|
144
|
+
partIndex: resolvePartIndex(opts.partIndex),
|
|
120
145
|
},
|
|
121
|
-
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
if (!res.ok) {
|
|
125
|
-
const errorText = await res.text().catch(() => "");
|
|
126
|
-
throw new Error(`BlueBubbles edit failed (${res.status}): ${errorText || "unknown"}`);
|
|
127
|
-
}
|
|
146
|
+
});
|
|
128
147
|
}
|
|
129
148
|
|
|
130
149
|
/**
|
|
@@ -140,32 +159,14 @@ export async function unsendBlueBubblesMessage(
|
|
|
140
159
|
throw new Error("BlueBubbles unsend requires messageGuid");
|
|
141
160
|
}
|
|
142
161
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
162
|
+
await sendPrivateApiJsonRequest({
|
|
163
|
+
opts,
|
|
164
|
+
feature: "unsend",
|
|
165
|
+
action: "unsend",
|
|
166
|
+
method: "POST",
|
|
147
167
|
path: `/api/v1/message/${encodeURIComponent(trimmedGuid)}/unsend`,
|
|
148
|
-
|
|
168
|
+
payload: { partIndex: resolvePartIndex(opts.partIndex) },
|
|
149
169
|
});
|
|
150
|
-
|
|
151
|
-
const payload = {
|
|
152
|
-
partIndex: typeof opts.partIndex === "number" ? opts.partIndex : 0,
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const res = await blueBubblesFetchWithTimeout(
|
|
156
|
-
url,
|
|
157
|
-
{
|
|
158
|
-
method: "POST",
|
|
159
|
-
headers: { "Content-Type": "application/json" },
|
|
160
|
-
body: JSON.stringify(payload),
|
|
161
|
-
},
|
|
162
|
-
opts.timeoutMs,
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
if (!res.ok) {
|
|
166
|
-
const errorText = await res.text().catch(() => "");
|
|
167
|
-
throw new Error(`BlueBubbles unsend failed (${res.status}): ${errorText || "unknown"}`);
|
|
168
|
-
}
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
/**
|
|
@@ -181,28 +182,14 @@ export async function renameBlueBubblesChat(
|
|
|
181
182
|
throw new Error("BlueBubbles rename requires chatGuid");
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
await sendPrivateApiJsonRequest({
|
|
186
|
+
opts,
|
|
187
|
+
feature: "renameGroup",
|
|
188
|
+
action: "rename",
|
|
189
|
+
method: "PUT",
|
|
188
190
|
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}`,
|
|
189
|
-
|
|
191
|
+
payload: { displayName },
|
|
190
192
|
});
|
|
191
|
-
|
|
192
|
-
const res = await blueBubblesFetchWithTimeout(
|
|
193
|
-
url,
|
|
194
|
-
{
|
|
195
|
-
method: "PUT",
|
|
196
|
-
headers: { "Content-Type": "application/json" },
|
|
197
|
-
body: JSON.stringify({ displayName }),
|
|
198
|
-
},
|
|
199
|
-
opts.timeoutMs,
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
if (!res.ok) {
|
|
203
|
-
const errorText = await res.text().catch(() => "");
|
|
204
|
-
throw new Error(`BlueBubbles rename failed (${res.status}): ${errorText || "unknown"}`);
|
|
205
|
-
}
|
|
206
193
|
}
|
|
207
194
|
|
|
208
195
|
/**
|
|
@@ -222,28 +209,14 @@ export async function addBlueBubblesParticipant(
|
|
|
222
209
|
throw new Error("BlueBubbles addParticipant requires address");
|
|
223
210
|
}
|
|
224
211
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
212
|
+
await sendPrivateApiJsonRequest({
|
|
213
|
+
opts,
|
|
214
|
+
feature: "addParticipant",
|
|
215
|
+
action: "addParticipant",
|
|
216
|
+
method: "POST",
|
|
229
217
|
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}/participant`,
|
|
230
|
-
|
|
218
|
+
payload: { address: trimmedAddress },
|
|
231
219
|
});
|
|
232
|
-
|
|
233
|
-
const res = await blueBubblesFetchWithTimeout(
|
|
234
|
-
url,
|
|
235
|
-
{
|
|
236
|
-
method: "POST",
|
|
237
|
-
headers: { "Content-Type": "application/json" },
|
|
238
|
-
body: JSON.stringify({ address: trimmedAddress }),
|
|
239
|
-
},
|
|
240
|
-
opts.timeoutMs,
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
if (!res.ok) {
|
|
244
|
-
const errorText = await res.text().catch(() => "");
|
|
245
|
-
throw new Error(`BlueBubbles addParticipant failed (${res.status}): ${errorText || "unknown"}`);
|
|
246
|
-
}
|
|
247
220
|
}
|
|
248
221
|
|
|
249
222
|
/**
|
|
@@ -263,30 +236,14 @@ export async function removeBlueBubblesParticipant(
|
|
|
263
236
|
throw new Error("BlueBubbles removeParticipant requires address");
|
|
264
237
|
}
|
|
265
238
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
239
|
+
await sendPrivateApiJsonRequest({
|
|
240
|
+
opts,
|
|
241
|
+
feature: "removeParticipant",
|
|
242
|
+
action: "removeParticipant",
|
|
243
|
+
method: "DELETE",
|
|
270
244
|
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}/participant`,
|
|
271
|
-
|
|
245
|
+
payload: { address: trimmedAddress },
|
|
272
246
|
});
|
|
273
|
-
|
|
274
|
-
const res = await blueBubblesFetchWithTimeout(
|
|
275
|
-
url,
|
|
276
|
-
{
|
|
277
|
-
method: "DELETE",
|
|
278
|
-
headers: { "Content-Type": "application/json" },
|
|
279
|
-
body: JSON.stringify({ address: trimmedAddress }),
|
|
280
|
-
},
|
|
281
|
-
opts.timeoutMs,
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
if (!res.ok) {
|
|
285
|
-
const errorText = await res.text().catch(() => "");
|
|
286
|
-
throw new Error(
|
|
287
|
-
`BlueBubbles removeParticipant failed (${res.status}): ${errorText || "unknown"}`,
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
247
|
}
|
|
291
248
|
|
|
292
249
|
/**
|
|
@@ -301,20 +258,13 @@ export async function leaveBlueBubblesChat(
|
|
|
301
258
|
throw new Error("BlueBubbles leaveChat requires chatGuid");
|
|
302
259
|
}
|
|
303
260
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
261
|
+
await sendPrivateApiJsonRequest({
|
|
262
|
+
opts,
|
|
263
|
+
feature: "leaveGroup",
|
|
264
|
+
action: "leaveChat",
|
|
265
|
+
method: "POST",
|
|
308
266
|
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}/leave`,
|
|
309
|
-
password,
|
|
310
267
|
});
|
|
311
|
-
|
|
312
|
-
const res = await blueBubblesFetchWithTimeout(url, { method: "POST" }, opts.timeoutMs);
|
|
313
|
-
|
|
314
|
-
if (!res.ok) {
|
|
315
|
-
const errorText = await res.text().catch(() => "");
|
|
316
|
-
throw new Error(`BlueBubbles leaveChat failed (${res.status}): ${errorText || "unknown"}`);
|
|
317
|
-
}
|
|
318
268
|
}
|
|
319
269
|
|
|
320
270
|
/**
|
package/src/config-schema.ts
CHANGED
|
@@ -43,6 +43,7 @@ const bluebubblesAccountSchema = z
|
|
|
43
43
|
mediaMaxMb: z.number().int().positive().optional(),
|
|
44
44
|
mediaLocalRoots: z.array(z.string()).optional(),
|
|
45
45
|
sendReadReceipts: z.boolean().optional(),
|
|
46
|
+
allowPrivateNetwork: z.boolean().optional(),
|
|
46
47
|
blockStreaming: z.boolean().optional(),
|
|
47
48
|
groups: z.object({}).catchall(bluebubblesGroupConfigSchema).optional(),
|
|
48
49
|
})
|