@openclaw/bluebubbles 2026.3.2 → 2026.3.7
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/index.ts +2 -2
- package/package.json +4 -1
- package/src/account-resolve.ts +1 -1
- package/src/accounts.ts +7 -36
- package/src/actions.test.ts +1 -1
- package/src/actions.ts +1 -1
- package/src/attachments.test.ts +1 -1
- package/src/attachments.ts +1 -1
- package/src/channel.ts +50 -70
- package/src/chat.ts +46 -39
- package/src/config-apply.ts +77 -0
- package/src/config-schema.test.ts +1 -1
- package/src/config-schema.ts +1 -1
- package/src/history.ts +1 -1
- package/src/media-send.test.ts +1 -1
- package/src/media-send.ts +1 -1
- package/src/monitor-debounce.ts +1 -1
- package/src/monitor-normalize.ts +2 -11
- package/src/monitor-processing.ts +23 -22
- package/src/monitor-shared.ts +1 -1
- package/src/monitor.test.ts +3 -3
- package/src/monitor.ts +126 -138
- package/src/monitor.webhook-auth.test.ts +77 -172
- package/src/monitor.webhook-route.test.ts +1 -1
- package/src/onboarding.secret-input.test.ts +10 -2
- package/src/onboarding.ts +29 -67
- package/src/probe.ts +1 -1
- package/src/reactions.ts +1 -1
- package/src/request-url.ts +1 -12
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +8 -14
- package/src/send.test.ts +1 -1
- package/src/send.ts +17 -22
- package/src/targets.ts +1 -1
- package/src/types.ts +2 -2
package/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/bluebubbles";
|
|
3
3
|
import { bluebubblesPlugin } from "./src/channel.js";
|
|
4
4
|
import { setBlueBubblesRuntime } from "./src/runtime.js";
|
|
5
5
|
|
package/package.json
CHANGED
package/src/account-resolve.ts
CHANGED
package/src/accounts.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
DEFAULT_ACCOUNT_ID,
|
|
4
|
-
normalizeAccountId,
|
|
5
|
-
normalizeOptionalAccountId,
|
|
6
|
-
} from "openclaw/plugin-sdk/account-id";
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
|
+
import { createAccountListHelpers, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
7
3
|
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
|
|
8
4
|
import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js";
|
|
9
5
|
|
|
@@ -16,36 +12,11 @@ export type ResolvedBlueBubblesAccount = {
|
|
|
16
12
|
baseUrl?: string;
|
|
17
13
|
};
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return Object.keys(accounts).filter(Boolean);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function listBlueBubblesAccountIds(cfg: OpenClawConfig): string[] {
|
|
28
|
-
const ids = listConfiguredAccountIds(cfg);
|
|
29
|
-
if (ids.length === 0) {
|
|
30
|
-
return [DEFAULT_ACCOUNT_ID];
|
|
31
|
-
}
|
|
32
|
-
return ids.toSorted((a, b) => a.localeCompare(b));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function resolveDefaultBlueBubblesAccountId(cfg: OpenClawConfig): string {
|
|
36
|
-
const preferred = normalizeOptionalAccountId(cfg.channels?.bluebubbles?.defaultAccount);
|
|
37
|
-
if (
|
|
38
|
-
preferred &&
|
|
39
|
-
listBlueBubblesAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
|
40
|
-
) {
|
|
41
|
-
return preferred;
|
|
42
|
-
}
|
|
43
|
-
const ids = listBlueBubblesAccountIds(cfg);
|
|
44
|
-
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
45
|
-
return DEFAULT_ACCOUNT_ID;
|
|
46
|
-
}
|
|
47
|
-
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
48
|
-
}
|
|
15
|
+
const {
|
|
16
|
+
listAccountIds: listBlueBubblesAccountIds,
|
|
17
|
+
resolveDefaultAccountId: resolveDefaultBlueBubblesAccountId,
|
|
18
|
+
} = createAccountListHelpers("bluebubbles");
|
|
19
|
+
export { listBlueBubblesAccountIds, resolveDefaultBlueBubblesAccountId };
|
|
49
20
|
|
|
50
21
|
function resolveAccountConfig(
|
|
51
22
|
cfg: OpenClawConfig,
|
package/src/actions.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
2
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
3
3
|
import { bluebubblesMessageActions } from "./actions.js";
|
|
4
4
|
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
package/src/actions.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
readStringParam,
|
|
11
11
|
type ChannelMessageActionAdapter,
|
|
12
12
|
type ChannelMessageActionName,
|
|
13
|
-
} from "openclaw/plugin-sdk";
|
|
13
|
+
} from "openclaw/plugin-sdk/bluebubbles";
|
|
14
14
|
import { resolveBlueBubblesAccount } from "./accounts.js";
|
|
15
15
|
import { sendBlueBubblesAttachment } from "./attachments.js";
|
|
16
16
|
import {
|
package/src/attachments.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import "./test-mocks.js";
|
|
4
4
|
import { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js";
|
package/src/attachments.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
4
4
|
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
|
|
5
5
|
import { postMultipartFormData } from "./multipart.js";
|
|
6
6
|
import {
|
package/src/channel.ts
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ChannelAccountSnapshot,
|
|
3
|
+
ChannelPlugin,
|
|
4
|
+
OpenClawConfig,
|
|
5
|
+
} from "openclaw/plugin-sdk/bluebubbles";
|
|
2
6
|
import {
|
|
3
7
|
applyAccountNameToChannelSection,
|
|
4
8
|
buildChannelConfigSchema,
|
|
9
|
+
buildComputedAccountStatusSnapshot,
|
|
5
10
|
buildProbeChannelStatusSummary,
|
|
6
11
|
collectBlueBubblesStatusIssues,
|
|
7
12
|
DEFAULT_ACCOUNT_ID,
|
|
8
13
|
deleteAccountFromConfigSection,
|
|
9
|
-
formatPairingApproveHint,
|
|
10
14
|
migrateBaseNameToDefaultAccount,
|
|
11
15
|
normalizeAccountId,
|
|
12
16
|
PAIRING_APPROVED_MESSAGE,
|
|
13
17
|
resolveBlueBubblesGroupRequireMention,
|
|
14
18
|
resolveBlueBubblesGroupToolPolicy,
|
|
15
19
|
setAccountEnabledInConfigSection,
|
|
16
|
-
} from "openclaw/plugin-sdk";
|
|
20
|
+
} from "openclaw/plugin-sdk/bluebubbles";
|
|
21
|
+
import {
|
|
22
|
+
buildAccountScopedDmSecurityPolicy,
|
|
23
|
+
collectOpenGroupPolicyRestrictSendersWarnings,
|
|
24
|
+
formatNormalizedAllowFromEntries,
|
|
25
|
+
mapAllowFromEntries,
|
|
26
|
+
} from "openclaw/plugin-sdk/compat";
|
|
17
27
|
import {
|
|
18
28
|
listBlueBubblesAccountIds,
|
|
19
29
|
type ResolvedBlueBubblesAccount,
|
|
@@ -21,6 +31,7 @@ import {
|
|
|
21
31
|
resolveDefaultBlueBubblesAccountId,
|
|
22
32
|
} from "./accounts.js";
|
|
23
33
|
import { bluebubblesMessageActions } from "./actions.js";
|
|
34
|
+
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
|
|
24
35
|
import { BlueBubblesConfigSchema } from "./config-schema.js";
|
|
25
36
|
import { sendBlueBubblesMedia } from "./media-send.js";
|
|
26
37
|
import { resolveBlueBubblesMessageId } from "./monitor.js";
|
|
@@ -105,41 +116,37 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
|
|
105
116
|
baseUrl: account.baseUrl,
|
|
106
117
|
}),
|
|
107
118
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
108
|
-
(resolveBlueBubblesAccount({ cfg: cfg, accountId }).config.allowFrom
|
|
109
|
-
String(entry),
|
|
110
|
-
),
|
|
119
|
+
mapAllowFromEntries(resolveBlueBubblesAccount({ cfg: cfg, accountId }).config.allowFrom),
|
|
111
120
|
formatAllowFrom: ({ allowFrom }) =>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
.map((entry) => normalizeBlueBubblesHandle(entry)),
|
|
121
|
+
formatNormalizedAllowFromEntries({
|
|
122
|
+
allowFrom,
|
|
123
|
+
normalizeEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")),
|
|
124
|
+
}),
|
|
117
125
|
},
|
|
118
126
|
actions: bluebubblesMessageActions,
|
|
119
127
|
security: {
|
|
120
128
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
:
|
|
126
|
-
|
|
127
|
-
policy: account.config.dmPolicy ?? "pairing",
|
|
129
|
+
return buildAccountScopedDmSecurityPolicy({
|
|
130
|
+
cfg,
|
|
131
|
+
channelKey: "bluebubbles",
|
|
132
|
+
accountId,
|
|
133
|
+
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
134
|
+
policy: account.config.dmPolicy,
|
|
128
135
|
allowFrom: account.config.allowFrom ?? [],
|
|
129
|
-
|
|
130
|
-
allowFromPath: basePath,
|
|
131
|
-
approveHint: formatPairingApproveHint("bluebubbles"),
|
|
136
|
+
policyPathSuffix: "dmPolicy",
|
|
132
137
|
normalizeEntry: (raw) => normalizeBlueBubblesHandle(raw.replace(/^bluebubbles:/i, "")),
|
|
133
|
-
};
|
|
138
|
+
});
|
|
134
139
|
},
|
|
135
140
|
collectWarnings: ({ account }) => {
|
|
136
141
|
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
return collectOpenGroupPolicyRestrictSendersWarnings({
|
|
143
|
+
groupPolicy,
|
|
144
|
+
surface: "BlueBubbles groups",
|
|
145
|
+
openScope: "any member",
|
|
146
|
+
groupPolicyPath: "channels.bluebubbles.groupPolicy",
|
|
147
|
+
groupAllowFromPath: "channels.bluebubbles.groupAllowFrom",
|
|
148
|
+
mentionGated: false,
|
|
149
|
+
});
|
|
143
150
|
},
|
|
144
151
|
},
|
|
145
152
|
messaging: {
|
|
@@ -250,41 +257,16 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
|
|
250
257
|
channelKey: "bluebubbles",
|
|
251
258
|
})
|
|
252
259
|
: namedConfig;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
enabled: true,
|
|
261
|
-
...(input.httpUrl ? { serverUrl: input.httpUrl } : {}),
|
|
262
|
-
...(input.password ? { password: input.password } : {}),
|
|
263
|
-
...(input.webhookPath ? { webhookPath: input.webhookPath } : {}),
|
|
264
|
-
},
|
|
265
|
-
},
|
|
266
|
-
} as OpenClawConfig;
|
|
267
|
-
}
|
|
268
|
-
return {
|
|
269
|
-
...next,
|
|
270
|
-
channels: {
|
|
271
|
-
...next.channels,
|
|
272
|
-
bluebubbles: {
|
|
273
|
-
...next.channels?.bluebubbles,
|
|
274
|
-
enabled: true,
|
|
275
|
-
accounts: {
|
|
276
|
-
...next.channels?.bluebubbles?.accounts,
|
|
277
|
-
[accountId]: {
|
|
278
|
-
...next.channels?.bluebubbles?.accounts?.[accountId],
|
|
279
|
-
enabled: true,
|
|
280
|
-
...(input.httpUrl ? { serverUrl: input.httpUrl } : {}),
|
|
281
|
-
...(input.password ? { password: input.password } : {}),
|
|
282
|
-
...(input.webhookPath ? { webhookPath: input.webhookPath } : {}),
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
},
|
|
260
|
+
return applyBlueBubblesConnectionConfig({
|
|
261
|
+
cfg: next,
|
|
262
|
+
accountId,
|
|
263
|
+
patch: {
|
|
264
|
+
serverUrl: input.httpUrl,
|
|
265
|
+
password: input.password,
|
|
266
|
+
webhookPath: input.webhookPath,
|
|
286
267
|
},
|
|
287
|
-
|
|
268
|
+
onlyDefinedFields: true,
|
|
269
|
+
});
|
|
288
270
|
},
|
|
289
271
|
},
|
|
290
272
|
pairing: {
|
|
@@ -368,20 +350,18 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
|
|
368
350
|
buildAccountSnapshot: ({ account, runtime, probe }) => {
|
|
369
351
|
const running = runtime?.running ?? false;
|
|
370
352
|
const probeOk = (probe as BlueBubblesProbe | undefined)?.ok;
|
|
371
|
-
|
|
353
|
+
const base = buildComputedAccountStatusSnapshot({
|
|
372
354
|
accountId: account.accountId,
|
|
373
355
|
name: account.name,
|
|
374
356
|
enabled: account.enabled,
|
|
375
357
|
configured: account.configured,
|
|
358
|
+
runtime,
|
|
359
|
+
probe,
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
...base,
|
|
376
363
|
baseUrl: account.baseUrl,
|
|
377
|
-
running,
|
|
378
364
|
connected: probeOk ?? running,
|
|
379
|
-
lastStartAt: runtime?.lastStartAt ?? null,
|
|
380
|
-
lastStopAt: runtime?.lastStopAt ?? null,
|
|
381
|
-
lastError: runtime?.lastError ?? null,
|
|
382
|
-
probe,
|
|
383
|
-
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
384
|
-
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
385
365
|
};
|
|
386
366
|
},
|
|
387
367
|
},
|
package/src/chat.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
4
4
|
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
|
|
5
5
|
import { postMultipartFormData } from "./multipart.js";
|
|
6
6
|
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
|
@@ -30,6 +30,39 @@ function resolvePartIndex(partIndex: number | undefined): number {
|
|
|
30
30
|
return typeof partIndex === "number" ? partIndex : 0;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
async function sendBlueBubblesChatEndpointRequest(params: {
|
|
34
|
+
chatGuid: string;
|
|
35
|
+
opts: BlueBubblesChatOpts;
|
|
36
|
+
endpoint: "read" | "typing";
|
|
37
|
+
method: "POST" | "DELETE";
|
|
38
|
+
action: "read" | "typing";
|
|
39
|
+
}): Promise<void> {
|
|
40
|
+
const trimmed = params.chatGuid.trim();
|
|
41
|
+
if (!trimmed) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const { baseUrl, password, accountId } = resolveAccount(params.opts);
|
|
45
|
+
if (getCachedBlueBubblesPrivateApiStatus(accountId) === false) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const url = buildBlueBubblesApiUrl({
|
|
49
|
+
baseUrl,
|
|
50
|
+
path: `/api/v1/chat/${encodeURIComponent(trimmed)}/${params.endpoint}`,
|
|
51
|
+
password,
|
|
52
|
+
});
|
|
53
|
+
const res = await blueBubblesFetchWithTimeout(
|
|
54
|
+
url,
|
|
55
|
+
{ method: params.method },
|
|
56
|
+
params.opts.timeoutMs,
|
|
57
|
+
);
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
const errorText = await res.text().catch(() => "");
|
|
60
|
+
throw new Error(
|
|
61
|
+
`BlueBubbles ${params.action} failed (${res.status}): ${errorText || "unknown"}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
33
66
|
async function sendPrivateApiJsonRequest(params: {
|
|
34
67
|
opts: BlueBubblesChatOpts;
|
|
35
68
|
feature: string;
|
|
@@ -65,24 +98,13 @@ export async function markBlueBubblesChatRead(
|
|
|
65
98
|
chatGuid: string,
|
|
66
99
|
opts: BlueBubblesChatOpts = {},
|
|
67
100
|
): Promise<void> {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
const url = buildBlueBubblesApiUrl({
|
|
77
|
-
baseUrl,
|
|
78
|
-
path: `/api/v1/chat/${encodeURIComponent(trimmed)}/read`,
|
|
79
|
-
password,
|
|
101
|
+
await sendBlueBubblesChatEndpointRequest({
|
|
102
|
+
chatGuid,
|
|
103
|
+
opts,
|
|
104
|
+
endpoint: "read",
|
|
105
|
+
method: "POST",
|
|
106
|
+
action: "read",
|
|
80
107
|
});
|
|
81
|
-
const res = await blueBubblesFetchWithTimeout(url, { method: "POST" }, opts.timeoutMs);
|
|
82
|
-
if (!res.ok) {
|
|
83
|
-
const errorText = await res.text().catch(() => "");
|
|
84
|
-
throw new Error(`BlueBubbles read failed (${res.status}): ${errorText || "unknown"}`);
|
|
85
|
-
}
|
|
86
108
|
}
|
|
87
109
|
|
|
88
110
|
export async function sendBlueBubblesTyping(
|
|
@@ -90,28 +112,13 @@ export async function sendBlueBubblesTyping(
|
|
|
90
112
|
typing: boolean,
|
|
91
113
|
opts: BlueBubblesChatOpts = {},
|
|
92
114
|
): Promise<void> {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const url = buildBlueBubblesApiUrl({
|
|
102
|
-
baseUrl,
|
|
103
|
-
path: `/api/v1/chat/${encodeURIComponent(trimmed)}/typing`,
|
|
104
|
-
password,
|
|
115
|
+
await sendBlueBubblesChatEndpointRequest({
|
|
116
|
+
chatGuid,
|
|
117
|
+
opts,
|
|
118
|
+
endpoint: "typing",
|
|
119
|
+
method: typing ? "POST" : "DELETE",
|
|
120
|
+
action: "typing",
|
|
105
121
|
});
|
|
106
|
-
const res = await blueBubblesFetchWithTimeout(
|
|
107
|
-
url,
|
|
108
|
-
{ method: typing ? "POST" : "DELETE" },
|
|
109
|
-
opts.timeoutMs,
|
|
110
|
-
);
|
|
111
|
-
if (!res.ok) {
|
|
112
|
-
const errorText = await res.text().catch(() => "");
|
|
113
|
-
throw new Error(`BlueBubbles typing failed (${res.status}): ${errorText || "unknown"}`);
|
|
114
|
-
}
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
/**
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
|
+
|
|
3
|
+
type BlueBubblesConfigPatch = {
|
|
4
|
+
serverUrl?: string;
|
|
5
|
+
password?: unknown;
|
|
6
|
+
webhookPath?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type AccountEnabledMode = boolean | "preserve-or-true";
|
|
10
|
+
|
|
11
|
+
function normalizePatch(
|
|
12
|
+
patch: BlueBubblesConfigPatch,
|
|
13
|
+
onlyDefinedFields: boolean,
|
|
14
|
+
): BlueBubblesConfigPatch {
|
|
15
|
+
if (!onlyDefinedFields) {
|
|
16
|
+
return patch;
|
|
17
|
+
}
|
|
18
|
+
const next: BlueBubblesConfigPatch = {};
|
|
19
|
+
if (patch.serverUrl !== undefined) {
|
|
20
|
+
next.serverUrl = patch.serverUrl;
|
|
21
|
+
}
|
|
22
|
+
if (patch.password !== undefined) {
|
|
23
|
+
next.password = patch.password;
|
|
24
|
+
}
|
|
25
|
+
if (patch.webhookPath !== undefined) {
|
|
26
|
+
next.webhookPath = patch.webhookPath;
|
|
27
|
+
}
|
|
28
|
+
return next;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function applyBlueBubblesConnectionConfig(params: {
|
|
32
|
+
cfg: OpenClawConfig;
|
|
33
|
+
accountId: string;
|
|
34
|
+
patch: BlueBubblesConfigPatch;
|
|
35
|
+
onlyDefinedFields?: boolean;
|
|
36
|
+
accountEnabled?: AccountEnabledMode;
|
|
37
|
+
}): OpenClawConfig {
|
|
38
|
+
const patch = normalizePatch(params.patch, params.onlyDefinedFields === true);
|
|
39
|
+
if (params.accountId === DEFAULT_ACCOUNT_ID) {
|
|
40
|
+
return {
|
|
41
|
+
...params.cfg,
|
|
42
|
+
channels: {
|
|
43
|
+
...params.cfg.channels,
|
|
44
|
+
bluebubbles: {
|
|
45
|
+
...params.cfg.channels?.bluebubbles,
|
|
46
|
+
enabled: true,
|
|
47
|
+
...patch,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const currentAccount = params.cfg.channels?.bluebubbles?.accounts?.[params.accountId];
|
|
54
|
+
const enabled =
|
|
55
|
+
params.accountEnabled === "preserve-or-true"
|
|
56
|
+
? (currentAccount?.enabled ?? true)
|
|
57
|
+
: (params.accountEnabled ?? true);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
...params.cfg,
|
|
61
|
+
channels: {
|
|
62
|
+
...params.cfg.channels,
|
|
63
|
+
bluebubbles: {
|
|
64
|
+
...params.cfg.channels?.bluebubbles,
|
|
65
|
+
enabled: true,
|
|
66
|
+
accounts: {
|
|
67
|
+
...params.cfg.channels?.bluebubbles?.accounts,
|
|
68
|
+
[params.accountId]: {
|
|
69
|
+
...currentAccount,
|
|
70
|
+
enabled,
|
|
71
|
+
...patch,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -5,7 +5,7 @@ describe("BlueBubblesConfigSchema", () => {
|
|
|
5
5
|
it("accepts account config when serverUrl and password are both set", () => {
|
|
6
6
|
const parsed = BlueBubblesConfigSchema.safeParse({
|
|
7
7
|
serverUrl: "http://localhost:1234",
|
|
8
|
-
password: "secret",
|
|
8
|
+
password: "secret", // pragma: allowlist secret
|
|
9
9
|
});
|
|
10
10
|
expect(parsed.success).toBe(true);
|
|
11
11
|
});
|
package/src/config-schema.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk";
|
|
1
|
+
import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { buildSecretInputSchema, hasConfiguredSecretInput } from "./secret-input.js";
|
|
4
4
|
|
package/src/history.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
2
|
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
|
|
3
3
|
import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js";
|
|
4
4
|
|
package/src/media-send.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
5
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
|
|
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";
|
package/src/media-send.ts
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { resolveChannelMediaMaxBytes, type OpenClawConfig } from "openclaw/plugin-sdk";
|
|
6
|
+
import { resolveChannelMediaMaxBytes, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
7
7
|
import { resolveBlueBubblesAccount } from "./accounts.js";
|
|
8
8
|
import { sendBlueBubblesAttachment } from "./attachments.js";
|
|
9
9
|
import { resolveBlueBubblesMessageId } from "./monitor.js";
|
package/src/monitor-debounce.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
2
|
import type { NormalizedWebhookMessage } from "./monitor-normalize.js";
|
|
3
3
|
import type { BlueBubblesCoreRuntime, WebhookTarget } from "./monitor-shared.js";
|
|
4
4
|
|
package/src/monitor-normalize.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFiniteNumber } from "../../../src/infra/parse-finite-number.js";
|
|
1
2
|
import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js";
|
|
2
3
|
import type { BlueBubblesAttachment } from "./types.js";
|
|
3
4
|
|
|
@@ -35,17 +36,7 @@ function readNumberLike(record: Record<string, unknown> | null, key: string): nu
|
|
|
35
36
|
if (!record) {
|
|
36
37
|
return undefined;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
40
|
-
return value;
|
|
41
|
-
}
|
|
42
|
-
if (typeof value === "string") {
|
|
43
|
-
const parsed = Number.parseFloat(value);
|
|
44
|
-
if (Number.isFinite(parsed)) {
|
|
45
|
-
return parsed;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return undefined;
|
|
39
|
+
return parseFiniteNumber(record[key]);
|
|
49
40
|
}
|
|
50
41
|
|
|
51
42
|
function extractAttachments(message: Record<string, unknown>): BlueBubblesAttachment[] {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
2
|
import {
|
|
3
3
|
DM_GROUP_ACCESS_REASON,
|
|
4
4
|
createScopedPairingAccess,
|
|
5
5
|
createReplyPrefixOptions,
|
|
6
6
|
evictOldHistoryKeys,
|
|
7
|
+
issuePairingChallenge,
|
|
7
8
|
logAckFailure,
|
|
8
9
|
logInboundDrop,
|
|
9
10
|
logTypingFailure,
|
|
11
|
+
mapAllowFromEntries,
|
|
10
12
|
readStoreAllowFromForDmPolicy,
|
|
11
13
|
recordPendingHistoryEntryIfEnabled,
|
|
12
14
|
resolveAckReaction,
|
|
@@ -14,7 +16,7 @@ import {
|
|
|
14
16
|
resolveControlCommandGate,
|
|
15
17
|
stripMarkdown,
|
|
16
18
|
type HistoryEntry,
|
|
17
|
-
} from "openclaw/plugin-sdk";
|
|
19
|
+
} from "openclaw/plugin-sdk/bluebubbles";
|
|
18
20
|
import { downloadBlueBubblesAttachment } from "./attachments.js";
|
|
19
21
|
import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js";
|
|
20
22
|
import { fetchBlueBubblesHistory } from "./history.js";
|
|
@@ -509,7 +511,7 @@ export async function processMessage(
|
|
|
509
511
|
|
|
510
512
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
511
513
|
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
|
512
|
-
const configuredAllowFrom = (account.config.allowFrom
|
|
514
|
+
const configuredAllowFrom = mapAllowFromEntries(account.config.allowFrom);
|
|
513
515
|
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
|
|
514
516
|
provider: "bluebubbles",
|
|
515
517
|
accountId: account.accountId,
|
|
@@ -595,25 +597,24 @@ export async function processMessage(
|
|
|
595
597
|
}
|
|
596
598
|
|
|
597
599
|
if (accessDecision.decision === "pairing") {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
+
await issuePairingChallenge({
|
|
601
|
+
channel: "bluebubbles",
|
|
602
|
+
senderId: message.senderId,
|
|
603
|
+
senderIdLine: `Your BlueBubbles sender id: ${message.senderId}`,
|
|
600
604
|
meta: { name: message.senderName },
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
code,
|
|
612
|
-
}),
|
|
613
|
-
{ cfg: config, accountId: account.accountId },
|
|
614
|
-
);
|
|
605
|
+
upsertPairingRequest: pairing.upsertPairingRequest,
|
|
606
|
+
onCreated: () => {
|
|
607
|
+
runtime.log?.(`[bluebubbles] pairing request sender=${message.senderId} created=true`);
|
|
608
|
+
logVerbose(core, runtime, `bluebubbles pairing request sender=${message.senderId}`);
|
|
609
|
+
},
|
|
610
|
+
sendPairingReply: async (text) => {
|
|
611
|
+
await sendMessageBlueBubbles(message.senderId, text, {
|
|
612
|
+
cfg: config,
|
|
613
|
+
accountId: account.accountId,
|
|
614
|
+
});
|
|
615
615
|
statusSink?.({ lastOutboundAt: Date.now() });
|
|
616
|
-
}
|
|
616
|
+
},
|
|
617
|
+
onReplyError: (err) => {
|
|
617
618
|
logVerbose(
|
|
618
619
|
core,
|
|
619
620
|
runtime,
|
|
@@ -622,8 +623,8 @@ export async function processMessage(
|
|
|
622
623
|
runtime.error?.(
|
|
623
624
|
`[bluebubbles] pairing reply failed sender=${message.senderId}: ${String(err)}`,
|
|
624
625
|
);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
626
|
+
},
|
|
627
|
+
});
|
|
627
628
|
return;
|
|
628
629
|
}
|
|
629
630
|
|
package/src/monitor-shared.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeWebhookPath, type OpenClawConfig } from "openclaw/plugin-sdk";
|
|
1
|
+
import { normalizeWebhookPath, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
2
|
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
|
|
3
3
|
import { getBlueBubblesRuntime } from "./runtime.js";
|
|
4
4
|
import type { BlueBubblesAccountConfig } from "./types.js";
|