@openclaw/bluebubbles 2026.3.1 → 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/README.md +1 -1
- package/index.ts +2 -4
- package/package.json +4 -1
- package/src/account-resolve.ts +20 -3
- package/src/accounts.test.ts +25 -0
- package/src/accounts.ts +11 -39
- package/src/actions.test.ts +1 -1
- package/src/actions.ts +5 -20
- package/src/attachments.test.ts +1 -1
- package/src/attachments.ts +1 -1
- package/src/channel.ts +53 -80
- package/src/chat.ts +46 -39
- package/src/config-apply.ts +77 -0
- package/src/config-schema.test.ts +13 -1
- package/src/config-schema.ts +5 -4
- 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 +205 -0
- package/src/monitor-normalize.ts +2 -11
- package/src/monitor-processing.ts +26 -24
- package/src/monitor-shared.ts +1 -1
- package/src/monitor.test.ts +45 -738
- package/src/monitor.ts +164 -383
- package/src/monitor.webhook-auth.test.ts +767 -0
- package/src/monitor.webhook-route.test.ts +44 -0
- package/src/onboarding.secret-input.test.ts +89 -0
- package/src/onboarding.ts +37 -69
- package/src/probe.ts +6 -5
- package/src/reactions.ts +1 -1
- package/src/request-url.ts +1 -12
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +13 -0
- package/src/send-helpers.ts +34 -22
- package/src/send.test.ts +25 -1
- package/src/send.ts +24 -24
- package/src/targets.ts +3 -6
- package/src/types.ts +2 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ If you’re looking for **how to use BlueBubbles as an agent/tool user**, see:
|
|
|
10
10
|
|
|
11
11
|
- Extension package: `extensions/bluebubbles/` (entry: `index.ts`).
|
|
12
12
|
- Channel implementation: `extensions/bluebubbles/src/channel.ts`.
|
|
13
|
-
- Webhook handling: `extensions/bluebubbles/src/monitor.ts` (register via `
|
|
13
|
+
- Webhook handling: `extensions/bluebubbles/src/monitor.ts` (register per-account route via `registerPluginHttpRoute`).
|
|
14
14
|
- REST helpers: `extensions/bluebubbles/src/send.ts` + `extensions/bluebubbles/src/probe.ts`.
|
|
15
15
|
- Runtime bridge: `extensions/bluebubbles/src/runtime.ts` (set via `api.runtime`).
|
|
16
16
|
- Catalog entry for onboarding: `src/channels/plugins/catalog.ts`.
|
package/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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
|
-
import { handleBlueBubblesWebhookRequest } from "./src/monitor.js";
|
|
5
4
|
import { setBlueBubblesRuntime } from "./src/runtime.js";
|
|
6
5
|
|
|
7
6
|
const plugin = {
|
|
@@ -12,7 +11,6 @@ const plugin = {
|
|
|
12
11
|
register(api: OpenClawPluginApi) {
|
|
13
12
|
setBlueBubblesRuntime(api.runtime);
|
|
14
13
|
api.registerChannel({ plugin: bluebubblesPlugin });
|
|
15
|
-
api.registerHttpHandler(handleBlueBubblesWebhookRequest);
|
|
16
14
|
},
|
|
17
15
|
};
|
|
18
16
|
|
package/package.json
CHANGED
package/src/account-resolve.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
2
|
import { resolveBlueBubblesAccount } from "./accounts.js";
|
|
3
|
+
import { normalizeResolvedSecretInputString } from "./secret-input.js";
|
|
3
4
|
|
|
4
5
|
export type BlueBubblesAccountResolveOpts = {
|
|
5
6
|
serverUrl?: string;
|
|
@@ -18,8 +19,24 @@ export function resolveBlueBubblesServerAccount(params: BlueBubblesAccountResolv
|
|
|
18
19
|
cfg: params.cfg ?? {},
|
|
19
20
|
accountId: params.accountId,
|
|
20
21
|
});
|
|
21
|
-
const baseUrl =
|
|
22
|
-
|
|
22
|
+
const baseUrl =
|
|
23
|
+
normalizeResolvedSecretInputString({
|
|
24
|
+
value: params.serverUrl,
|
|
25
|
+
path: "channels.bluebubbles.serverUrl",
|
|
26
|
+
}) ||
|
|
27
|
+
normalizeResolvedSecretInputString({
|
|
28
|
+
value: account.config.serverUrl,
|
|
29
|
+
path: `channels.bluebubbles.accounts.${account.accountId}.serverUrl`,
|
|
30
|
+
});
|
|
31
|
+
const password =
|
|
32
|
+
normalizeResolvedSecretInputString({
|
|
33
|
+
value: params.password,
|
|
34
|
+
path: "channels.bluebubbles.password",
|
|
35
|
+
}) ||
|
|
36
|
+
normalizeResolvedSecretInputString({
|
|
37
|
+
value: account.config.password,
|
|
38
|
+
path: `channels.bluebubbles.accounts.${account.accountId}.password`,
|
|
39
|
+
});
|
|
23
40
|
if (!baseUrl) {
|
|
24
41
|
throw new Error("BlueBubbles serverUrl is required");
|
|
25
42
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveBlueBubblesAccount } from "./accounts.js";
|
|
3
|
+
|
|
4
|
+
describe("resolveBlueBubblesAccount", () => {
|
|
5
|
+
it("treats SecretRef passwords as configured when serverUrl exists", () => {
|
|
6
|
+
const resolved = resolveBlueBubblesAccount({
|
|
7
|
+
cfg: {
|
|
8
|
+
channels: {
|
|
9
|
+
bluebubbles: {
|
|
10
|
+
enabled: true,
|
|
11
|
+
serverUrl: "http://localhost:1234",
|
|
12
|
+
password: {
|
|
13
|
+
source: "env",
|
|
14
|
+
provider: "default",
|
|
15
|
+
id: "BLUEBUBBLES_PASSWORD",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(resolved.configured).toBe(true);
|
|
23
|
+
expect(resolved.baseUrl).toBe("http://localhost:1234");
|
|
24
|
+
});
|
|
25
|
+
});
|
package/src/accounts.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
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";
|
|
3
|
+
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
|
|
7
4
|
import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js";
|
|
8
5
|
|
|
9
6
|
export type ResolvedBlueBubblesAccount = {
|
|
@@ -15,36 +12,11 @@ export type ResolvedBlueBubblesAccount = {
|
|
|
15
12
|
baseUrl?: string;
|
|
16
13
|
};
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return Object.keys(accounts).filter(Boolean);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function listBlueBubblesAccountIds(cfg: OpenClawConfig): string[] {
|
|
27
|
-
const ids = listConfiguredAccountIds(cfg);
|
|
28
|
-
if (ids.length === 0) {
|
|
29
|
-
return [DEFAULT_ACCOUNT_ID];
|
|
30
|
-
}
|
|
31
|
-
return ids.toSorted((a, b) => a.localeCompare(b));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function resolveDefaultBlueBubblesAccountId(cfg: OpenClawConfig): string {
|
|
35
|
-
const preferred = normalizeOptionalAccountId(cfg.channels?.bluebubbles?.defaultAccount);
|
|
36
|
-
if (
|
|
37
|
-
preferred &&
|
|
38
|
-
listBlueBubblesAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
|
39
|
-
) {
|
|
40
|
-
return preferred;
|
|
41
|
-
}
|
|
42
|
-
const ids = listBlueBubblesAccountIds(cfg);
|
|
43
|
-
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
44
|
-
return DEFAULT_ACCOUNT_ID;
|
|
45
|
-
}
|
|
46
|
-
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
47
|
-
}
|
|
15
|
+
const {
|
|
16
|
+
listAccountIds: listBlueBubblesAccountIds,
|
|
17
|
+
resolveDefaultAccountId: resolveDefaultBlueBubblesAccountId,
|
|
18
|
+
} = createAccountListHelpers("bluebubbles");
|
|
19
|
+
export { listBlueBubblesAccountIds, resolveDefaultBlueBubblesAccountId };
|
|
48
20
|
|
|
49
21
|
function resolveAccountConfig(
|
|
50
22
|
cfg: OpenClawConfig,
|
|
@@ -79,9 +51,9 @@ export function resolveBlueBubblesAccount(params: {
|
|
|
79
51
|
const baseEnabled = params.cfg.channels?.bluebubbles?.enabled;
|
|
80
52
|
const merged = mergeBlueBubblesAccountConfig(params.cfg, accountId);
|
|
81
53
|
const accountEnabled = merged.enabled !== false;
|
|
82
|
-
const serverUrl = merged.serverUrl
|
|
83
|
-
const password = merged.password
|
|
84
|
-
const configured = Boolean(serverUrl && password);
|
|
54
|
+
const serverUrl = normalizeSecretInputString(merged.serverUrl);
|
|
55
|
+
const password = normalizeSecretInputString(merged.password);
|
|
56
|
+
const configured = Boolean(serverUrl && hasConfiguredSecretInput(merged.password));
|
|
85
57
|
const baseUrl = serverUrl ? normalizeBlueBubblesServerUrl(serverUrl) : undefined;
|
|
86
58
|
return {
|
|
87
59
|
accountId,
|
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
|
@@ -5,11 +5,12 @@ import {
|
|
|
5
5
|
extractToolSend,
|
|
6
6
|
jsonResult,
|
|
7
7
|
readNumberParam,
|
|
8
|
+
readBooleanParam,
|
|
8
9
|
readReactionParams,
|
|
9
10
|
readStringParam,
|
|
10
11
|
type ChannelMessageActionAdapter,
|
|
11
12
|
type ChannelMessageActionName,
|
|
12
|
-
} from "openclaw/plugin-sdk";
|
|
13
|
+
} from "openclaw/plugin-sdk/bluebubbles";
|
|
13
14
|
import { resolveBlueBubblesAccount } from "./accounts.js";
|
|
14
15
|
import { sendBlueBubblesAttachment } from "./attachments.js";
|
|
15
16
|
import {
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
import { resolveBlueBubblesMessageId } from "./monitor.js";
|
|
25
26
|
import { getCachedBlueBubblesPrivateApiStatus, isMacOS26OrHigher } from "./probe.js";
|
|
26
27
|
import { sendBlueBubblesReaction } from "./reactions.js";
|
|
28
|
+
import { normalizeSecretInputString } from "./secret-input.js";
|
|
27
29
|
import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
|
|
28
30
|
import { normalizeBlueBubblesHandle, parseBlueBubblesTarget } from "./targets.js";
|
|
29
31
|
import type { BlueBubblesSendTarget } from "./types.js";
|
|
@@ -52,23 +54,6 @@ function readMessageText(params: Record<string, unknown>): string | undefined {
|
|
|
52
54
|
return readStringParam(params, "text") ?? readStringParam(params, "message");
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
function readBooleanParam(params: Record<string, unknown>, key: string): boolean | undefined {
|
|
56
|
-
const raw = params[key];
|
|
57
|
-
if (typeof raw === "boolean") {
|
|
58
|
-
return raw;
|
|
59
|
-
}
|
|
60
|
-
if (typeof raw === "string") {
|
|
61
|
-
const trimmed = raw.trim().toLowerCase();
|
|
62
|
-
if (trimmed === "true") {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
if (trimmed === "false") {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
57
|
/** Supported action names for BlueBubbles */
|
|
73
58
|
const SUPPORTED_ACTIONS = new Set<ChannelMessageActionName>(BLUEBUBBLES_ACTION_NAMES);
|
|
74
59
|
const PRIVATE_API_ACTIONS = new Set<ChannelMessageActionName>([
|
|
@@ -118,8 +103,8 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
|
|
118
103
|
cfg: cfg,
|
|
119
104
|
accountId: accountId ?? undefined,
|
|
120
105
|
});
|
|
121
|
-
const baseUrl = account.config.serverUrl
|
|
122
|
-
const password = account.config.password
|
|
106
|
+
const baseUrl = normalizeSecretInputString(account.config.serverUrl);
|
|
107
|
+
const password = normalizeSecretInputString(account.config.password);
|
|
123
108
|
const opts = { cfg: cfg, accountId: accountId ?? undefined };
|
|
124
109
|
const assertPrivateApiEnabled = () => {
|
|
125
110
|
if (getCachedBlueBubblesPrivateApiStatus(account.accountId) === false) {
|
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,18 +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,
|
|
10
|
+
buildProbeChannelStatusSummary,
|
|
5
11
|
collectBlueBubblesStatusIssues,
|
|
6
12
|
DEFAULT_ACCOUNT_ID,
|
|
7
13
|
deleteAccountFromConfigSection,
|
|
8
|
-
formatPairingApproveHint,
|
|
9
14
|
migrateBaseNameToDefaultAccount,
|
|
10
15
|
normalizeAccountId,
|
|
11
16
|
PAIRING_APPROVED_MESSAGE,
|
|
12
17
|
resolveBlueBubblesGroupRequireMention,
|
|
13
18
|
resolveBlueBubblesGroupToolPolicy,
|
|
14
19
|
setAccountEnabledInConfigSection,
|
|
15
|
-
} 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";
|
|
16
27
|
import {
|
|
17
28
|
listBlueBubblesAccountIds,
|
|
18
29
|
type ResolvedBlueBubblesAccount,
|
|
@@ -20,6 +31,7 @@ import {
|
|
|
20
31
|
resolveDefaultBlueBubblesAccountId,
|
|
21
32
|
} from "./accounts.js";
|
|
22
33
|
import { bluebubblesMessageActions } from "./actions.js";
|
|
34
|
+
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
|
|
23
35
|
import { BlueBubblesConfigSchema } from "./config-schema.js";
|
|
24
36
|
import { sendBlueBubblesMedia } from "./media-send.js";
|
|
25
37
|
import { resolveBlueBubblesMessageId } from "./monitor.js";
|
|
@@ -104,41 +116,37 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
|
|
104
116
|
baseUrl: account.baseUrl,
|
|
105
117
|
}),
|
|
106
118
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
107
|
-
(resolveBlueBubblesAccount({ cfg: cfg, accountId }).config.allowFrom
|
|
108
|
-
String(entry),
|
|
109
|
-
),
|
|
119
|
+
mapAllowFromEntries(resolveBlueBubblesAccount({ cfg: cfg, accountId }).config.allowFrom),
|
|
110
120
|
formatAllowFrom: ({ allowFrom }) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
.map((entry) => normalizeBlueBubblesHandle(entry)),
|
|
121
|
+
formatNormalizedAllowFromEntries({
|
|
122
|
+
allowFrom,
|
|
123
|
+
normalizeEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")),
|
|
124
|
+
}),
|
|
116
125
|
},
|
|
117
126
|
actions: bluebubblesMessageActions,
|
|
118
127
|
security: {
|
|
119
128
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
:
|
|
125
|
-
|
|
126
|
-
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,
|
|
127
135
|
allowFrom: account.config.allowFrom ?? [],
|
|
128
|
-
|
|
129
|
-
allowFromPath: basePath,
|
|
130
|
-
approveHint: formatPairingApproveHint("bluebubbles"),
|
|
136
|
+
policyPathSuffix: "dmPolicy",
|
|
131
137
|
normalizeEntry: (raw) => normalizeBlueBubblesHandle(raw.replace(/^bluebubbles:/i, "")),
|
|
132
|
-
};
|
|
138
|
+
});
|
|
133
139
|
},
|
|
134
140
|
collectWarnings: ({ account }) => {
|
|
135
141
|
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
});
|
|
142
150
|
},
|
|
143
151
|
},
|
|
144
152
|
messaging: {
|
|
@@ -249,41 +257,16 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
|
|
249
257
|
channelKey: "bluebubbles",
|
|
250
258
|
})
|
|
251
259
|
: namedConfig;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
enabled: true,
|
|
260
|
-
...(input.httpUrl ? { serverUrl: input.httpUrl } : {}),
|
|
261
|
-
...(input.password ? { password: input.password } : {}),
|
|
262
|
-
...(input.webhookPath ? { webhookPath: input.webhookPath } : {}),
|
|
263
|
-
},
|
|
264
|
-
},
|
|
265
|
-
} as OpenClawConfig;
|
|
266
|
-
}
|
|
267
|
-
return {
|
|
268
|
-
...next,
|
|
269
|
-
channels: {
|
|
270
|
-
...next.channels,
|
|
271
|
-
bluebubbles: {
|
|
272
|
-
...next.channels?.bluebubbles,
|
|
273
|
-
enabled: true,
|
|
274
|
-
accounts: {
|
|
275
|
-
...next.channels?.bluebubbles?.accounts,
|
|
276
|
-
[accountId]: {
|
|
277
|
-
...next.channels?.bluebubbles?.accounts?.[accountId],
|
|
278
|
-
enabled: true,
|
|
279
|
-
...(input.httpUrl ? { serverUrl: input.httpUrl } : {}),
|
|
280
|
-
...(input.password ? { password: input.password } : {}),
|
|
281
|
-
...(input.webhookPath ? { webhookPath: input.webhookPath } : {}),
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
},
|
|
260
|
+
return applyBlueBubblesConnectionConfig({
|
|
261
|
+
cfg: next,
|
|
262
|
+
accountId,
|
|
263
|
+
patch: {
|
|
264
|
+
serverUrl: input.httpUrl,
|
|
265
|
+
password: input.password,
|
|
266
|
+
webhookPath: input.webhookPath,
|
|
285
267
|
},
|
|
286
|
-
|
|
268
|
+
onlyDefinedFields: true,
|
|
269
|
+
});
|
|
287
270
|
},
|
|
288
271
|
},
|
|
289
272
|
pairing: {
|
|
@@ -356,16 +339,8 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
|
|
356
339
|
lastError: null,
|
|
357
340
|
},
|
|
358
341
|
collectStatusIssues: collectBlueBubblesStatusIssues,
|
|
359
|
-
buildChannelSummary: ({ snapshot }) =>
|
|
360
|
-
|
|
361
|
-
baseUrl: snapshot.baseUrl ?? null,
|
|
362
|
-
running: snapshot.running ?? false,
|
|
363
|
-
lastStartAt: snapshot.lastStartAt ?? null,
|
|
364
|
-
lastStopAt: snapshot.lastStopAt ?? null,
|
|
365
|
-
lastError: snapshot.lastError ?? null,
|
|
366
|
-
probe: snapshot.probe,
|
|
367
|
-
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
368
|
-
}),
|
|
342
|
+
buildChannelSummary: ({ snapshot }) =>
|
|
343
|
+
buildProbeChannelStatusSummary(snapshot, { baseUrl: snapshot.baseUrl ?? null }),
|
|
369
344
|
probeAccount: async ({ account, timeoutMs }) =>
|
|
370
345
|
probeBlueBubbles({
|
|
371
346
|
baseUrl: account.baseUrl,
|
|
@@ -375,20 +350,18 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
|
|
375
350
|
buildAccountSnapshot: ({ account, runtime, probe }) => {
|
|
376
351
|
const running = runtime?.running ?? false;
|
|
377
352
|
const probeOk = (probe as BlueBubblesProbe | undefined)?.ok;
|
|
378
|
-
|
|
353
|
+
const base = buildComputedAccountStatusSnapshot({
|
|
379
354
|
accountId: account.accountId,
|
|
380
355
|
name: account.name,
|
|
381
356
|
enabled: account.enabled,
|
|
382
357
|
configured: account.configured,
|
|
358
|
+
runtime,
|
|
359
|
+
probe,
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
...base,
|
|
383
363
|
baseUrl: account.baseUrl,
|
|
384
|
-
running,
|
|
385
364
|
connected: probeOk ?? running,
|
|
386
|
-
lastStartAt: runtime?.lastStartAt ?? null,
|
|
387
|
-
lastStopAt: runtime?.lastStopAt ?? null,
|
|
388
|
-
lastError: runtime?.lastError ?? null,
|
|
389
|
-
probe,
|
|
390
|
-
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
391
|
-
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
392
365
|
};
|
|
393
366
|
},
|
|
394
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,19 @@ 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
|
+
});
|
|
10
|
+
expect(parsed.success).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("accepts SecretRef password when serverUrl is set", () => {
|
|
14
|
+
const parsed = BlueBubblesConfigSchema.safeParse({
|
|
15
|
+
serverUrl: "http://localhost:1234",
|
|
16
|
+
password: {
|
|
17
|
+
source: "env",
|
|
18
|
+
provider: "default",
|
|
19
|
+
id: "BLUEBUBBLES_PASSWORD",
|
|
20
|
+
},
|
|
9
21
|
});
|
|
10
22
|
expect(parsed.success).toBe(true);
|
|
11
23
|
});
|