@openclaw/bluebubbles 2026.2.25 → 2026.3.2
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 +0 -2
- package/package.json +1 -1
- package/src/account-resolve.ts +19 -2
- package/src/accounts.test.ts +25 -0
- package/src/accounts.ts +18 -5
- package/src/actions.ts +4 -19
- package/src/attachments.test.ts +20 -2
- package/src/attachments.ts +15 -1
- package/src/channel.ts +3 -10
- package/src/config-schema.test.ts +12 -0
- package/src/config-schema.ts +5 -3
- package/src/monitor-debounce.ts +205 -0
- package/src/monitor-processing.ts +43 -22
- package/src/monitor.test.ts +87 -717
- package/src/monitor.ts +157 -364
- package/src/monitor.webhook-auth.test.ts +862 -0
- package/src/monitor.webhook-route.test.ts +44 -0
- package/src/onboarding.secret-input.test.ts +81 -0
- package/src/onboarding.ts +8 -2
- package/src/probe.ts +5 -4
- package/src/secret-input.ts +19 -0
- package/src/send-helpers.ts +34 -22
- package/src/send.test.ts +24 -0
- package/src/send.ts +7 -2
- package/src/targets.ts +2 -5
- package/src/types.ts +2 -0
package/src/monitor.test.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
3
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
4
|
-
import { removeAckReactionAfterReply, shouldAckReaction } from "openclaw/plugin-sdk";
|
|
5
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
|
|
6
6
|
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
|
|
7
7
|
import { fetchBlueBubblesHistory } from "./history.js";
|
|
8
8
|
import {
|
|
@@ -50,8 +50,11 @@ const mockReadAllowFromStore = vi.fn().mockResolvedValue([]);
|
|
|
50
50
|
const mockUpsertPairingRequest = vi.fn().mockResolvedValue({ code: "TESTCODE", created: true });
|
|
51
51
|
const mockResolveAgentRoute = vi.fn(() => ({
|
|
52
52
|
agentId: "main",
|
|
53
|
+
channel: "bluebubbles",
|
|
53
54
|
accountId: "default",
|
|
54
55
|
sessionKey: "agent:main:bluebubbles:dm:+15551234567",
|
|
56
|
+
mainSessionKey: "agent:main:main",
|
|
57
|
+
matchedBy: "default",
|
|
55
58
|
}));
|
|
56
59
|
const mockBuildMentionRegexes = vi.fn(() => [/\bbert\b/i]);
|
|
57
60
|
const mockMatchesMentionPatterns = vi.fn((text: string, regexes: RegExp[]) =>
|
|
@@ -66,109 +69,57 @@ const mockMatchesMentionWithExplicit = vi.fn(
|
|
|
66
69
|
},
|
|
67
70
|
);
|
|
68
71
|
const mockResolveRequireMention = vi.fn(() => false);
|
|
69
|
-
const mockResolveGroupPolicy = vi.fn(() => "open");
|
|
72
|
+
const mockResolveGroupPolicy = vi.fn(() => "open" as const);
|
|
70
73
|
type DispatchReplyParams = Parameters<
|
|
71
74
|
PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"]
|
|
72
75
|
>[0];
|
|
76
|
+
const EMPTY_DISPATCH_RESULT = {
|
|
77
|
+
queuedFinal: false,
|
|
78
|
+
counts: { tool: 0, block: 0, final: 0 },
|
|
79
|
+
} as const;
|
|
73
80
|
const mockDispatchReplyWithBufferedBlockDispatcher = vi.fn(
|
|
74
|
-
async (_params: DispatchReplyParams)
|
|
81
|
+
async (_params: DispatchReplyParams) => EMPTY_DISPATCH_RESULT,
|
|
75
82
|
);
|
|
76
83
|
const mockHasControlCommand = vi.fn(() => false);
|
|
77
84
|
const mockResolveCommandAuthorizedFromAuthorizers = vi.fn(() => false);
|
|
78
85
|
const mockSaveMediaBuffer = vi.fn().mockResolvedValue({
|
|
86
|
+
id: "test-media.jpg",
|
|
79
87
|
path: "/tmp/test-media.jpg",
|
|
88
|
+
size: Buffer.byteLength("test"),
|
|
80
89
|
contentType: "image/jpeg",
|
|
81
90
|
});
|
|
82
91
|
const mockResolveStorePath = vi.fn(() => "/tmp/sessions.json");
|
|
83
92
|
const mockReadSessionUpdatedAt = vi.fn(() => undefined);
|
|
84
|
-
const mockResolveEnvelopeFormatOptions = vi.fn(() => ({
|
|
85
|
-
template: "channel+name+time",
|
|
86
|
-
}));
|
|
93
|
+
const mockResolveEnvelopeFormatOptions = vi.fn(() => ({}));
|
|
87
94
|
const mockFormatAgentEnvelope = vi.fn((opts: { body: string }) => opts.body);
|
|
88
95
|
const mockFormatInboundEnvelope = vi.fn((opts: { body: string }) => opts.body);
|
|
89
96
|
const mockChunkMarkdownText = vi.fn((text: string) => [text]);
|
|
90
97
|
const mockChunkByNewline = vi.fn((text: string) => (text ? [text] : []));
|
|
91
98
|
const mockChunkTextWithMode = vi.fn((text: string) => (text ? [text] : []));
|
|
92
99
|
const mockChunkMarkdownTextWithMode = vi.fn((text: string) => (text ? [text] : []));
|
|
93
|
-
const mockResolveChunkMode = vi.fn(() => "length");
|
|
100
|
+
const mockResolveChunkMode = vi.fn(() => "length" as const);
|
|
94
101
|
const mockFetchBlueBubblesHistory = vi.mocked(fetchBlueBubblesHistory);
|
|
95
102
|
|
|
96
103
|
function createMockRuntime(): PluginRuntime {
|
|
97
|
-
return {
|
|
98
|
-
version: "1.0.0",
|
|
99
|
-
config: {
|
|
100
|
-
loadConfig: vi.fn(() => ({})) as unknown as PluginRuntime["config"]["loadConfig"],
|
|
101
|
-
writeConfigFile: vi.fn() as unknown as PluginRuntime["config"]["writeConfigFile"],
|
|
102
|
-
},
|
|
104
|
+
return createPluginRuntimeMock({
|
|
103
105
|
system: {
|
|
104
|
-
enqueueSystemEvent:
|
|
105
|
-
mockEnqueueSystemEvent as unknown as PluginRuntime["system"]["enqueueSystemEvent"],
|
|
106
|
-
runCommandWithTimeout: vi.fn() as unknown as PluginRuntime["system"]["runCommandWithTimeout"],
|
|
107
|
-
formatNativeDependencyHint: vi.fn(
|
|
108
|
-
() => "",
|
|
109
|
-
) as unknown as PluginRuntime["system"]["formatNativeDependencyHint"],
|
|
110
|
-
},
|
|
111
|
-
media: {
|
|
112
|
-
loadWebMedia: vi.fn() as unknown as PluginRuntime["media"]["loadWebMedia"],
|
|
113
|
-
detectMime: vi.fn() as unknown as PluginRuntime["media"]["detectMime"],
|
|
114
|
-
mediaKindFromMime: vi.fn() as unknown as PluginRuntime["media"]["mediaKindFromMime"],
|
|
115
|
-
isVoiceCompatibleAudio:
|
|
116
|
-
vi.fn() as unknown as PluginRuntime["media"]["isVoiceCompatibleAudio"],
|
|
117
|
-
getImageMetadata: vi.fn() as unknown as PluginRuntime["media"]["getImageMetadata"],
|
|
118
|
-
resizeToJpeg: vi.fn() as unknown as PluginRuntime["media"]["resizeToJpeg"],
|
|
119
|
-
},
|
|
120
|
-
tts: {
|
|
121
|
-
textToSpeechTelephony: vi.fn() as unknown as PluginRuntime["tts"]["textToSpeechTelephony"],
|
|
122
|
-
},
|
|
123
|
-
tools: {
|
|
124
|
-
createMemoryGetTool: vi.fn() as unknown as PluginRuntime["tools"]["createMemoryGetTool"],
|
|
125
|
-
createMemorySearchTool:
|
|
126
|
-
vi.fn() as unknown as PluginRuntime["tools"]["createMemorySearchTool"],
|
|
127
|
-
registerMemoryCli: vi.fn() as unknown as PluginRuntime["tools"]["registerMemoryCli"],
|
|
106
|
+
enqueueSystemEvent: mockEnqueueSystemEvent,
|
|
128
107
|
},
|
|
129
108
|
channel: {
|
|
130
109
|
text: {
|
|
131
|
-
chunkMarkdownText:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
mockChunkByNewline as unknown as PluginRuntime["channel"]["text"]["chunkByNewline"],
|
|
136
|
-
chunkMarkdownTextWithMode:
|
|
137
|
-
mockChunkMarkdownTextWithMode as unknown as PluginRuntime["channel"]["text"]["chunkMarkdownTextWithMode"],
|
|
138
|
-
chunkTextWithMode:
|
|
139
|
-
mockChunkTextWithMode as unknown as PluginRuntime["channel"]["text"]["chunkTextWithMode"],
|
|
110
|
+
chunkMarkdownText: mockChunkMarkdownText,
|
|
111
|
+
chunkByNewline: mockChunkByNewline,
|
|
112
|
+
chunkMarkdownTextWithMode: mockChunkMarkdownTextWithMode,
|
|
113
|
+
chunkTextWithMode: mockChunkTextWithMode,
|
|
140
114
|
resolveChunkMode:
|
|
141
115
|
mockResolveChunkMode as unknown as PluginRuntime["channel"]["text"]["resolveChunkMode"],
|
|
142
|
-
|
|
143
|
-
() => 4000,
|
|
144
|
-
) as unknown as PluginRuntime["channel"]["text"]["resolveTextChunkLimit"],
|
|
145
|
-
hasControlCommand:
|
|
146
|
-
mockHasControlCommand as unknown as PluginRuntime["channel"]["text"]["hasControlCommand"],
|
|
147
|
-
resolveMarkdownTableMode: vi.fn(
|
|
148
|
-
() => "code",
|
|
149
|
-
) as unknown as PluginRuntime["channel"]["text"]["resolveMarkdownTableMode"],
|
|
150
|
-
convertMarkdownTables: vi.fn(
|
|
151
|
-
(text: string) => text,
|
|
152
|
-
) as unknown as PluginRuntime["channel"]["text"]["convertMarkdownTables"],
|
|
116
|
+
hasControlCommand: mockHasControlCommand,
|
|
153
117
|
},
|
|
154
118
|
reply: {
|
|
155
119
|
dispatchReplyWithBufferedBlockDispatcher:
|
|
156
120
|
mockDispatchReplyWithBufferedBlockDispatcher as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
resolveEffectiveMessagesConfig:
|
|
160
|
-
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveEffectiveMessagesConfig"],
|
|
161
|
-
resolveHumanDelayConfig:
|
|
162
|
-
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveHumanDelayConfig"],
|
|
163
|
-
dispatchReplyFromConfig:
|
|
164
|
-
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyFromConfig"],
|
|
165
|
-
finalizeInboundContext: vi.fn(
|
|
166
|
-
(ctx: Record<string, unknown>) => ctx,
|
|
167
|
-
) as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
|
|
168
|
-
formatAgentEnvelope:
|
|
169
|
-
mockFormatAgentEnvelope as unknown as PluginRuntime["channel"]["reply"]["formatAgentEnvelope"],
|
|
170
|
-
formatInboundEnvelope:
|
|
171
|
-
mockFormatInboundEnvelope as unknown as PluginRuntime["channel"]["reply"]["formatInboundEnvelope"],
|
|
121
|
+
formatAgentEnvelope: mockFormatAgentEnvelope,
|
|
122
|
+
formatInboundEnvelope: mockFormatInboundEnvelope,
|
|
172
123
|
resolveEnvelopeFormatOptions:
|
|
173
124
|
mockResolveEnvelopeFormatOptions as unknown as PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"],
|
|
174
125
|
},
|
|
@@ -177,99 +128,33 @@ function createMockRuntime(): PluginRuntime {
|
|
|
177
128
|
mockResolveAgentRoute as unknown as PluginRuntime["channel"]["routing"]["resolveAgentRoute"],
|
|
178
129
|
},
|
|
179
130
|
pairing: {
|
|
180
|
-
buildPairingReply:
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
mockReadAllowFromStore as unknown as PluginRuntime["channel"]["pairing"]["readAllowFromStore"],
|
|
184
|
-
upsertPairingRequest:
|
|
185
|
-
mockUpsertPairingRequest as unknown as PluginRuntime["channel"]["pairing"]["upsertPairingRequest"],
|
|
131
|
+
buildPairingReply: mockBuildPairingReply,
|
|
132
|
+
readAllowFromStore: mockReadAllowFromStore,
|
|
133
|
+
upsertPairingRequest: mockUpsertPairingRequest,
|
|
186
134
|
},
|
|
187
135
|
media: {
|
|
188
|
-
fetchRemoteMedia:
|
|
189
|
-
vi.fn() as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"],
|
|
190
136
|
saveMediaBuffer:
|
|
191
137
|
mockSaveMediaBuffer as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
|
|
192
138
|
},
|
|
193
139
|
session: {
|
|
194
|
-
resolveStorePath:
|
|
195
|
-
|
|
196
|
-
readSessionUpdatedAt:
|
|
197
|
-
mockReadSessionUpdatedAt as unknown as PluginRuntime["channel"]["session"]["readSessionUpdatedAt"],
|
|
198
|
-
recordInboundSession:
|
|
199
|
-
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
|
|
200
|
-
recordSessionMetaFromInbound:
|
|
201
|
-
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordSessionMetaFromInbound"],
|
|
202
|
-
updateLastRoute:
|
|
203
|
-
vi.fn() as unknown as PluginRuntime["channel"]["session"]["updateLastRoute"],
|
|
140
|
+
resolveStorePath: mockResolveStorePath,
|
|
141
|
+
readSessionUpdatedAt: mockReadSessionUpdatedAt,
|
|
204
142
|
},
|
|
205
143
|
mentions: {
|
|
206
|
-
buildMentionRegexes:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
mockMatchesMentionPatterns as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionPatterns"],
|
|
210
|
-
matchesMentionWithExplicit:
|
|
211
|
-
mockMatchesMentionWithExplicit as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionWithExplicit"],
|
|
212
|
-
},
|
|
213
|
-
reactions: {
|
|
214
|
-
shouldAckReaction,
|
|
215
|
-
removeAckReactionAfterReply,
|
|
144
|
+
buildMentionRegexes: mockBuildMentionRegexes,
|
|
145
|
+
matchesMentionPatterns: mockMatchesMentionPatterns,
|
|
146
|
+
matchesMentionWithExplicit: mockMatchesMentionWithExplicit,
|
|
216
147
|
},
|
|
217
148
|
groups: {
|
|
218
149
|
resolveGroupPolicy:
|
|
219
150
|
mockResolveGroupPolicy as unknown as PluginRuntime["channel"]["groups"]["resolveGroupPolicy"],
|
|
220
|
-
resolveRequireMention:
|
|
221
|
-
mockResolveRequireMention as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"],
|
|
222
|
-
},
|
|
223
|
-
debounce: {
|
|
224
|
-
// Create a pass-through debouncer that immediately calls onFlush
|
|
225
|
-
createInboundDebouncer: vi.fn(
|
|
226
|
-
(params: { onFlush: (items: unknown[]) => Promise<void> }) => ({
|
|
227
|
-
enqueue: async (item: unknown) => {
|
|
228
|
-
await params.onFlush([item]);
|
|
229
|
-
},
|
|
230
|
-
flushKey: vi.fn(),
|
|
231
|
-
}),
|
|
232
|
-
) as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"],
|
|
233
|
-
resolveInboundDebounceMs: vi.fn(
|
|
234
|
-
() => 0,
|
|
235
|
-
) as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"],
|
|
151
|
+
resolveRequireMention: mockResolveRequireMention,
|
|
236
152
|
},
|
|
237
153
|
commands: {
|
|
238
|
-
resolveCommandAuthorizedFromAuthorizers:
|
|
239
|
-
mockResolveCommandAuthorizedFromAuthorizers as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
|
|
240
|
-
isControlCommandMessage:
|
|
241
|
-
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["isControlCommandMessage"],
|
|
242
|
-
shouldComputeCommandAuthorized:
|
|
243
|
-
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldComputeCommandAuthorized"],
|
|
244
|
-
shouldHandleTextCommands:
|
|
245
|
-
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldHandleTextCommands"],
|
|
154
|
+
resolveCommandAuthorizedFromAuthorizers: mockResolveCommandAuthorizedFromAuthorizers,
|
|
246
155
|
},
|
|
247
|
-
discord: {} as PluginRuntime["channel"]["discord"],
|
|
248
|
-
activity: {} as PluginRuntime["channel"]["activity"],
|
|
249
|
-
line: {} as PluginRuntime["channel"]["line"],
|
|
250
|
-
slack: {} as PluginRuntime["channel"]["slack"],
|
|
251
|
-
telegram: {} as PluginRuntime["channel"]["telegram"],
|
|
252
|
-
signal: {} as PluginRuntime["channel"]["signal"],
|
|
253
|
-
imessage: {} as PluginRuntime["channel"]["imessage"],
|
|
254
|
-
whatsapp: {} as PluginRuntime["channel"]["whatsapp"],
|
|
255
|
-
},
|
|
256
|
-
logging: {
|
|
257
|
-
shouldLogVerbose: vi.fn(
|
|
258
|
-
() => false,
|
|
259
|
-
) as unknown as PluginRuntime["logging"]["shouldLogVerbose"],
|
|
260
|
-
getChildLogger: vi.fn(() => ({
|
|
261
|
-
info: vi.fn(),
|
|
262
|
-
warn: vi.fn(),
|
|
263
|
-
error: vi.fn(),
|
|
264
|
-
debug: vi.fn(),
|
|
265
|
-
})) as unknown as PluginRuntime["logging"]["getChildLogger"],
|
|
266
|
-
},
|
|
267
|
-
state: {
|
|
268
|
-
resolveStateDir: vi.fn(
|
|
269
|
-
() => "/tmp/openclaw",
|
|
270
|
-
) as unknown as PluginRuntime["state"]["resolveStateDir"],
|
|
271
156
|
},
|
|
272
|
-
};
|
|
157
|
+
});
|
|
273
158
|
}
|
|
274
159
|
|
|
275
160
|
function createMockAccount(
|
|
@@ -376,573 +261,6 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
376
261
|
unregister?.();
|
|
377
262
|
});
|
|
378
263
|
|
|
379
|
-
describe("webhook parsing + auth handling", () => {
|
|
380
|
-
it("rejects non-POST requests", async () => {
|
|
381
|
-
const account = createMockAccount();
|
|
382
|
-
const config: OpenClawConfig = {};
|
|
383
|
-
const core = createMockRuntime();
|
|
384
|
-
setBlueBubblesRuntime(core);
|
|
385
|
-
|
|
386
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
387
|
-
account,
|
|
388
|
-
config,
|
|
389
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
390
|
-
core,
|
|
391
|
-
path: "/bluebubbles-webhook",
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const req = createMockRequest("GET", "/bluebubbles-webhook", {});
|
|
395
|
-
const res = createMockResponse();
|
|
396
|
-
|
|
397
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
398
|
-
|
|
399
|
-
expect(handled).toBe(true);
|
|
400
|
-
expect(res.statusCode).toBe(405);
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
it("accepts POST requests with valid JSON payload", async () => {
|
|
404
|
-
const account = createMockAccount();
|
|
405
|
-
const config: OpenClawConfig = {};
|
|
406
|
-
const core = createMockRuntime();
|
|
407
|
-
setBlueBubblesRuntime(core);
|
|
408
|
-
|
|
409
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
410
|
-
account,
|
|
411
|
-
config,
|
|
412
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
413
|
-
core,
|
|
414
|
-
path: "/bluebubbles-webhook",
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
const payload = {
|
|
418
|
-
type: "new-message",
|
|
419
|
-
data: {
|
|
420
|
-
text: "hello",
|
|
421
|
-
handle: { address: "+15551234567" },
|
|
422
|
-
isGroup: false,
|
|
423
|
-
isFromMe: false,
|
|
424
|
-
guid: "msg-1",
|
|
425
|
-
date: Date.now(),
|
|
426
|
-
},
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
430
|
-
const res = createMockResponse();
|
|
431
|
-
|
|
432
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
433
|
-
|
|
434
|
-
expect(handled).toBe(true);
|
|
435
|
-
expect(res.statusCode).toBe(200);
|
|
436
|
-
expect(res.body).toBe("ok");
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
it("rejects requests with invalid JSON", async () => {
|
|
440
|
-
const account = createMockAccount();
|
|
441
|
-
const config: OpenClawConfig = {};
|
|
442
|
-
const core = createMockRuntime();
|
|
443
|
-
setBlueBubblesRuntime(core);
|
|
444
|
-
|
|
445
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
446
|
-
account,
|
|
447
|
-
config,
|
|
448
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
449
|
-
core,
|
|
450
|
-
path: "/bluebubbles-webhook",
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook", "invalid json {{");
|
|
454
|
-
const res = createMockResponse();
|
|
455
|
-
|
|
456
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
457
|
-
|
|
458
|
-
expect(handled).toBe(true);
|
|
459
|
-
expect(res.statusCode).toBe(400);
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
it("accepts URL-encoded payload wrappers", async () => {
|
|
463
|
-
const account = createMockAccount();
|
|
464
|
-
const config: OpenClawConfig = {};
|
|
465
|
-
const core = createMockRuntime();
|
|
466
|
-
setBlueBubblesRuntime(core);
|
|
467
|
-
|
|
468
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
469
|
-
account,
|
|
470
|
-
config,
|
|
471
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
472
|
-
core,
|
|
473
|
-
path: "/bluebubbles-webhook",
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
const payload = {
|
|
477
|
-
type: "new-message",
|
|
478
|
-
data: {
|
|
479
|
-
text: "hello",
|
|
480
|
-
handle: { address: "+15551234567" },
|
|
481
|
-
isGroup: false,
|
|
482
|
-
isFromMe: false,
|
|
483
|
-
guid: "msg-1",
|
|
484
|
-
date: Date.now(),
|
|
485
|
-
},
|
|
486
|
-
};
|
|
487
|
-
const encodedBody = new URLSearchParams({
|
|
488
|
-
payload: JSON.stringify(payload),
|
|
489
|
-
}).toString();
|
|
490
|
-
|
|
491
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook", encodedBody);
|
|
492
|
-
const res = createMockResponse();
|
|
493
|
-
|
|
494
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
495
|
-
|
|
496
|
-
expect(handled).toBe(true);
|
|
497
|
-
expect(res.statusCode).toBe(200);
|
|
498
|
-
expect(res.body).toBe("ok");
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
it("returns 408 when request body times out (Slow-Loris protection)", async () => {
|
|
502
|
-
vi.useFakeTimers();
|
|
503
|
-
try {
|
|
504
|
-
const account = createMockAccount();
|
|
505
|
-
const config: OpenClawConfig = {};
|
|
506
|
-
const core = createMockRuntime();
|
|
507
|
-
setBlueBubblesRuntime(core);
|
|
508
|
-
|
|
509
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
510
|
-
account,
|
|
511
|
-
config,
|
|
512
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
513
|
-
core,
|
|
514
|
-
path: "/bluebubbles-webhook",
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
// Create a request that never sends data or ends (simulates slow-loris)
|
|
518
|
-
const req = new EventEmitter() as IncomingMessage;
|
|
519
|
-
req.method = "POST";
|
|
520
|
-
req.url = "/bluebubbles-webhook";
|
|
521
|
-
req.headers = {};
|
|
522
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
523
|
-
remoteAddress: "127.0.0.1",
|
|
524
|
-
};
|
|
525
|
-
req.destroy = vi.fn();
|
|
526
|
-
|
|
527
|
-
const res = createMockResponse();
|
|
528
|
-
|
|
529
|
-
const handledPromise = handleBlueBubblesWebhookRequest(req, res);
|
|
530
|
-
|
|
531
|
-
// Advance past the 30s timeout
|
|
532
|
-
await vi.advanceTimersByTimeAsync(31_000);
|
|
533
|
-
|
|
534
|
-
const handled = await handledPromise;
|
|
535
|
-
expect(handled).toBe(true);
|
|
536
|
-
expect(res.statusCode).toBe(408);
|
|
537
|
-
expect(req.destroy).toHaveBeenCalled();
|
|
538
|
-
} finally {
|
|
539
|
-
vi.useRealTimers();
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
it("authenticates via password query parameter", async () => {
|
|
544
|
-
const account = createMockAccount({ password: "secret-token" });
|
|
545
|
-
const config: OpenClawConfig = {};
|
|
546
|
-
const core = createMockRuntime();
|
|
547
|
-
setBlueBubblesRuntime(core);
|
|
548
|
-
|
|
549
|
-
// Mock non-localhost request
|
|
550
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook?password=secret-token", {
|
|
551
|
-
type: "new-message",
|
|
552
|
-
data: {
|
|
553
|
-
text: "hello",
|
|
554
|
-
handle: { address: "+15551234567" },
|
|
555
|
-
isGroup: false,
|
|
556
|
-
isFromMe: false,
|
|
557
|
-
guid: "msg-1",
|
|
558
|
-
},
|
|
559
|
-
});
|
|
560
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
561
|
-
remoteAddress: "192.168.1.100",
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
565
|
-
account,
|
|
566
|
-
config,
|
|
567
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
568
|
-
core,
|
|
569
|
-
path: "/bluebubbles-webhook",
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
const res = createMockResponse();
|
|
573
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
574
|
-
|
|
575
|
-
expect(handled).toBe(true);
|
|
576
|
-
expect(res.statusCode).toBe(200);
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
it("authenticates via x-password header", async () => {
|
|
580
|
-
const account = createMockAccount({ password: "secret-token" });
|
|
581
|
-
const config: OpenClawConfig = {};
|
|
582
|
-
const core = createMockRuntime();
|
|
583
|
-
setBlueBubblesRuntime(core);
|
|
584
|
-
|
|
585
|
-
const req = createMockRequest(
|
|
586
|
-
"POST",
|
|
587
|
-
"/bluebubbles-webhook",
|
|
588
|
-
{
|
|
589
|
-
type: "new-message",
|
|
590
|
-
data: {
|
|
591
|
-
text: "hello",
|
|
592
|
-
handle: { address: "+15551234567" },
|
|
593
|
-
isGroup: false,
|
|
594
|
-
isFromMe: false,
|
|
595
|
-
guid: "msg-1",
|
|
596
|
-
},
|
|
597
|
-
},
|
|
598
|
-
{ "x-password": "secret-token" },
|
|
599
|
-
);
|
|
600
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
601
|
-
remoteAddress: "192.168.1.100",
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
605
|
-
account,
|
|
606
|
-
config,
|
|
607
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
608
|
-
core,
|
|
609
|
-
path: "/bluebubbles-webhook",
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
const res = createMockResponse();
|
|
613
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
614
|
-
|
|
615
|
-
expect(handled).toBe(true);
|
|
616
|
-
expect(res.statusCode).toBe(200);
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
it("rejects unauthorized requests with wrong password", async () => {
|
|
620
|
-
const account = createMockAccount({ password: "secret-token" });
|
|
621
|
-
const config: OpenClawConfig = {};
|
|
622
|
-
const core = createMockRuntime();
|
|
623
|
-
setBlueBubblesRuntime(core);
|
|
624
|
-
|
|
625
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook?password=wrong-token", {
|
|
626
|
-
type: "new-message",
|
|
627
|
-
data: {
|
|
628
|
-
text: "hello",
|
|
629
|
-
handle: { address: "+15551234567" },
|
|
630
|
-
isGroup: false,
|
|
631
|
-
isFromMe: false,
|
|
632
|
-
guid: "msg-1",
|
|
633
|
-
},
|
|
634
|
-
});
|
|
635
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
636
|
-
remoteAddress: "192.168.1.100",
|
|
637
|
-
};
|
|
638
|
-
|
|
639
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
640
|
-
account,
|
|
641
|
-
config,
|
|
642
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
643
|
-
core,
|
|
644
|
-
path: "/bluebubbles-webhook",
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
const res = createMockResponse();
|
|
648
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
649
|
-
|
|
650
|
-
expect(handled).toBe(true);
|
|
651
|
-
expect(res.statusCode).toBe(401);
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
it("rejects ambiguous routing when multiple targets match the same password", async () => {
|
|
655
|
-
const accountA = createMockAccount({ password: "secret-token" });
|
|
656
|
-
const accountB = createMockAccount({ password: "secret-token" });
|
|
657
|
-
const config: OpenClawConfig = {};
|
|
658
|
-
const core = createMockRuntime();
|
|
659
|
-
setBlueBubblesRuntime(core);
|
|
660
|
-
|
|
661
|
-
const sinkA = vi.fn();
|
|
662
|
-
const sinkB = vi.fn();
|
|
663
|
-
|
|
664
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook?password=secret-token", {
|
|
665
|
-
type: "new-message",
|
|
666
|
-
data: {
|
|
667
|
-
text: "hello",
|
|
668
|
-
handle: { address: "+15551234567" },
|
|
669
|
-
isGroup: false,
|
|
670
|
-
isFromMe: false,
|
|
671
|
-
guid: "msg-1",
|
|
672
|
-
},
|
|
673
|
-
});
|
|
674
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
675
|
-
remoteAddress: "192.168.1.100",
|
|
676
|
-
};
|
|
677
|
-
|
|
678
|
-
const unregisterA = registerBlueBubblesWebhookTarget({
|
|
679
|
-
account: accountA,
|
|
680
|
-
config,
|
|
681
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
682
|
-
core,
|
|
683
|
-
path: "/bluebubbles-webhook",
|
|
684
|
-
statusSink: sinkA,
|
|
685
|
-
});
|
|
686
|
-
const unregisterB = registerBlueBubblesWebhookTarget({
|
|
687
|
-
account: accountB,
|
|
688
|
-
config,
|
|
689
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
690
|
-
core,
|
|
691
|
-
path: "/bluebubbles-webhook",
|
|
692
|
-
statusSink: sinkB,
|
|
693
|
-
});
|
|
694
|
-
unregister = () => {
|
|
695
|
-
unregisterA();
|
|
696
|
-
unregisterB();
|
|
697
|
-
};
|
|
698
|
-
|
|
699
|
-
const res = createMockResponse();
|
|
700
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
701
|
-
|
|
702
|
-
expect(handled).toBe(true);
|
|
703
|
-
expect(res.statusCode).toBe(401);
|
|
704
|
-
expect(sinkA).not.toHaveBeenCalled();
|
|
705
|
-
expect(sinkB).not.toHaveBeenCalled();
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
it("ignores targets without passwords when a password-authenticated target matches", async () => {
|
|
709
|
-
const accountStrict = createMockAccount({ password: "secret-token" });
|
|
710
|
-
const accountWithoutPassword = createMockAccount({ password: undefined });
|
|
711
|
-
const config: OpenClawConfig = {};
|
|
712
|
-
const core = createMockRuntime();
|
|
713
|
-
setBlueBubblesRuntime(core);
|
|
714
|
-
|
|
715
|
-
const sinkStrict = vi.fn();
|
|
716
|
-
const sinkWithoutPassword = vi.fn();
|
|
717
|
-
|
|
718
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook?password=secret-token", {
|
|
719
|
-
type: "new-message",
|
|
720
|
-
data: {
|
|
721
|
-
text: "hello",
|
|
722
|
-
handle: { address: "+15551234567" },
|
|
723
|
-
isGroup: false,
|
|
724
|
-
isFromMe: false,
|
|
725
|
-
guid: "msg-1",
|
|
726
|
-
},
|
|
727
|
-
});
|
|
728
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
729
|
-
remoteAddress: "192.168.1.100",
|
|
730
|
-
};
|
|
731
|
-
|
|
732
|
-
const unregisterStrict = registerBlueBubblesWebhookTarget({
|
|
733
|
-
account: accountStrict,
|
|
734
|
-
config,
|
|
735
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
736
|
-
core,
|
|
737
|
-
path: "/bluebubbles-webhook",
|
|
738
|
-
statusSink: sinkStrict,
|
|
739
|
-
});
|
|
740
|
-
const unregisterNoPassword = registerBlueBubblesWebhookTarget({
|
|
741
|
-
account: accountWithoutPassword,
|
|
742
|
-
config,
|
|
743
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
744
|
-
core,
|
|
745
|
-
path: "/bluebubbles-webhook",
|
|
746
|
-
statusSink: sinkWithoutPassword,
|
|
747
|
-
});
|
|
748
|
-
unregister = () => {
|
|
749
|
-
unregisterStrict();
|
|
750
|
-
unregisterNoPassword();
|
|
751
|
-
};
|
|
752
|
-
|
|
753
|
-
const res = createMockResponse();
|
|
754
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
755
|
-
|
|
756
|
-
expect(handled).toBe(true);
|
|
757
|
-
expect(res.statusCode).toBe(200);
|
|
758
|
-
expect(sinkStrict).toHaveBeenCalledTimes(1);
|
|
759
|
-
expect(sinkWithoutPassword).not.toHaveBeenCalled();
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
it("requires authentication for loopback requests when password is configured", async () => {
|
|
763
|
-
const account = createMockAccount({ password: "secret-token" });
|
|
764
|
-
const config: OpenClawConfig = {};
|
|
765
|
-
const core = createMockRuntime();
|
|
766
|
-
setBlueBubblesRuntime(core);
|
|
767
|
-
for (const remoteAddress of ["127.0.0.1", "::1", "::ffff:127.0.0.1"]) {
|
|
768
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook", {
|
|
769
|
-
type: "new-message",
|
|
770
|
-
data: {
|
|
771
|
-
text: "hello",
|
|
772
|
-
handle: { address: "+15551234567" },
|
|
773
|
-
isGroup: false,
|
|
774
|
-
isFromMe: false,
|
|
775
|
-
guid: "msg-1",
|
|
776
|
-
},
|
|
777
|
-
});
|
|
778
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
779
|
-
remoteAddress,
|
|
780
|
-
};
|
|
781
|
-
|
|
782
|
-
const loopbackUnregister = registerBlueBubblesWebhookTarget({
|
|
783
|
-
account,
|
|
784
|
-
config,
|
|
785
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
786
|
-
core,
|
|
787
|
-
path: "/bluebubbles-webhook",
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
const res = createMockResponse();
|
|
791
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
792
|
-
expect(handled).toBe(true);
|
|
793
|
-
expect(res.statusCode).toBe(401);
|
|
794
|
-
|
|
795
|
-
loopbackUnregister();
|
|
796
|
-
}
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
it("rejects targets without passwords for loopback and proxied-looking requests", async () => {
|
|
800
|
-
const account = createMockAccount({ password: undefined });
|
|
801
|
-
const config: OpenClawConfig = {};
|
|
802
|
-
const core = createMockRuntime();
|
|
803
|
-
setBlueBubblesRuntime(core);
|
|
804
|
-
|
|
805
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
806
|
-
account,
|
|
807
|
-
config,
|
|
808
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
809
|
-
core,
|
|
810
|
-
path: "/bluebubbles-webhook",
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
const headerVariants: Record<string, string>[] = [
|
|
814
|
-
{ host: "localhost" },
|
|
815
|
-
{ host: "localhost", "x-forwarded-for": "203.0.113.10" },
|
|
816
|
-
{ host: "localhost", forwarded: "for=203.0.113.10;proto=https;host=example.com" },
|
|
817
|
-
];
|
|
818
|
-
for (const headers of headerVariants) {
|
|
819
|
-
const req = createMockRequest(
|
|
820
|
-
"POST",
|
|
821
|
-
"/bluebubbles-webhook",
|
|
822
|
-
{
|
|
823
|
-
type: "new-message",
|
|
824
|
-
data: {
|
|
825
|
-
text: "hello",
|
|
826
|
-
handle: { address: "+15551234567" },
|
|
827
|
-
isGroup: false,
|
|
828
|
-
isFromMe: false,
|
|
829
|
-
guid: "msg-1",
|
|
830
|
-
},
|
|
831
|
-
},
|
|
832
|
-
headers,
|
|
833
|
-
);
|
|
834
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
835
|
-
remoteAddress: "127.0.0.1",
|
|
836
|
-
};
|
|
837
|
-
const res = createMockResponse();
|
|
838
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
839
|
-
expect(handled).toBe(true);
|
|
840
|
-
expect(res.statusCode).toBe(401);
|
|
841
|
-
}
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
it("ignores unregistered webhook paths", async () => {
|
|
845
|
-
const req = createMockRequest("POST", "/unregistered-path", {});
|
|
846
|
-
const res = createMockResponse();
|
|
847
|
-
|
|
848
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
849
|
-
|
|
850
|
-
expect(handled).toBe(false);
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
it("parses chatId when provided as a string (webhook variant)", async () => {
|
|
854
|
-
const { resolveChatGuidForTarget } = await import("./send.js");
|
|
855
|
-
vi.mocked(resolveChatGuidForTarget).mockClear();
|
|
856
|
-
|
|
857
|
-
const account = createMockAccount({ groupPolicy: "open" });
|
|
858
|
-
const config: OpenClawConfig = {};
|
|
859
|
-
const core = createMockRuntime();
|
|
860
|
-
setBlueBubblesRuntime(core);
|
|
861
|
-
|
|
862
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
863
|
-
account,
|
|
864
|
-
config,
|
|
865
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
866
|
-
core,
|
|
867
|
-
path: "/bluebubbles-webhook",
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
const payload = {
|
|
871
|
-
type: "new-message",
|
|
872
|
-
data: {
|
|
873
|
-
text: "hello from group",
|
|
874
|
-
handle: { address: "+15551234567" },
|
|
875
|
-
isGroup: true,
|
|
876
|
-
isFromMe: false,
|
|
877
|
-
guid: "msg-1",
|
|
878
|
-
chatId: "123",
|
|
879
|
-
date: Date.now(),
|
|
880
|
-
},
|
|
881
|
-
};
|
|
882
|
-
|
|
883
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
884
|
-
const res = createMockResponse();
|
|
885
|
-
|
|
886
|
-
await handleBlueBubblesWebhookRequest(req, res);
|
|
887
|
-
await flushAsync();
|
|
888
|
-
|
|
889
|
-
expect(resolveChatGuidForTarget).toHaveBeenCalledWith(
|
|
890
|
-
expect.objectContaining({
|
|
891
|
-
target: { kind: "chat_id", chatId: 123 },
|
|
892
|
-
}),
|
|
893
|
-
);
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
it("extracts chatGuid from nested chat object fields (webhook variant)", async () => {
|
|
897
|
-
const { sendMessageBlueBubbles, resolveChatGuidForTarget } = await import("./send.js");
|
|
898
|
-
vi.mocked(sendMessageBlueBubbles).mockClear();
|
|
899
|
-
vi.mocked(resolveChatGuidForTarget).mockClear();
|
|
900
|
-
|
|
901
|
-
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
|
902
|
-
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
const account = createMockAccount({ groupPolicy: "open" });
|
|
906
|
-
const config: OpenClawConfig = {};
|
|
907
|
-
const core = createMockRuntime();
|
|
908
|
-
setBlueBubblesRuntime(core);
|
|
909
|
-
|
|
910
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
911
|
-
account,
|
|
912
|
-
config,
|
|
913
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
914
|
-
core,
|
|
915
|
-
path: "/bluebubbles-webhook",
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
const payload = {
|
|
919
|
-
type: "new-message",
|
|
920
|
-
data: {
|
|
921
|
-
text: "hello from group",
|
|
922
|
-
handle: { address: "+15551234567" },
|
|
923
|
-
isGroup: true,
|
|
924
|
-
isFromMe: false,
|
|
925
|
-
guid: "msg-1",
|
|
926
|
-
chat: { chatGuid: "iMessage;+;chat123456" },
|
|
927
|
-
date: Date.now(),
|
|
928
|
-
},
|
|
929
|
-
};
|
|
930
|
-
|
|
931
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
932
|
-
const res = createMockResponse();
|
|
933
|
-
|
|
934
|
-
await handleBlueBubblesWebhookRequest(req, res);
|
|
935
|
-
await flushAsync();
|
|
936
|
-
|
|
937
|
-
expect(resolveChatGuidForTarget).not.toHaveBeenCalled();
|
|
938
|
-
expect(sendMessageBlueBubbles).toHaveBeenCalledWith(
|
|
939
|
-
"chat_guid:iMessage;+;chat123456",
|
|
940
|
-
expect.any(String),
|
|
941
|
-
expect.any(Object),
|
|
942
|
-
);
|
|
943
|
-
});
|
|
944
|
-
});
|
|
945
|
-
|
|
946
264
|
describe("DM pairing behavior vs allowFrom", () => {
|
|
947
265
|
it("allows DM from sender in allowFrom list", async () => {
|
|
948
266
|
const account = createMockAccount({
|
|
@@ -2287,6 +1605,51 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2287
1605
|
|
|
2288
1606
|
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
2289
1607
|
});
|
|
1608
|
+
|
|
1609
|
+
it("does not auto-authorize DM control commands in open mode without allowlists", async () => {
|
|
1610
|
+
mockHasControlCommand.mockReturnValue(true);
|
|
1611
|
+
|
|
1612
|
+
const account = createMockAccount({
|
|
1613
|
+
dmPolicy: "open",
|
|
1614
|
+
allowFrom: [],
|
|
1615
|
+
});
|
|
1616
|
+
const config: OpenClawConfig = {};
|
|
1617
|
+
const core = createMockRuntime();
|
|
1618
|
+
setBlueBubblesRuntime(core);
|
|
1619
|
+
|
|
1620
|
+
unregister = registerBlueBubblesWebhookTarget({
|
|
1621
|
+
account,
|
|
1622
|
+
config,
|
|
1623
|
+
runtime: { log: vi.fn(), error: vi.fn() },
|
|
1624
|
+
core,
|
|
1625
|
+
path: "/bluebubbles-webhook",
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
const payload = {
|
|
1629
|
+
type: "new-message",
|
|
1630
|
+
data: {
|
|
1631
|
+
text: "/status",
|
|
1632
|
+
handle: { address: "+15559999999" },
|
|
1633
|
+
isGroup: false,
|
|
1634
|
+
isFromMe: false,
|
|
1635
|
+
guid: "msg-dm-open-unauthorized",
|
|
1636
|
+
date: Date.now(),
|
|
1637
|
+
},
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
1641
|
+
const res = createMockResponse();
|
|
1642
|
+
|
|
1643
|
+
await handleBlueBubblesWebhookRequest(req, res);
|
|
1644
|
+
await flushAsync();
|
|
1645
|
+
|
|
1646
|
+
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
|
1647
|
+
const latestDispatch =
|
|
1648
|
+
mockDispatchReplyWithBufferedBlockDispatcher.mock.calls[
|
|
1649
|
+
mockDispatchReplyWithBufferedBlockDispatcher.mock.calls.length - 1
|
|
1650
|
+
]?.[0];
|
|
1651
|
+
expect(latestDispatch?.ctx?.CommandAuthorized).toBe(false);
|
|
1652
|
+
});
|
|
2290
1653
|
});
|
|
2291
1654
|
|
|
2292
1655
|
describe("typing/read receipt toggles", () => {
|
|
@@ -2404,6 +1767,7 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2404
1767
|
|
|
2405
1768
|
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
|
2406
1769
|
await params.dispatcherOptions.onReplyStart?.();
|
|
1770
|
+
return EMPTY_DISPATCH_RESULT;
|
|
2407
1771
|
});
|
|
2408
1772
|
|
|
2409
1773
|
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
@@ -2454,6 +1818,7 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2454
1818
|
await params.dispatcherOptions.onReplyStart?.();
|
|
2455
1819
|
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
|
|
2456
1820
|
await params.dispatcherOptions.onIdle?.();
|
|
1821
|
+
return EMPTY_DISPATCH_RESULT;
|
|
2457
1822
|
});
|
|
2458
1823
|
|
|
2459
1824
|
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
@@ -2499,7 +1864,9 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2499
1864
|
},
|
|
2500
1865
|
};
|
|
2501
1866
|
|
|
2502
|
-
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(
|
|
1867
|
+
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(
|
|
1868
|
+
async () => EMPTY_DISPATCH_RESULT,
|
|
1869
|
+
);
|
|
2503
1870
|
|
|
2504
1871
|
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
2505
1872
|
const res = createMockResponse();
|
|
@@ -2521,6 +1888,7 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2521
1888
|
|
|
2522
1889
|
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
|
2523
1890
|
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
|
|
1891
|
+
return EMPTY_DISPATCH_RESULT;
|
|
2524
1892
|
});
|
|
2525
1893
|
|
|
2526
1894
|
const account = createMockAccount();
|
|
@@ -2572,6 +1940,7 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2572
1940
|
|
|
2573
1941
|
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
|
2574
1942
|
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
|
|
1943
|
+
return EMPTY_DISPATCH_RESULT;
|
|
2575
1944
|
});
|
|
2576
1945
|
|
|
2577
1946
|
const account = createMockAccount();
|
|
@@ -2644,6 +2013,7 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2644
2013
|
|
|
2645
2014
|
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
|
2646
2015
|
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
|
|
2016
|
+
return EMPTY_DISPATCH_RESULT;
|
|
2647
2017
|
});
|
|
2648
2018
|
|
|
2649
2019
|
const account = createMockAccount();
|