@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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js";
|
|
4
|
+
import { setActivePluginRegistry } from "../../../src/plugins/runtime.js";
|
|
5
|
+
import type { WebhookTarget } from "./monitor-shared.js";
|
|
6
|
+
import { registerBlueBubblesWebhookTarget } from "./monitor.js";
|
|
7
|
+
|
|
8
|
+
function createTarget(): WebhookTarget {
|
|
9
|
+
return {
|
|
10
|
+
account: { accountId: "default" } as WebhookTarget["account"],
|
|
11
|
+
config: {} as OpenClawConfig,
|
|
12
|
+
runtime: {},
|
|
13
|
+
core: {} as WebhookTarget["core"],
|
|
14
|
+
path: "/bluebubbles-webhook",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("registerBlueBubblesWebhookTarget", () => {
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
setActivePluginRegistry(createEmptyPluginRegistry());
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("registers and unregisters plugin HTTP route at path boundaries", () => {
|
|
24
|
+
const registry = createEmptyPluginRegistry();
|
|
25
|
+
setActivePluginRegistry(registry);
|
|
26
|
+
|
|
27
|
+
const unregisterA = registerBlueBubblesWebhookTarget(createTarget());
|
|
28
|
+
const unregisterB = registerBlueBubblesWebhookTarget(createTarget());
|
|
29
|
+
|
|
30
|
+
expect(registry.httpRoutes).toHaveLength(1);
|
|
31
|
+
expect(registry.httpRoutes[0]).toEqual(
|
|
32
|
+
expect.objectContaining({
|
|
33
|
+
pluginId: "bluebubbles",
|
|
34
|
+
path: "/bluebubbles-webhook",
|
|
35
|
+
source: "bluebubbles-webhook",
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
unregisterA();
|
|
40
|
+
expect(registry.httpRoutes).toHaveLength(1);
|
|
41
|
+
unregisterB();
|
|
42
|
+
expect(registry.httpRoutes).toHaveLength(0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { WizardPrompter } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
vi.mock("openclaw/plugin-sdk/bluebubbles", () => ({
|
|
5
|
+
DEFAULT_ACCOUNT_ID: "default",
|
|
6
|
+
addWildcardAllowFrom: vi.fn(),
|
|
7
|
+
formatDocsLink: (_url: string, fallback: string) => fallback,
|
|
8
|
+
hasConfiguredSecretInput: (value: unknown) => {
|
|
9
|
+
if (typeof value === "string") {
|
|
10
|
+
return value.trim().length > 0;
|
|
11
|
+
}
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const ref = value as { source?: unknown; provider?: unknown; id?: unknown };
|
|
16
|
+
const validSource = ref.source === "env" || ref.source === "file" || ref.source === "exec";
|
|
17
|
+
return (
|
|
18
|
+
validSource &&
|
|
19
|
+
typeof ref.provider === "string" &&
|
|
20
|
+
ref.provider.trim().length > 0 &&
|
|
21
|
+
typeof ref.id === "string" &&
|
|
22
|
+
ref.id.trim().length > 0
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
mergeAllowFromEntries: (_existing: unknown, entries: string[]) => entries,
|
|
26
|
+
createAccountListHelpers: () => ({
|
|
27
|
+
listAccountIds: () => ["default"],
|
|
28
|
+
resolveDefaultAccountId: () => "default",
|
|
29
|
+
}),
|
|
30
|
+
normalizeSecretInputString: (value: unknown) => {
|
|
31
|
+
if (typeof value !== "string") {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const trimmed = value.trim();
|
|
35
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
36
|
+
},
|
|
37
|
+
normalizeAccountId: (value?: string | null) =>
|
|
38
|
+
value && value.trim().length > 0 ? value : "default",
|
|
39
|
+
promptAccountId: vi.fn(),
|
|
40
|
+
resolveAccountIdForConfigure: async (params: {
|
|
41
|
+
accountOverride?: string;
|
|
42
|
+
defaultAccountId: string;
|
|
43
|
+
}) => params.accountOverride?.trim() || params.defaultAccountId,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
describe("bluebubbles onboarding SecretInput", () => {
|
|
47
|
+
it("preserves existing password SecretRef when user keeps current credential", async () => {
|
|
48
|
+
const { blueBubblesOnboardingAdapter } = await import("./onboarding.js");
|
|
49
|
+
type ConfigureContext = Parameters<
|
|
50
|
+
NonNullable<typeof blueBubblesOnboardingAdapter.configure>
|
|
51
|
+
>[0];
|
|
52
|
+
const passwordRef = { source: "env", provider: "default", id: "BLUEBUBBLES_PASSWORD" };
|
|
53
|
+
const confirm = vi
|
|
54
|
+
.fn()
|
|
55
|
+
.mockResolvedValueOnce(true) // keep server URL
|
|
56
|
+
.mockResolvedValueOnce(true) // keep password SecretRef
|
|
57
|
+
.mockResolvedValueOnce(false); // keep default webhook path
|
|
58
|
+
const text = vi.fn();
|
|
59
|
+
const note = vi.fn();
|
|
60
|
+
|
|
61
|
+
const prompter = {
|
|
62
|
+
confirm,
|
|
63
|
+
text,
|
|
64
|
+
note,
|
|
65
|
+
} as unknown as WizardPrompter;
|
|
66
|
+
|
|
67
|
+
const context = {
|
|
68
|
+
cfg: {
|
|
69
|
+
channels: {
|
|
70
|
+
bluebubbles: {
|
|
71
|
+
enabled: true,
|
|
72
|
+
serverUrl: "http://127.0.0.1:1234",
|
|
73
|
+
password: passwordRef,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
prompter,
|
|
78
|
+
runtime: { ...console, exit: vi.fn() } as ConfigureContext["runtime"],
|
|
79
|
+
forceAllowFrom: false,
|
|
80
|
+
accountOverrides: {},
|
|
81
|
+
shouldPromptAccountIds: false,
|
|
82
|
+
} satisfies ConfigureContext;
|
|
83
|
+
|
|
84
|
+
const result = await blueBubblesOnboardingAdapter.configure(context);
|
|
85
|
+
|
|
86
|
+
expect(result.cfg.channels?.bluebubbles?.password).toEqual(passwordRef);
|
|
87
|
+
expect(text).not.toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
});
|
package/src/onboarding.ts
CHANGED
|
@@ -4,39 +4,33 @@ import type {
|
|
|
4
4
|
OpenClawConfig,
|
|
5
5
|
DmPolicy,
|
|
6
6
|
WizardPrompter,
|
|
7
|
-
} from "openclaw/plugin-sdk";
|
|
7
|
+
} from "openclaw/plugin-sdk/bluebubbles";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_ACCOUNT_ID,
|
|
10
|
-
addWildcardAllowFrom,
|
|
11
10
|
formatDocsLink,
|
|
12
11
|
mergeAllowFromEntries,
|
|
13
12
|
normalizeAccountId,
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
resolveAccountIdForConfigure,
|
|
14
|
+
setTopLevelChannelDmPolicyWithAllowFrom,
|
|
15
|
+
} from "openclaw/plugin-sdk/bluebubbles";
|
|
16
16
|
import {
|
|
17
17
|
listBlueBubblesAccountIds,
|
|
18
18
|
resolveBlueBubblesAccount,
|
|
19
19
|
resolveDefaultBlueBubblesAccountId,
|
|
20
20
|
} from "./accounts.js";
|
|
21
|
+
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
|
|
22
|
+
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
|
|
21
23
|
import { parseBlueBubblesAllowTarget } from "./targets.js";
|
|
22
24
|
import { normalizeBlueBubblesServerUrl } from "./types.js";
|
|
23
25
|
|
|
24
26
|
const channel = "bluebubbles" as const;
|
|
25
27
|
|
|
26
28
|
function setBlueBubblesDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy): OpenClawConfig {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
...cfg.channels,
|
|
33
|
-
bluebubbles: {
|
|
34
|
-
...cfg.channels?.bluebubbles,
|
|
35
|
-
dmPolicy,
|
|
36
|
-
...(allowFrom ? { allowFrom } : {}),
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
};
|
|
29
|
+
return setTopLevelChannelDmPolicyWithAllowFrom({
|
|
30
|
+
cfg,
|
|
31
|
+
channel: "bluebubbles",
|
|
32
|
+
dmPolicy,
|
|
33
|
+
});
|
|
40
34
|
}
|
|
41
35
|
|
|
42
36
|
function setBlueBubblesAllowFrom(
|
|
@@ -158,21 +152,16 @@ export const blueBubblesOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
158
152
|
};
|
|
159
153
|
},
|
|
160
154
|
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
|
|
161
|
-
const blueBubblesOverride = accountOverrides.bluebubbles?.trim();
|
|
162
155
|
const defaultAccountId = resolveDefaultBlueBubblesAccountId(cfg);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
listAccountIds: listBlueBubblesAccountIds,
|
|
173
|
-
defaultAccountId,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
156
|
+
const accountId = await resolveAccountIdForConfigure({
|
|
157
|
+
cfg,
|
|
158
|
+
prompter,
|
|
159
|
+
label: "BlueBubbles",
|
|
160
|
+
accountOverride: accountOverrides.bluebubbles,
|
|
161
|
+
shouldPromptAccountIds,
|
|
162
|
+
listAccountIds: listBlueBubblesAccountIds,
|
|
163
|
+
defaultAccountId,
|
|
164
|
+
});
|
|
176
165
|
|
|
177
166
|
let next = cfg;
|
|
178
167
|
const resolvedAccount = resolveBlueBubblesAccount({ cfg: next, accountId });
|
|
@@ -222,8 +211,11 @@ export const blueBubblesOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
222
211
|
}
|
|
223
212
|
|
|
224
213
|
// Prompt for password
|
|
225
|
-
|
|
226
|
-
|
|
214
|
+
const existingPassword = resolvedAccount.config.password;
|
|
215
|
+
const existingPasswordText = normalizeSecretInputString(existingPassword);
|
|
216
|
+
const hasConfiguredPassword = hasConfiguredSecretInput(existingPassword);
|
|
217
|
+
let password: unknown = existingPasswordText;
|
|
218
|
+
if (!hasConfiguredPassword) {
|
|
227
219
|
await prompter.note(
|
|
228
220
|
[
|
|
229
221
|
"Enter the BlueBubbles server password.",
|
|
@@ -247,6 +239,8 @@ export const blueBubblesOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
247
239
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
248
240
|
});
|
|
249
241
|
password = String(entered).trim();
|
|
242
|
+
} else if (!existingPasswordText) {
|
|
243
|
+
password = existingPassword;
|
|
250
244
|
}
|
|
251
245
|
}
|
|
252
246
|
|
|
@@ -277,42 +271,16 @@ export const blueBubblesOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
277
271
|
}
|
|
278
272
|
|
|
279
273
|
// Apply config
|
|
280
|
-
|
|
281
|
-
next
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
webhookPath,
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
} else {
|
|
295
|
-
next = {
|
|
296
|
-
...next,
|
|
297
|
-
channels: {
|
|
298
|
-
...next.channels,
|
|
299
|
-
bluebubbles: {
|
|
300
|
-
...next.channels?.bluebubbles,
|
|
301
|
-
enabled: true,
|
|
302
|
-
accounts: {
|
|
303
|
-
...next.channels?.bluebubbles?.accounts,
|
|
304
|
-
[accountId]: {
|
|
305
|
-
...next.channels?.bluebubbles?.accounts?.[accountId],
|
|
306
|
-
enabled: next.channels?.bluebubbles?.accounts?.[accountId]?.enabled ?? true,
|
|
307
|
-
serverUrl,
|
|
308
|
-
password,
|
|
309
|
-
webhookPath,
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
|
-
},
|
|
313
|
-
},
|
|
314
|
-
};
|
|
315
|
-
}
|
|
274
|
+
next = applyBlueBubblesConnectionConfig({
|
|
275
|
+
cfg: next,
|
|
276
|
+
accountId,
|
|
277
|
+
patch: {
|
|
278
|
+
serverUrl,
|
|
279
|
+
password,
|
|
280
|
+
webhookPath,
|
|
281
|
+
},
|
|
282
|
+
accountEnabled: "preserve-or-true",
|
|
283
|
+
});
|
|
316
284
|
|
|
317
285
|
await prompter.note(
|
|
318
286
|
[
|
package/src/probe.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BaseProbeResult } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { BaseProbeResult } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
|
+
import { normalizeSecretInputString } from "./secret-input.js";
|
|
2
3
|
import { buildBlueBubblesApiUrl, blueBubblesFetchWithTimeout } from "./types.js";
|
|
3
4
|
|
|
4
5
|
export type BlueBubblesProbe = BaseProbeResult & {
|
|
@@ -35,8 +36,8 @@ export async function fetchBlueBubblesServerInfo(params: {
|
|
|
35
36
|
accountId?: string;
|
|
36
37
|
timeoutMs?: number;
|
|
37
38
|
}): Promise<BlueBubblesServerInfo | null> {
|
|
38
|
-
const baseUrl = params.baseUrl
|
|
39
|
-
const password = params.password
|
|
39
|
+
const baseUrl = normalizeSecretInputString(params.baseUrl);
|
|
40
|
+
const password = normalizeSecretInputString(params.password);
|
|
40
41
|
if (!baseUrl || !password) {
|
|
41
42
|
return null;
|
|
42
43
|
}
|
|
@@ -138,8 +139,8 @@ export async function probeBlueBubbles(params: {
|
|
|
138
139
|
password?: string | null;
|
|
139
140
|
timeoutMs?: number;
|
|
140
141
|
}): Promise<BlueBubblesProbe> {
|
|
141
|
-
const baseUrl = params.baseUrl
|
|
142
|
-
const password = params.password
|
|
142
|
+
const baseUrl = normalizeSecretInputString(params.baseUrl);
|
|
143
|
+
const password = normalizeSecretInputString(params.password);
|
|
143
144
|
if (!baseUrl) {
|
|
144
145
|
return { ok: false, error: "serverUrl not configured" };
|
|
145
146
|
}
|
package/src/reactions.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 { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
|
4
4
|
import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js";
|
package/src/request-url.ts
CHANGED
|
@@ -1,12 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
if (typeof input === "string") {
|
|
3
|
-
return input;
|
|
4
|
-
}
|
|
5
|
-
if (input instanceof URL) {
|
|
6
|
-
return input.toString();
|
|
7
|
-
}
|
|
8
|
-
if (typeof input === "object" && input && "url" in input && typeof input.url === "string") {
|
|
9
|
-
return input.url;
|
|
10
|
-
}
|
|
11
|
-
return String(input);
|
|
12
|
-
}
|
|
1
|
+
export { resolveRequestUrl } from "openclaw/plugin-sdk/bluebubbles";
|
package/src/runtime.ts
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSecretInputSchema,
|
|
3
|
+
hasConfiguredSecretInput,
|
|
4
|
+
normalizeResolvedSecretInputString,
|
|
5
|
+
normalizeSecretInputString,
|
|
6
|
+
} from "openclaw/plugin-sdk/bluebubbles";
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
buildSecretInputSchema,
|
|
10
|
+
hasConfiguredSecretInput,
|
|
11
|
+
normalizeResolvedSecretInputString,
|
|
12
|
+
normalizeSecretInputString,
|
|
13
|
+
};
|
package/src/send-helpers.ts
CHANGED
|
@@ -23,31 +23,43 @@ export function extractBlueBubblesMessageId(payload: unknown): string {
|
|
|
23
23
|
if (!payload || typeof payload !== "object") {
|
|
24
24
|
return "unknown";
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
? (
|
|
26
|
+
|
|
27
|
+
const asRecord = (value: unknown): Record<string, unknown> | null =>
|
|
28
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
29
|
+
? (value as Record<string, unknown>)
|
|
30
30
|
: null;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for (const candidate of candidates) {
|
|
45
|
-
if (typeof candidate === "string" && candidate.trim()) {
|
|
46
|
-
return candidate.trim();
|
|
31
|
+
|
|
32
|
+
const record = payload as Record<string, unknown>;
|
|
33
|
+
const dataRecord = asRecord(record.data);
|
|
34
|
+
const resultRecord = asRecord(record.result);
|
|
35
|
+
const payloadRecord = asRecord(record.payload);
|
|
36
|
+
const messageRecord = asRecord(record.message);
|
|
37
|
+
const dataArrayFirst = Array.isArray(record.data) ? asRecord(record.data[0]) : null;
|
|
38
|
+
|
|
39
|
+
const roots = [record, dataRecord, resultRecord, payloadRecord, messageRecord, dataArrayFirst];
|
|
40
|
+
|
|
41
|
+
for (const root of roots) {
|
|
42
|
+
if (!root) {
|
|
43
|
+
continue;
|
|
47
44
|
}
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
const candidates = [
|
|
46
|
+
root.message_id,
|
|
47
|
+
root.messageId,
|
|
48
|
+
root.messageGuid,
|
|
49
|
+
root.message_guid,
|
|
50
|
+
root.guid,
|
|
51
|
+
root.id,
|
|
52
|
+
root.uuid,
|
|
53
|
+
];
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
56
|
+
return candidate.trim();
|
|
57
|
+
}
|
|
58
|
+
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
59
|
+
return String(candidate);
|
|
60
|
+
}
|
|
50
61
|
}
|
|
51
62
|
}
|
|
63
|
+
|
|
52
64
|
return "unknown";
|
|
53
65
|
}
|
package/src/send.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 { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import "./test-mocks.js";
|
|
4
4
|
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
|
|
@@ -721,6 +721,30 @@ describe("send", () => {
|
|
|
721
721
|
expect(result.messageId).toBe("msg-guid-789");
|
|
722
722
|
});
|
|
723
723
|
|
|
724
|
+
it("extracts top-level message_id from response payload", async () => {
|
|
725
|
+
mockResolvedHandleTarget();
|
|
726
|
+
mockSendResponse({ message_id: "bb-msg-321" });
|
|
727
|
+
|
|
728
|
+
const result = await sendMessageBlueBubbles("+15551234567", "Hello", {
|
|
729
|
+
serverUrl: "http://localhost:1234",
|
|
730
|
+
password: "test",
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
expect(result.messageId).toBe("bb-msg-321");
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("extracts nested result.message_id from response payload", async () => {
|
|
737
|
+
mockResolvedHandleTarget();
|
|
738
|
+
mockSendResponse({ result: { message_id: "bb-msg-654" } });
|
|
739
|
+
|
|
740
|
+
const result = await sendMessageBlueBubbles("+15551234567", "Hello", {
|
|
741
|
+
serverUrl: "http://localhost:1234",
|
|
742
|
+
password: "test",
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
expect(result.messageId).toBe("bb-msg-654");
|
|
746
|
+
});
|
|
747
|
+
|
|
724
748
|
it("resolves credentials from config", async () => {
|
|
725
749
|
mockResolvedHandleTarget();
|
|
726
750
|
mockSendResponse({ data: { guid: "msg-123" } });
|
package/src/send.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
3
|
-
import { stripMarkdown } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
|
|
3
|
+
import { stripMarkdown } from "openclaw/plugin-sdk/bluebubbles";
|
|
4
4
|
import { resolveBlueBubblesAccount } from "./accounts.js";
|
|
5
5
|
import {
|
|
6
6
|
getCachedBlueBubblesPrivateApiStatus,
|
|
7
7
|
isBlueBubblesPrivateApiStatusEnabled,
|
|
8
8
|
} from "./probe.js";
|
|
9
9
|
import { warnBlueBubbles } from "./runtime.js";
|
|
10
|
+
import { normalizeSecretInputString } from "./secret-input.js";
|
|
10
11
|
import { extractBlueBubblesMessageId, resolveBlueBubblesSendTarget } from "./send-helpers.js";
|
|
11
12
|
import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js";
|
|
12
13
|
import {
|
|
@@ -107,6 +108,19 @@ function resolvePrivateApiDecision(params: {
|
|
|
107
108
|
};
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
async function parseBlueBubblesMessageResponse(res: Response): Promise<BlueBubblesSendResult> {
|
|
112
|
+
const body = await res.text();
|
|
113
|
+
if (!body) {
|
|
114
|
+
return { messageId: "ok" };
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(body) as unknown;
|
|
118
|
+
return { messageId: extractBlueBubblesMessageId(parsed) };
|
|
119
|
+
} catch {
|
|
120
|
+
return { messageId: "ok" };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
110
124
|
type BlueBubblesChatRecord = Record<string, unknown>;
|
|
111
125
|
|
|
112
126
|
function extractChatGuid(chat: BlueBubblesChatRecord): string | null {
|
|
@@ -341,16 +355,7 @@ async function createNewChatWithMessage(params: {
|
|
|
341
355
|
}
|
|
342
356
|
throw new Error(`BlueBubbles create chat failed (${res.status}): ${errorText || "unknown"}`);
|
|
343
357
|
}
|
|
344
|
-
|
|
345
|
-
if (!body) {
|
|
346
|
-
return { messageId: "ok" };
|
|
347
|
-
}
|
|
348
|
-
try {
|
|
349
|
-
const parsed = JSON.parse(body) as unknown;
|
|
350
|
-
return { messageId: extractBlueBubblesMessageId(parsed) };
|
|
351
|
-
} catch {
|
|
352
|
-
return { messageId: "ok" };
|
|
353
|
-
}
|
|
358
|
+
return parseBlueBubblesMessageResponse(res);
|
|
354
359
|
}
|
|
355
360
|
|
|
356
361
|
export async function sendMessageBlueBubbles(
|
|
@@ -372,8 +377,12 @@ export async function sendMessageBlueBubbles(
|
|
|
372
377
|
cfg: opts.cfg ?? {},
|
|
373
378
|
accountId: opts.accountId,
|
|
374
379
|
});
|
|
375
|
-
const baseUrl =
|
|
376
|
-
|
|
380
|
+
const baseUrl =
|
|
381
|
+
normalizeSecretInputString(opts.serverUrl) ||
|
|
382
|
+
normalizeSecretInputString(account.config.serverUrl);
|
|
383
|
+
const password =
|
|
384
|
+
normalizeSecretInputString(opts.password) ||
|
|
385
|
+
normalizeSecretInputString(account.config.password);
|
|
377
386
|
if (!baseUrl) {
|
|
378
387
|
throw new Error("BlueBubbles serverUrl is required");
|
|
379
388
|
}
|
|
@@ -459,14 +468,5 @@ export async function sendMessageBlueBubbles(
|
|
|
459
468
|
const errorText = await res.text();
|
|
460
469
|
throw new Error(`BlueBubbles send failed (${res.status}): ${errorText || "unknown"}`);
|
|
461
470
|
}
|
|
462
|
-
|
|
463
|
-
if (!body) {
|
|
464
|
-
return { messageId: "ok" };
|
|
465
|
-
}
|
|
466
|
-
try {
|
|
467
|
-
const parsed = JSON.parse(body) as unknown;
|
|
468
|
-
return { messageId: extractBlueBubblesMessageId(parsed) };
|
|
469
|
-
} catch {
|
|
470
|
-
return { messageId: "ok" };
|
|
471
|
-
}
|
|
471
|
+
return parseBlueBubblesMessageResponse(res);
|
|
472
472
|
}
|
package/src/targets.ts
CHANGED
|
@@ -2,9 +2,10 @@ import {
|
|
|
2
2
|
isAllowedParsedChatSender,
|
|
3
3
|
parseChatAllowTargetPrefixes,
|
|
4
4
|
parseChatTargetPrefixesOrThrow,
|
|
5
|
+
type ParsedChatTarget,
|
|
5
6
|
resolveServicePrefixedAllowTarget,
|
|
6
7
|
resolveServicePrefixedTarget,
|
|
7
|
-
} from "openclaw/plugin-sdk";
|
|
8
|
+
} from "openclaw/plugin-sdk/bluebubbles";
|
|
8
9
|
|
|
9
10
|
export type BlueBubblesService = "imessage" | "sms" | "auto";
|
|
10
11
|
|
|
@@ -14,11 +15,7 @@ export type BlueBubblesTarget =
|
|
|
14
15
|
| { kind: "chat_identifier"; chatIdentifier: string }
|
|
15
16
|
| { kind: "handle"; to: string; service: BlueBubblesService };
|
|
16
17
|
|
|
17
|
-
export type BlueBubblesAllowTarget =
|
|
18
|
-
| { kind: "chat_id"; chatId: number }
|
|
19
|
-
| { kind: "chat_guid"; chatGuid: string }
|
|
20
|
-
| { kind: "chat_identifier"; chatIdentifier: string }
|
|
21
|
-
| { kind: "handle"; handle: string };
|
|
18
|
+
export type BlueBubblesAllowTarget = ParsedChatTarget | { kind: "handle"; handle: string };
|
|
22
19
|
|
|
23
20
|
const CHAT_ID_PREFIXES = ["chat_id:", "chatid:", "chat:"];
|
|
24
21
|
const CHAT_GUID_PREFIXES = ["chat_guid:", "chatguid:", "guid:"];
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/bluebubbles";
|
|
2
2
|
|
|
3
|
-
export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk";
|
|
3
|
+
export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/bluebubbles";
|
|
4
4
|
|
|
5
5
|
export type BlueBubblesGroupConfig = {
|
|
6
6
|
/** If true, only respond in this group when mentioned. */
|