@openclaw/bluebubbles 2026.1.29 → 2026.2.1

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 CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
-
4
3
  import { bluebubblesPlugin } from "./src/channel.js";
5
4
  import { handleBlueBubblesWebhookRequest } from "./src/monitor.js";
6
5
  import { setBlueBubblesRuntime } from "./src/runtime.js";
@@ -1,8 +1,6 @@
1
1
  {
2
2
  "id": "bluebubbles",
3
- "channels": [
4
- "bluebubbles"
5
- ],
3
+ "channels": ["bluebubbles"],
6
4
  "configSchema": {
7
5
  "type": "object",
8
6
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@openclaw/bluebubbles",
3
- "version": "2026.1.29",
4
- "type": "module",
3
+ "version": "2026.2.1",
5
4
  "description": "OpenClaw BlueBubbles channel plugin",
5
+ "type": "module",
6
+ "devDependencies": {
7
+ "openclaw": "workspace:*"
8
+ },
6
9
  "openclaw": {
7
10
  "extensions": [
8
11
  "./index.ts"
package/src/accounts.ts CHANGED
@@ -13,19 +13,25 @@ export type ResolvedBlueBubblesAccount = {
13
13
 
14
14
  function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
15
15
  const accounts = cfg.channels?.bluebubbles?.accounts;
16
- if (!accounts || typeof accounts !== "object") return [];
16
+ if (!accounts || typeof accounts !== "object") {
17
+ return [];
18
+ }
17
19
  return Object.keys(accounts).filter(Boolean);
18
20
  }
19
21
 
20
22
  export function listBlueBubblesAccountIds(cfg: OpenClawConfig): string[] {
21
23
  const ids = listConfiguredAccountIds(cfg);
22
- if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
23
- return ids.sort((a, b) => a.localeCompare(b));
24
+ if (ids.length === 0) {
25
+ return [DEFAULT_ACCOUNT_ID];
26
+ }
27
+ return ids.toSorted((a, b) => a.localeCompare(b));
24
28
  }
25
29
 
26
30
  export function resolveDefaultBlueBubblesAccountId(cfg: OpenClawConfig): string {
27
31
  const ids = listBlueBubblesAccountIds(cfg);
28
- if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
32
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) {
33
+ return DEFAULT_ACCOUNT_ID;
34
+ }
29
35
  return ids[0] ?? DEFAULT_ACCOUNT_ID;
30
36
  }
31
37
 
@@ -34,7 +40,9 @@ function resolveAccountConfig(
34
40
  accountId: string,
35
41
  ): BlueBubblesAccountConfig | undefined {
36
42
  const accounts = cfg.channels?.bluebubbles?.accounts;
37
- if (!accounts || typeof accounts !== "object") return undefined;
43
+ if (!accounts || typeof accounts !== "object") {
44
+ return undefined;
45
+ }
38
46
  return accounts[accountId] as BlueBubblesAccountConfig | undefined;
39
47
  }
40
48
 
@@ -1,7 +1,6 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
1
2
  import { describe, expect, it, vi, beforeEach } from "vitest";
2
-
3
3
  import { bluebubblesMessageActions } from "./actions.js";
4
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
5
4
 
6
5
  vi.mock("./accounts.js", () => ({
7
6
  resolveBlueBubblesAccount: vi.fn(({ cfg, accountId }) => {
package/src/actions.ts CHANGED
@@ -9,14 +9,10 @@ import {
9
9
  type ChannelMessageActionAdapter,
10
10
  type ChannelMessageActionName,
11
11
  type ChannelToolSend,
12
- type OpenClawConfig,
13
12
  } from "openclaw/plugin-sdk";
14
-
13
+ import type { BlueBubblesSendTarget } from "./types.js";
15
14
  import { resolveBlueBubblesAccount } from "./accounts.js";
16
- import { resolveBlueBubblesMessageId } from "./monitor.js";
17
- import { isMacOS26OrHigher } from "./probe.js";
18
- import { sendBlueBubblesReaction } from "./reactions.js";
19
- import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
15
+ import { sendBlueBubblesAttachment } from "./attachments.js";
20
16
  import {
21
17
  editBlueBubblesMessage,
22
18
  unsendBlueBubblesMessage,
@@ -26,16 +22,22 @@ import {
26
22
  removeBlueBubblesParticipant,
27
23
  leaveBlueBubblesChat,
28
24
  } from "./chat.js";
29
- import { sendBlueBubblesAttachment } from "./attachments.js";
25
+ import { resolveBlueBubblesMessageId } from "./monitor.js";
26
+ import { isMacOS26OrHigher } from "./probe.js";
27
+ import { sendBlueBubblesReaction } from "./reactions.js";
28
+ import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
30
29
  import { normalizeBlueBubblesHandle, parseBlueBubblesTarget } from "./targets.js";
31
- import type { BlueBubblesSendTarget } from "./types.js";
32
30
 
33
31
  const providerId = "bluebubbles";
34
32
 
35
33
  function mapTarget(raw: string): BlueBubblesSendTarget {
36
34
  const parsed = parseBlueBubblesTarget(raw);
37
- if (parsed.kind === "chat_guid") return { kind: "chat_guid", chatGuid: parsed.chatGuid };
38
- if (parsed.kind === "chat_id") return { kind: "chat_id", chatId: parsed.chatId };
35
+ if (parsed.kind === "chat_guid") {
36
+ return { kind: "chat_guid", chatGuid: parsed.chatGuid };
37
+ }
38
+ if (parsed.kind === "chat_id") {
39
+ return { kind: "chat_id", chatId: parsed.chatId };
40
+ }
39
41
  if (parsed.kind === "chat_identifier") {
40
42
  return { kind: "chat_identifier", chatIdentifier: parsed.chatIdentifier };
41
43
  }
@@ -52,11 +54,17 @@ function readMessageText(params: Record<string, unknown>): string | undefined {
52
54
 
53
55
  function readBooleanParam(params: Record<string, unknown>, key: string): boolean | undefined {
54
56
  const raw = params[key];
55
- if (typeof raw === "boolean") return raw;
57
+ if (typeof raw === "boolean") {
58
+ return raw;
59
+ }
56
60
  if (typeof raw === "string") {
57
61
  const trimmed = raw.trim().toLowerCase();
58
- if (trimmed === "true") return true;
59
- if (trimmed === "false") return false;
62
+ if (trimmed === "true") {
63
+ return true;
64
+ }
65
+ if (trimmed === "false") {
66
+ return false;
67
+ }
60
68
  }
61
69
  return undefined;
62
70
  }
@@ -66,41 +74,55 @@ const SUPPORTED_ACTIONS = new Set<ChannelMessageActionName>(BLUEBUBBLES_ACTION_N
66
74
 
67
75
  export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
68
76
  listActions: ({ cfg }) => {
69
- const account = resolveBlueBubblesAccount({ cfg: cfg as OpenClawConfig });
70
- if (!account.enabled || !account.configured) return [];
71
- const gate = createActionGate((cfg as OpenClawConfig).channels?.bluebubbles?.actions);
77
+ const account = resolveBlueBubblesAccount({ cfg: cfg });
78
+ if (!account.enabled || !account.configured) {
79
+ return [];
80
+ }
81
+ const gate = createActionGate(cfg.channels?.bluebubbles?.actions);
72
82
  const actions = new Set<ChannelMessageActionName>();
73
83
  const macOS26 = isMacOS26OrHigher(account.accountId);
74
84
  for (const action of BLUEBUBBLES_ACTION_NAMES) {
75
85
  const spec = BLUEBUBBLES_ACTIONS[action];
76
- if (!spec?.gate) continue;
77
- if (spec.unsupportedOnMacOS26 && macOS26) continue;
78
- if (gate(spec.gate)) actions.add(action);
86
+ if (!spec?.gate) {
87
+ continue;
88
+ }
89
+ if (spec.unsupportedOnMacOS26 && macOS26) {
90
+ continue;
91
+ }
92
+ if (gate(spec.gate)) {
93
+ actions.add(action);
94
+ }
79
95
  }
80
96
  return Array.from(actions);
81
97
  },
82
98
  supportsAction: ({ action }) => SUPPORTED_ACTIONS.has(action),
83
99
  extractToolSend: ({ args }): ChannelToolSend | null => {
84
100
  const action = typeof args.action === "string" ? args.action.trim() : "";
85
- if (action !== "sendMessage") return null;
101
+ if (action !== "sendMessage") {
102
+ return null;
103
+ }
86
104
  const to = typeof args.to === "string" ? args.to : undefined;
87
- if (!to) return null;
105
+ if (!to) {
106
+ return null;
107
+ }
88
108
  const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined;
89
109
  return { to, accountId };
90
110
  },
91
111
  handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
92
112
  const account = resolveBlueBubblesAccount({
93
- cfg: cfg as OpenClawConfig,
113
+ cfg: cfg,
94
114
  accountId: accountId ?? undefined,
95
115
  });
96
116
  const baseUrl = account.config.serverUrl?.trim();
97
117
  const password = account.config.password?.trim();
98
- const opts = { cfg: cfg as OpenClawConfig, accountId: accountId ?? undefined };
118
+ const opts = { cfg: cfg, accountId: accountId ?? undefined };
99
119
 
100
120
  // Helper to resolve chatGuid from various params or session context
101
121
  const resolveChatGuid = async (): Promise<string> => {
102
122
  const chatGuid = readStringParam(params, "chatGuid");
103
- if (chatGuid?.trim()) return chatGuid.trim();
123
+ if (chatGuid?.trim()) {
124
+ return chatGuid.trim();
125
+ }
104
126
 
105
127
  const chatIdentifier = readStringParam(params, "chatIdentifier");
106
128
  const chatId = readNumberParam(params, "chatId", { integer: true });
@@ -185,8 +207,12 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
185
207
  readStringParam(params, "message");
186
208
  if (!rawMessageId || !newText) {
187
209
  const missing: string[] = [];
188
- if (!rawMessageId) missing.push("messageId (the message ID to edit)");
189
- if (!newText) missing.push("text (the new message content)");
210
+ if (!rawMessageId) {
211
+ missing.push("messageId (the message ID to edit)");
212
+ }
213
+ if (!newText) {
214
+ missing.push("text (the new message content)");
215
+ }
190
216
  throw new Error(
191
217
  `BlueBubbles edit requires: ${missing.join(", ")}. ` +
192
218
  `Use action=edit with messageId=<message_id>, text=<new_content>.`,
@@ -234,9 +260,15 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
234
260
  const to = readStringParam(params, "to") ?? readStringParam(params, "target");
235
261
  if (!rawMessageId || !text || !to) {
236
262
  const missing: string[] = [];
237
- if (!rawMessageId) missing.push("messageId (the message ID to reply to)");
238
- if (!text) missing.push("text or message (the reply message content)");
239
- if (!to) missing.push("to or target (the chat target)");
263
+ if (!rawMessageId) {
264
+ missing.push("messageId (the message ID to reply to)");
265
+ }
266
+ if (!text) {
267
+ missing.push("text or message (the reply message content)");
268
+ }
269
+ if (!to) {
270
+ missing.push("to or target (the chat target)");
271
+ }
240
272
  throw new Error(
241
273
  `BlueBubbles reply requires: ${missing.join(", ")}. ` +
242
274
  `Use action=reply with messageId=<message_id>, message=<your reply>, target=<chat_target>.`,
@@ -262,12 +294,17 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
262
294
  const effectId = readStringParam(params, "effectId") ?? readStringParam(params, "effect");
263
295
  if (!text || !to || !effectId) {
264
296
  const missing: string[] = [];
265
- if (!text) missing.push("text or message (the message content)");
266
- if (!to) missing.push("to or target (the chat target)");
267
- if (!effectId)
297
+ if (!text) {
298
+ missing.push("text or message (the message content)");
299
+ }
300
+ if (!to) {
301
+ missing.push("to or target (the chat target)");
302
+ }
303
+ if (!effectId) {
268
304
  missing.push(
269
305
  "effectId or effect (e.g., slam, loud, gentle, invisible-ink, confetti, lasers, fireworks, balloons, heart)",
270
306
  );
307
+ }
271
308
  throw new Error(
272
309
  `BlueBubbles sendWithEffect requires: ${missing.join(", ")}. ` +
273
310
  `Use action=sendWithEffect with message=<message>, target=<chat_target>, effectId=<effect_name>.`,
@@ -300,9 +337,7 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
300
337
  const resolvedChatGuid = await resolveChatGuid();
301
338
  const base64Buffer = readStringParam(params, "buffer");
302
339
  const filename =
303
- readStringParam(params, "filename") ??
304
- readStringParam(params, "name") ??
305
- "icon.png";
340
+ readStringParam(params, "filename") ?? readStringParam(params, "name") ?? "icon.png";
306
341
  const contentType =
307
342
  readStringParam(params, "contentType") ?? readStringParam(params, "mimeType");
308
343
 
@@ -1,7 +1,6 @@
1
1
  import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2
-
3
- import { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js";
4
2
  import type { BlueBubblesAttachment } from "./types.js";
3
+ import { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js";
5
4
 
6
5
  vi.mock("./accounts.js", () => ({
7
6
  resolveBlueBubblesAccount: vi.fn(({ cfg, accountId }) => {
@@ -1,6 +1,6 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
1
2
  import crypto from "node:crypto";
2
3
  import path from "node:path";
3
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
4
4
  import { resolveBlueBubblesAccount } from "./accounts.js";
5
5
  import { resolveChatGuidForTarget } from "./send.js";
6
6
  import { parseBlueBubblesTarget, normalizeBlueBubblesHandle } from "./targets.js";
@@ -31,7 +31,9 @@ function sanitizeFilename(input: string | undefined, fallback: string): string {
31
31
 
32
32
  function ensureExtension(filename: string, extension: string, fallbackBase: string): string {
33
33
  const currentExt = path.extname(filename);
34
- if (currentExt.toLowerCase() === extension) return filename;
34
+ if (currentExt.toLowerCase() === extension) {
35
+ return filename;
36
+ }
35
37
  const base = currentExt ? filename.slice(0, -currentExt.length) : filename;
36
38
  return `${base || fallbackBase}${extension}`;
37
39
  }
@@ -39,8 +41,10 @@ function ensureExtension(filename: string, extension: string, fallbackBase: stri
39
41
  function resolveVoiceInfo(filename: string, contentType?: string) {
40
42
  const normalizedType = contentType?.trim().toLowerCase();
41
43
  const extension = path.extname(filename).toLowerCase();
42
- const isMp3 = extension === ".mp3" || (normalizedType ? AUDIO_MIME_MP3.has(normalizedType) : false);
43
- const isCaf = extension === ".caf" || (normalizedType ? AUDIO_MIME_CAF.has(normalizedType) : false);
44
+ const isMp3 =
45
+ extension === ".mp3" || (normalizedType ? AUDIO_MIME_MP3.has(normalizedType) : false);
46
+ const isCaf =
47
+ extension === ".caf" || (normalizedType ? AUDIO_MIME_CAF.has(normalizedType) : false);
44
48
  const isAudio = isMp3 || isCaf || Boolean(normalizedType?.startsWith("audio/"));
45
49
  return { isAudio, isMp3, isCaf };
46
50
  }
@@ -52,8 +56,12 @@ function resolveAccount(params: BlueBubblesAttachmentOpts) {
52
56
  });
53
57
  const baseUrl = params.serverUrl?.trim() || account.config.serverUrl?.trim();
54
58
  const password = params.password?.trim() || account.config.password?.trim();
55
- if (!baseUrl) throw new Error("BlueBubbles serverUrl is required");
56
- if (!password) throw new Error("BlueBubbles password is required");
59
+ if (!baseUrl) {
60
+ throw new Error("BlueBubbles serverUrl is required");
61
+ }
62
+ if (!password) {
63
+ throw new Error("BlueBubbles password is required");
64
+ }
57
65
  return { baseUrl, password };
58
66
  }
59
67
 
@@ -62,7 +70,9 @@ export async function downloadBlueBubblesAttachment(
62
70
  opts: BlueBubblesAttachmentOpts & { maxBytes?: number } = {},
63
71
  ): Promise<{ buffer: Uint8Array; contentType?: string }> {
64
72
  const guid = attachment.guid?.trim();
65
- if (!guid) throw new Error("BlueBubbles attachment guid is required");
73
+ if (!guid) {
74
+ throw new Error("BlueBubbles attachment guid is required");
75
+ }
66
76
  const { baseUrl, password } = resolveAccount(opts);
67
77
  const url = buildBlueBubblesApiUrl({
68
78
  baseUrl,
@@ -108,9 +118,14 @@ function resolveSendTarget(raw: string): BlueBubblesSendTarget {
108
118
  }
109
119
 
110
120
  function extractMessageId(payload: unknown): string {
111
- if (!payload || typeof payload !== "object") return "unknown";
121
+ if (!payload || typeof payload !== "object") {
122
+ return "unknown";
123
+ }
112
124
  const record = payload as Record<string, unknown>;
113
- const data = record.data && typeof record.data === "object" ? (record.data as Record<string, unknown>) : null;
125
+ const data =
126
+ record.data && typeof record.data === "object"
127
+ ? (record.data as Record<string, unknown>)
128
+ : null;
114
129
  const candidates = [
115
130
  record.messageId,
116
131
  record.guid,
@@ -120,8 +135,12 @@ function extractMessageId(payload: unknown): string {
120
135
  data?.id,
121
136
  ];
122
137
  for (const candidate of candidates) {
123
- if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
124
- if (typeof candidate === "number" && Number.isFinite(candidate)) return String(candidate);
138
+ if (typeof candidate === "string" && candidate.trim()) {
139
+ return candidate.trim();
140
+ }
141
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
142
+ return String(candidate);
143
+ }
125
144
  }
126
145
  return "unknown";
127
146
  }
@@ -205,9 +224,7 @@ export async function sendBlueBubblesAttachment(params: {
205
224
  const addFile = (name: string, fileBuffer: Uint8Array, fileName: string, mimeType?: string) => {
206
225
  parts.push(encoder.encode(`--${boundary}\r\n`));
207
226
  parts.push(
208
- encoder.encode(
209
- `Content-Disposition: form-data; name="${name}"; filename="${fileName}"\r\n`,
210
- ),
227
+ encoder.encode(`Content-Disposition: form-data; name="${name}"; filename="${fileName}"\r\n`),
211
228
  );
212
229
  parts.push(encoder.encode(`Content-Type: ${mimeType ?? "application/octet-stream"}\r\n\r\n`));
213
230
  parts.push(fileBuffer);
@@ -229,10 +246,7 @@ export async function sendBlueBubblesAttachment(params: {
229
246
  const trimmedReplyTo = replyToMessageGuid?.trim();
230
247
  if (trimmedReplyTo) {
231
248
  addField("selectedMessageGuid", trimmedReplyTo);
232
- addField(
233
- "partIndex",
234
- typeof replyToPartIndex === "number" ? String(replyToPartIndex) : "0",
235
- );
249
+ addField("partIndex", typeof replyToPartIndex === "number" ? String(replyToPartIndex) : "0");
236
250
  }
237
251
 
238
252
  // Add optional caption
@@ -268,11 +282,15 @@ export async function sendBlueBubblesAttachment(params: {
268
282
 
269
283
  if (!res.ok) {
270
284
  const errorText = await res.text();
271
- throw new Error(`BlueBubbles attachment send failed (${res.status}): ${errorText || "unknown"}`);
285
+ throw new Error(
286
+ `BlueBubbles attachment send failed (${res.status}): ${errorText || "unknown"}`,
287
+ );
272
288
  }
273
289
 
274
290
  const responseBody = await res.text();
275
- if (!responseBody) return { messageId: "ok" };
291
+ if (!responseBody) {
292
+ return { messageId: "ok" };
293
+ }
276
294
  try {
277
295
  const parsed = JSON.parse(responseBody) as unknown;
278
296
  return { messageId: extractMessageId(parsed) };
package/src/channel.ts CHANGED
@@ -13,15 +13,18 @@ import {
13
13
  resolveBlueBubblesGroupToolPolicy,
14
14
  setAccountEnabledInConfigSection,
15
15
  } from "openclaw/plugin-sdk";
16
-
17
16
  import {
18
17
  listBlueBubblesAccountIds,
19
18
  type ResolvedBlueBubblesAccount,
20
19
  resolveBlueBubblesAccount,
21
20
  resolveDefaultBlueBubblesAccountId,
22
21
  } from "./accounts.js";
22
+ import { bluebubblesMessageActions } from "./actions.js";
23
23
  import { BlueBubblesConfigSchema } from "./config-schema.js";
24
+ import { sendBlueBubblesMedia } from "./media-send.js";
24
25
  import { resolveBlueBubblesMessageId } from "./monitor.js";
26
+ import { monitorBlueBubblesProvider, resolveWebhookPathFromConfig } from "./monitor.js";
27
+ import { blueBubblesOnboardingAdapter } from "./onboarding.js";
25
28
  import { probeBlueBubbles, type BlueBubblesProbe } from "./probe.js";
26
29
  import { sendMessageBlueBubbles } from "./send.js";
27
30
  import {
@@ -31,10 +34,6 @@ import {
31
34
  normalizeBlueBubblesMessagingTarget,
32
35
  parseBlueBubblesTarget,
33
36
  } from "./targets.js";
34
- import { bluebubblesMessageActions } from "./actions.js";
35
- import { monitorBlueBubblesProvider, resolveWebhookPathFromConfig } from "./monitor.js";
36
- import { blueBubblesOnboardingAdapter } from "./onboarding.js";
37
- import { sendBlueBubblesMedia } from "./media-send.js";
38
37
 
39
38
  const meta = {
40
39
  id: "bluebubbles",
@@ -78,13 +77,12 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
78
77
  configSchema: buildChannelConfigSchema(BlueBubblesConfigSchema),
79
78
  onboarding: blueBubblesOnboardingAdapter,
80
79
  config: {
81
- listAccountIds: (cfg) => listBlueBubblesAccountIds(cfg as OpenClawConfig),
82
- resolveAccount: (cfg, accountId) =>
83
- resolveBlueBubblesAccount({ cfg: cfg as OpenClawConfig, accountId }),
84
- defaultAccountId: (cfg) => resolveDefaultBlueBubblesAccountId(cfg as OpenClawConfig),
80
+ listAccountIds: (cfg) => listBlueBubblesAccountIds(cfg),
81
+ resolveAccount: (cfg, accountId) => resolveBlueBubblesAccount({ cfg: cfg, accountId }),
82
+ defaultAccountId: (cfg) => resolveDefaultBlueBubblesAccountId(cfg),
85
83
  setAccountEnabled: ({ cfg, accountId, enabled }) =>
86
84
  setAccountEnabledInConfigSection({
87
- cfg: cfg as OpenClawConfig,
85
+ cfg: cfg,
88
86
  sectionKey: "bluebubbles",
89
87
  accountId,
90
88
  enabled,
@@ -92,7 +90,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
92
90
  }),
93
91
  deleteAccount: ({ cfg, accountId }) =>
94
92
  deleteAccountFromConfigSection({
95
- cfg: cfg as OpenClawConfig,
93
+ cfg: cfg,
96
94
  sectionKey: "bluebubbles",
97
95
  accountId,
98
96
  clearBaseFields: ["serverUrl", "password", "name", "webhookPath"],
@@ -106,9 +104,8 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
106
104
  baseUrl: account.baseUrl,
107
105
  }),
108
106
  resolveAllowFrom: ({ cfg, accountId }) =>
109
- (resolveBlueBubblesAccount({ cfg: cfg as OpenClawConfig, accountId }).config.allowFrom ??
110
- []).map(
111
- (entry) => String(entry),
107
+ (resolveBlueBubblesAccount({ cfg: cfg, accountId }).config.allowFrom ?? []).map((entry) =>
108
+ String(entry),
112
109
  ),
113
110
  formatAllowFrom: ({ allowFrom }) =>
114
111
  allowFrom
@@ -121,9 +118,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
121
118
  security: {
122
119
  resolveDmPolicy: ({ cfg, accountId, account }) => {
123
120
  const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
124
- const useAccountPath = Boolean(
125
- (cfg as OpenClawConfig).channels?.bluebubbles?.accounts?.[resolvedAccountId],
126
- );
121
+ const useAccountPath = Boolean(cfg.channels?.bluebubbles?.accounts?.[resolvedAccountId]);
127
122
  const basePath = useAccountPath
128
123
  ? `channels.bluebubbles.accounts.${resolvedAccountId}.`
129
124
  : "channels.bluebubbles.";
@@ -138,7 +133,9 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
138
133
  },
139
134
  collectWarnings: ({ account }) => {
140
135
  const groupPolicy = account.config.groupPolicy ?? "allowlist";
141
- if (groupPolicy !== "open") return [];
136
+ if (groupPolicy !== "open") {
137
+ return [];
138
+ }
142
139
  return [
143
140
  `- BlueBubbles groups: groupPolicy="open" allows any member to trigger the bot. Set channels.bluebubbles.groupPolicy="allowlist" + channels.bluebubbles.groupAllowFrom to restrict senders.`,
144
141
  ];
@@ -152,19 +149,25 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
152
149
  },
153
150
  formatTargetDisplay: ({ target, display }) => {
154
151
  const shouldParseDisplay = (value: string): boolean => {
155
- if (looksLikeBlueBubblesTargetId(value)) return true;
152
+ if (looksLikeBlueBubblesTargetId(value)) {
153
+ return true;
154
+ }
156
155
  return /^(bluebubbles:|chat_guid:|chat_id:|chat_identifier:)/i.test(value);
157
156
  };
158
157
 
159
158
  // Helper to extract a clean handle from any BlueBubbles target format
160
159
  const extractCleanDisplay = (value: string | undefined): string | null => {
161
160
  const trimmed = value?.trim();
162
- if (!trimmed) return null;
161
+ if (!trimmed) {
162
+ return null;
163
+ }
163
164
  try {
164
165
  const parsed = parseBlueBubblesTarget(trimmed);
165
166
  if (parsed.kind === "chat_guid") {
166
167
  const handle = extractHandleFromChatGuid(parsed.chatGuid);
167
- if (handle) return handle;
168
+ if (handle) {
169
+ return handle;
170
+ }
168
171
  }
169
172
  if (parsed.kind === "handle") {
170
173
  return normalizeBlueBubblesHandle(parsed.to);
@@ -179,9 +182,13 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
179
182
  .replace(/^chat_id:/i, "")
180
183
  .replace(/^chat_identifier:/i, "");
181
184
  const handle = extractHandleFromChatGuid(stripped);
182
- if (handle) return handle;
185
+ if (handle) {
186
+ return handle;
187
+ }
183
188
  // Don't return raw chat_guid formats - they contain internal routing info
184
- if (stripped.includes(";-;") || stripped.includes(";+;")) return null;
189
+ if (stripped.includes(";-;") || stripped.includes(";+;")) {
190
+ return null;
191
+ }
185
192
  return stripped;
186
193
  };
187
194
 
@@ -192,12 +199,16 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
192
199
  return trimmedDisplay;
193
200
  }
194
201
  const cleanDisplay = extractCleanDisplay(trimmedDisplay);
195
- if (cleanDisplay) return cleanDisplay;
202
+ if (cleanDisplay) {
203
+ return cleanDisplay;
204
+ }
196
205
  }
197
206
 
198
207
  // Fall back to extracting from target
199
208
  const cleanTarget = extractCleanDisplay(target);
200
- if (cleanTarget) return cleanTarget;
209
+ if (cleanTarget) {
210
+ return cleanTarget;
211
+ }
201
212
 
202
213
  // Last resort: return display or target as-is
203
214
  return display?.trim() || target?.trim() || "";
@@ -207,7 +218,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
207
218
  resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
208
219
  applyAccountName: ({ cfg, accountId, name }) =>
209
220
  applyAccountNameToChannelSection({
210
- cfg: cfg as OpenClawConfig,
221
+ cfg: cfg,
211
222
  channelKey: "bluebubbles",
212
223
  accountId,
213
224
  name,
@@ -216,13 +227,17 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
216
227
  if (!input.httpUrl && !input.password) {
217
228
  return "BlueBubbles requires --http-url and --password.";
218
229
  }
219
- if (!input.httpUrl) return "BlueBubbles requires --http-url.";
220
- if (!input.password) return "BlueBubbles requires --password.";
230
+ if (!input.httpUrl) {
231
+ return "BlueBubbles requires --http-url.";
232
+ }
233
+ if (!input.password) {
234
+ return "BlueBubbles requires --password.";
235
+ }
221
236
  return null;
222
237
  },
223
238
  applyAccountConfig: ({ cfg, accountId, input }) => {
224
239
  const namedConfig = applyAccountNameToChannelSection({
225
- cfg: cfg as OpenClawConfig,
240
+ cfg: cfg,
226
241
  channelKey: "bluebubbles",
227
242
  accountId,
228
243
  name: input.name,
@@ -257,9 +272,9 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
257
272
  ...next.channels?.bluebubbles,
258
273
  enabled: true,
259
274
  accounts: {
260
- ...(next.channels?.bluebubbles?.accounts ?? {}),
275
+ ...next.channels?.bluebubbles?.accounts,
261
276
  [accountId]: {
262
- ...(next.channels?.bluebubbles?.accounts?.[accountId] ?? {}),
277
+ ...next.channels?.bluebubbles?.accounts?.[accountId],
263
278
  enabled: true,
264
279
  ...(input.httpUrl ? { serverUrl: input.httpUrl } : {}),
265
280
  ...(input.password ? { password: input.password } : {}),
@@ -276,7 +291,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
276
291
  normalizeAllowEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")),
277
292
  notifyApproval: async ({ cfg, id }) => {
278
293
  await sendMessageBlueBubbles(id, PAIRING_APPROVED_MESSAGE, {
279
- cfg: cfg as OpenClawConfig,
294
+ cfg: cfg,
280
295
  });
281
296
  },
282
297
  },
@@ -300,7 +315,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
300
315
  ? resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true })
301
316
  : "";
302
317
  const result = await sendMessageBlueBubbles(to, text, {
303
- cfg: cfg as OpenClawConfig,
318
+ cfg: cfg,
304
319
  accountId: accountId ?? undefined,
305
320
  replyToMessageGuid: replyToMessageGuid || undefined,
306
321
  });
@@ -317,7 +332,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
317
332
  };
318
333
  const resolvedCaption = caption ?? text;
319
334
  const result = await sendBlueBubblesMedia({
320
- cfg: cfg as OpenClawConfig,
335
+ cfg: cfg,
321
336
  to,
322
337
  mediaUrl,
323
338
  mediaPath,
@@ -388,7 +403,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
388
403
  ctx.log?.info(`[${account.accountId}] starting provider (webhook=${webhookPath})`);
389
404
  return monitorBlueBubblesProvider({
390
405
  account,
391
- config: ctx.cfg as OpenClawConfig,
406
+ config: ctx.cfg,
392
407
  runtime: ctx.runtime,
393
408
  abortSignal: ctx.abortSignal,
394
409
  statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
package/src/chat.test.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2
-
3
2
  import { markBlueBubblesChatRead, sendBlueBubblesTyping, setGroupIconBlueBubbles } from "./chat.js";
4
3
 
5
4
  vi.mock("./accounts.js", () => ({