@openclaw/bluebubbles 2026.1.29
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 +20 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +33 -0
- package/src/accounts.ts +80 -0
- package/src/actions.test.ts +651 -0
- package/src/actions.ts +403 -0
- package/src/attachments.test.ts +346 -0
- package/src/attachments.ts +282 -0
- package/src/channel.ts +399 -0
- package/src/chat.test.ts +462 -0
- package/src/chat.ts +354 -0
- package/src/config-schema.ts +51 -0
- package/src/media-send.ts +168 -0
- package/src/monitor.test.ts +2146 -0
- package/src/monitor.ts +2276 -0
- package/src/onboarding.ts +340 -0
- package/src/probe.ts +127 -0
- package/src/reactions.test.ts +393 -0
- package/src/reactions.ts +183 -0
- package/src/runtime.ts +14 -0
- package/src/send.test.ts +809 -0
- package/src/send.ts +418 -0
- package/src/targets.test.ts +184 -0
- package/src/targets.ts +323 -0
- package/src/types.ts +127 -0
package/src/actions.ts
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BLUEBUBBLES_ACTION_NAMES,
|
|
3
|
+
BLUEBUBBLES_ACTIONS,
|
|
4
|
+
createActionGate,
|
|
5
|
+
jsonResult,
|
|
6
|
+
readNumberParam,
|
|
7
|
+
readReactionParams,
|
|
8
|
+
readStringParam,
|
|
9
|
+
type ChannelMessageActionAdapter,
|
|
10
|
+
type ChannelMessageActionName,
|
|
11
|
+
type ChannelToolSend,
|
|
12
|
+
type OpenClawConfig,
|
|
13
|
+
} from "openclaw/plugin-sdk";
|
|
14
|
+
|
|
15
|
+
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";
|
|
20
|
+
import {
|
|
21
|
+
editBlueBubblesMessage,
|
|
22
|
+
unsendBlueBubblesMessage,
|
|
23
|
+
renameBlueBubblesChat,
|
|
24
|
+
setGroupIconBlueBubbles,
|
|
25
|
+
addBlueBubblesParticipant,
|
|
26
|
+
removeBlueBubblesParticipant,
|
|
27
|
+
leaveBlueBubblesChat,
|
|
28
|
+
} from "./chat.js";
|
|
29
|
+
import { sendBlueBubblesAttachment } from "./attachments.js";
|
|
30
|
+
import { normalizeBlueBubblesHandle, parseBlueBubblesTarget } from "./targets.js";
|
|
31
|
+
import type { BlueBubblesSendTarget } from "./types.js";
|
|
32
|
+
|
|
33
|
+
const providerId = "bluebubbles";
|
|
34
|
+
|
|
35
|
+
function mapTarget(raw: string): BlueBubblesSendTarget {
|
|
36
|
+
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 };
|
|
39
|
+
if (parsed.kind === "chat_identifier") {
|
|
40
|
+
return { kind: "chat_identifier", chatIdentifier: parsed.chatIdentifier };
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
kind: "handle",
|
|
44
|
+
address: normalizeBlueBubblesHandle(parsed.to),
|
|
45
|
+
service: parsed.service,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readMessageText(params: Record<string, unknown>): string | undefined {
|
|
50
|
+
return readStringParam(params, "text") ?? readStringParam(params, "message");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readBooleanParam(params: Record<string, unknown>, key: string): boolean | undefined {
|
|
54
|
+
const raw = params[key];
|
|
55
|
+
if (typeof raw === "boolean") return raw;
|
|
56
|
+
if (typeof raw === "string") {
|
|
57
|
+
const trimmed = raw.trim().toLowerCase();
|
|
58
|
+
if (trimmed === "true") return true;
|
|
59
|
+
if (trimmed === "false") return false;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Supported action names for BlueBubbles */
|
|
65
|
+
const SUPPORTED_ACTIONS = new Set<ChannelMessageActionName>(BLUEBUBBLES_ACTION_NAMES);
|
|
66
|
+
|
|
67
|
+
export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
|
68
|
+
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);
|
|
72
|
+
const actions = new Set<ChannelMessageActionName>();
|
|
73
|
+
const macOS26 = isMacOS26OrHigher(account.accountId);
|
|
74
|
+
for (const action of BLUEBUBBLES_ACTION_NAMES) {
|
|
75
|
+
const spec = BLUEBUBBLES_ACTIONS[action];
|
|
76
|
+
if (!spec?.gate) continue;
|
|
77
|
+
if (spec.unsupportedOnMacOS26 && macOS26) continue;
|
|
78
|
+
if (gate(spec.gate)) actions.add(action);
|
|
79
|
+
}
|
|
80
|
+
return Array.from(actions);
|
|
81
|
+
},
|
|
82
|
+
supportsAction: ({ action }) => SUPPORTED_ACTIONS.has(action),
|
|
83
|
+
extractToolSend: ({ args }): ChannelToolSend | null => {
|
|
84
|
+
const action = typeof args.action === "string" ? args.action.trim() : "";
|
|
85
|
+
if (action !== "sendMessage") return null;
|
|
86
|
+
const to = typeof args.to === "string" ? args.to : undefined;
|
|
87
|
+
if (!to) return null;
|
|
88
|
+
const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined;
|
|
89
|
+
return { to, accountId };
|
|
90
|
+
},
|
|
91
|
+
handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
|
|
92
|
+
const account = resolveBlueBubblesAccount({
|
|
93
|
+
cfg: cfg as OpenClawConfig,
|
|
94
|
+
accountId: accountId ?? undefined,
|
|
95
|
+
});
|
|
96
|
+
const baseUrl = account.config.serverUrl?.trim();
|
|
97
|
+
const password = account.config.password?.trim();
|
|
98
|
+
const opts = { cfg: cfg as OpenClawConfig, accountId: accountId ?? undefined };
|
|
99
|
+
|
|
100
|
+
// Helper to resolve chatGuid from various params or session context
|
|
101
|
+
const resolveChatGuid = async (): Promise<string> => {
|
|
102
|
+
const chatGuid = readStringParam(params, "chatGuid");
|
|
103
|
+
if (chatGuid?.trim()) return chatGuid.trim();
|
|
104
|
+
|
|
105
|
+
const chatIdentifier = readStringParam(params, "chatIdentifier");
|
|
106
|
+
const chatId = readNumberParam(params, "chatId", { integer: true });
|
|
107
|
+
const to = readStringParam(params, "to");
|
|
108
|
+
// Fall back to session context if no explicit target provided
|
|
109
|
+
const contextTarget = toolContext?.currentChannelId?.trim();
|
|
110
|
+
|
|
111
|
+
const target = chatIdentifier?.trim()
|
|
112
|
+
? ({
|
|
113
|
+
kind: "chat_identifier",
|
|
114
|
+
chatIdentifier: chatIdentifier.trim(),
|
|
115
|
+
} as BlueBubblesSendTarget)
|
|
116
|
+
: typeof chatId === "number"
|
|
117
|
+
? ({ kind: "chat_id", chatId } as BlueBubblesSendTarget)
|
|
118
|
+
: to
|
|
119
|
+
? mapTarget(to)
|
|
120
|
+
: contextTarget
|
|
121
|
+
? mapTarget(contextTarget)
|
|
122
|
+
: null;
|
|
123
|
+
|
|
124
|
+
if (!target) {
|
|
125
|
+
throw new Error(`BlueBubbles ${action} requires chatGuid, chatIdentifier, chatId, or to.`);
|
|
126
|
+
}
|
|
127
|
+
if (!baseUrl || !password) {
|
|
128
|
+
throw new Error(`BlueBubbles ${action} requires serverUrl and password.`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const resolved = await resolveChatGuidForTarget({ baseUrl, password, target });
|
|
132
|
+
if (!resolved) {
|
|
133
|
+
throw new Error(`BlueBubbles ${action} failed: chatGuid not found for target.`);
|
|
134
|
+
}
|
|
135
|
+
return resolved;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Handle react action
|
|
139
|
+
if (action === "react") {
|
|
140
|
+
const { emoji, remove, isEmpty } = readReactionParams(params, {
|
|
141
|
+
removeErrorMessage: "Emoji is required to remove a BlueBubbles reaction.",
|
|
142
|
+
});
|
|
143
|
+
if (isEmpty && !remove) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
"BlueBubbles react requires emoji parameter. Use action=react with emoji=<emoji> and messageId=<message_id>.",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const rawMessageId = readStringParam(params, "messageId");
|
|
149
|
+
if (!rawMessageId) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
"BlueBubbles react requires messageId parameter (the message ID to react to). " +
|
|
152
|
+
"Use action=react with messageId=<message_id>, emoji=<emoji>, and to/chatGuid to identify the chat.",
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
// Resolve short ID (e.g., "1", "2") to full UUID
|
|
156
|
+
const messageId = resolveBlueBubblesMessageId(rawMessageId, { requireKnownShortId: true });
|
|
157
|
+
const partIndex = readNumberParam(params, "partIndex", { integer: true });
|
|
158
|
+
const resolvedChatGuid = await resolveChatGuid();
|
|
159
|
+
|
|
160
|
+
await sendBlueBubblesReaction({
|
|
161
|
+
chatGuid: resolvedChatGuid,
|
|
162
|
+
messageGuid: messageId,
|
|
163
|
+
emoji,
|
|
164
|
+
remove: remove || undefined,
|
|
165
|
+
partIndex: typeof partIndex === "number" ? partIndex : undefined,
|
|
166
|
+
opts,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return jsonResult({ ok: true, ...(remove ? { removed: true } : { added: emoji }) });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Handle edit action
|
|
173
|
+
if (action === "edit") {
|
|
174
|
+
// Edit is not supported on macOS 26+
|
|
175
|
+
if (isMacOS26OrHigher(accountId ?? undefined)) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
"BlueBubbles edit is not supported on macOS 26 or higher. " +
|
|
178
|
+
"Apple removed the ability to edit iMessages in this version.",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const rawMessageId = readStringParam(params, "messageId");
|
|
182
|
+
const newText =
|
|
183
|
+
readStringParam(params, "text") ??
|
|
184
|
+
readStringParam(params, "newText") ??
|
|
185
|
+
readStringParam(params, "message");
|
|
186
|
+
if (!rawMessageId || !newText) {
|
|
187
|
+
const missing: string[] = [];
|
|
188
|
+
if (!rawMessageId) missing.push("messageId (the message ID to edit)");
|
|
189
|
+
if (!newText) missing.push("text (the new message content)");
|
|
190
|
+
throw new Error(
|
|
191
|
+
`BlueBubbles edit requires: ${missing.join(", ")}. ` +
|
|
192
|
+
`Use action=edit with messageId=<message_id>, text=<new_content>.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
// Resolve short ID (e.g., "1", "2") to full UUID
|
|
196
|
+
const messageId = resolveBlueBubblesMessageId(rawMessageId, { requireKnownShortId: true });
|
|
197
|
+
const partIndex = readNumberParam(params, "partIndex", { integer: true });
|
|
198
|
+
const backwardsCompatMessage = readStringParam(params, "backwardsCompatMessage");
|
|
199
|
+
|
|
200
|
+
await editBlueBubblesMessage(messageId, newText, {
|
|
201
|
+
...opts,
|
|
202
|
+
partIndex: typeof partIndex === "number" ? partIndex : undefined,
|
|
203
|
+
backwardsCompatMessage: backwardsCompatMessage ?? undefined,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return jsonResult({ ok: true, edited: rawMessageId });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Handle unsend action
|
|
210
|
+
if (action === "unsend") {
|
|
211
|
+
const rawMessageId = readStringParam(params, "messageId");
|
|
212
|
+
if (!rawMessageId) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
"BlueBubbles unsend requires messageId parameter (the message ID to unsend). " +
|
|
215
|
+
"Use action=unsend with messageId=<message_id>.",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
// Resolve short ID (e.g., "1", "2") to full UUID
|
|
219
|
+
const messageId = resolveBlueBubblesMessageId(rawMessageId, { requireKnownShortId: true });
|
|
220
|
+
const partIndex = readNumberParam(params, "partIndex", { integer: true });
|
|
221
|
+
|
|
222
|
+
await unsendBlueBubblesMessage(messageId, {
|
|
223
|
+
...opts,
|
|
224
|
+
partIndex: typeof partIndex === "number" ? partIndex : undefined,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return jsonResult({ ok: true, unsent: rawMessageId });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Handle reply action
|
|
231
|
+
if (action === "reply") {
|
|
232
|
+
const rawMessageId = readStringParam(params, "messageId");
|
|
233
|
+
const text = readMessageText(params);
|
|
234
|
+
const to = readStringParam(params, "to") ?? readStringParam(params, "target");
|
|
235
|
+
if (!rawMessageId || !text || !to) {
|
|
236
|
+
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)");
|
|
240
|
+
throw new Error(
|
|
241
|
+
`BlueBubbles reply requires: ${missing.join(", ")}. ` +
|
|
242
|
+
`Use action=reply with messageId=<message_id>, message=<your reply>, target=<chat_target>.`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
// Resolve short ID (e.g., "1", "2") to full UUID
|
|
246
|
+
const messageId = resolveBlueBubblesMessageId(rawMessageId, { requireKnownShortId: true });
|
|
247
|
+
const partIndex = readNumberParam(params, "partIndex", { integer: true });
|
|
248
|
+
|
|
249
|
+
const result = await sendMessageBlueBubbles(to, text, {
|
|
250
|
+
...opts,
|
|
251
|
+
replyToMessageGuid: messageId,
|
|
252
|
+
replyToPartIndex: typeof partIndex === "number" ? partIndex : undefined,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return jsonResult({ ok: true, messageId: result.messageId, repliedTo: rawMessageId });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Handle sendWithEffect action
|
|
259
|
+
if (action === "sendWithEffect") {
|
|
260
|
+
const text = readMessageText(params);
|
|
261
|
+
const to = readStringParam(params, "to") ?? readStringParam(params, "target");
|
|
262
|
+
const effectId = readStringParam(params, "effectId") ?? readStringParam(params, "effect");
|
|
263
|
+
if (!text || !to || !effectId) {
|
|
264
|
+
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)
|
|
268
|
+
missing.push(
|
|
269
|
+
"effectId or effect (e.g., slam, loud, gentle, invisible-ink, confetti, lasers, fireworks, balloons, heart)",
|
|
270
|
+
);
|
|
271
|
+
throw new Error(
|
|
272
|
+
`BlueBubbles sendWithEffect requires: ${missing.join(", ")}. ` +
|
|
273
|
+
`Use action=sendWithEffect with message=<message>, target=<chat_target>, effectId=<effect_name>.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const result = await sendMessageBlueBubbles(to, text, {
|
|
278
|
+
...opts,
|
|
279
|
+
effectId,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return jsonResult({ ok: true, messageId: result.messageId, effect: effectId });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Handle renameGroup action
|
|
286
|
+
if (action === "renameGroup") {
|
|
287
|
+
const resolvedChatGuid = await resolveChatGuid();
|
|
288
|
+
const displayName = readStringParam(params, "displayName") ?? readStringParam(params, "name");
|
|
289
|
+
if (!displayName) {
|
|
290
|
+
throw new Error("BlueBubbles renameGroup requires displayName or name parameter.");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await renameBlueBubblesChat(resolvedChatGuid, displayName, opts);
|
|
294
|
+
|
|
295
|
+
return jsonResult({ ok: true, renamed: resolvedChatGuid, displayName });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Handle setGroupIcon action
|
|
299
|
+
if (action === "setGroupIcon") {
|
|
300
|
+
const resolvedChatGuid = await resolveChatGuid();
|
|
301
|
+
const base64Buffer = readStringParam(params, "buffer");
|
|
302
|
+
const filename =
|
|
303
|
+
readStringParam(params, "filename") ??
|
|
304
|
+
readStringParam(params, "name") ??
|
|
305
|
+
"icon.png";
|
|
306
|
+
const contentType =
|
|
307
|
+
readStringParam(params, "contentType") ?? readStringParam(params, "mimeType");
|
|
308
|
+
|
|
309
|
+
if (!base64Buffer) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
"BlueBubbles setGroupIcon requires an image. " +
|
|
312
|
+
"Use action=setGroupIcon with media=<image_url> or path=<local_file_path> to set the group icon.",
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Decode base64 to buffer
|
|
317
|
+
const buffer = Uint8Array.from(atob(base64Buffer), (c) => c.charCodeAt(0));
|
|
318
|
+
|
|
319
|
+
await setGroupIconBlueBubbles(resolvedChatGuid, buffer, filename, {
|
|
320
|
+
...opts,
|
|
321
|
+
contentType: contentType ?? undefined,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return jsonResult({ ok: true, chatGuid: resolvedChatGuid, iconSet: true });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Handle addParticipant action
|
|
328
|
+
if (action === "addParticipant") {
|
|
329
|
+
const resolvedChatGuid = await resolveChatGuid();
|
|
330
|
+
const address = readStringParam(params, "address") ?? readStringParam(params, "participant");
|
|
331
|
+
if (!address) {
|
|
332
|
+
throw new Error("BlueBubbles addParticipant requires address or participant parameter.");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
await addBlueBubblesParticipant(resolvedChatGuid, address, opts);
|
|
336
|
+
|
|
337
|
+
return jsonResult({ ok: true, added: address, chatGuid: resolvedChatGuid });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Handle removeParticipant action
|
|
341
|
+
if (action === "removeParticipant") {
|
|
342
|
+
const resolvedChatGuid = await resolveChatGuid();
|
|
343
|
+
const address = readStringParam(params, "address") ?? readStringParam(params, "participant");
|
|
344
|
+
if (!address) {
|
|
345
|
+
throw new Error("BlueBubbles removeParticipant requires address or participant parameter.");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await removeBlueBubblesParticipant(resolvedChatGuid, address, opts);
|
|
349
|
+
|
|
350
|
+
return jsonResult({ ok: true, removed: address, chatGuid: resolvedChatGuid });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Handle leaveGroup action
|
|
354
|
+
if (action === "leaveGroup") {
|
|
355
|
+
const resolvedChatGuid = await resolveChatGuid();
|
|
356
|
+
|
|
357
|
+
await leaveBlueBubblesChat(resolvedChatGuid, opts);
|
|
358
|
+
|
|
359
|
+
return jsonResult({ ok: true, left: resolvedChatGuid });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Handle sendAttachment action
|
|
363
|
+
if (action === "sendAttachment") {
|
|
364
|
+
const to = readStringParam(params, "to", { required: true });
|
|
365
|
+
const filename = readStringParam(params, "filename", { required: true });
|
|
366
|
+
const caption = readStringParam(params, "caption");
|
|
367
|
+
const contentType =
|
|
368
|
+
readStringParam(params, "contentType") ?? readStringParam(params, "mimeType");
|
|
369
|
+
const asVoice = readBooleanParam(params, "asVoice");
|
|
370
|
+
|
|
371
|
+
// Buffer can come from params.buffer (base64) or params.path (file path)
|
|
372
|
+
const base64Buffer = readStringParam(params, "buffer");
|
|
373
|
+
const filePath = readStringParam(params, "path") ?? readStringParam(params, "filePath");
|
|
374
|
+
|
|
375
|
+
let buffer: Uint8Array;
|
|
376
|
+
if (base64Buffer) {
|
|
377
|
+
// Decode base64 to buffer
|
|
378
|
+
buffer = Uint8Array.from(atob(base64Buffer), (c) => c.charCodeAt(0));
|
|
379
|
+
} else if (filePath) {
|
|
380
|
+
// Read file from path (will be handled by caller providing buffer)
|
|
381
|
+
throw new Error(
|
|
382
|
+
"BlueBubbles sendAttachment: filePath not supported in action, provide buffer as base64.",
|
|
383
|
+
);
|
|
384
|
+
} else {
|
|
385
|
+
throw new Error("BlueBubbles sendAttachment requires buffer (base64) parameter.");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const result = await sendBlueBubblesAttachment({
|
|
389
|
+
to,
|
|
390
|
+
buffer,
|
|
391
|
+
filename,
|
|
392
|
+
contentType: contentType ?? undefined,
|
|
393
|
+
caption: caption ?? undefined,
|
|
394
|
+
asVoice: asVoice ?? undefined,
|
|
395
|
+
opts,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
return jsonResult({ ok: true, messageId: result.messageId });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
|
|
402
|
+
},
|
|
403
|
+
};
|