@openclaw/bluebubbles 2026.3.1 → 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 +4 -3
- package/src/actions.ts +4 -19
- package/src/channel.ts +3 -10
- package/src/config-schema.test.ts +12 -0
- package/src/config-schema.ts +4 -3
- package/src/monitor-debounce.ts +205 -0
- package/src/monitor-processing.ts +3 -2
- package/src/monitor.test.ts +42 -735
- 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/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,127 +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
|
-
withReplyDispatcher: vi.fn(
|
|
166
|
-
async ({
|
|
167
|
-
dispatcher,
|
|
168
|
-
run,
|
|
169
|
-
onSettled,
|
|
170
|
-
}: Parameters<PluginRuntime["channel"]["reply"]["withReplyDispatcher"]>[0]) => {
|
|
171
|
-
try {
|
|
172
|
-
return await run();
|
|
173
|
-
} finally {
|
|
174
|
-
dispatcher.markComplete();
|
|
175
|
-
try {
|
|
176
|
-
await dispatcher.waitForIdle();
|
|
177
|
-
} finally {
|
|
178
|
-
await onSettled?.();
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
) as unknown as PluginRuntime["channel"]["reply"]["withReplyDispatcher"],
|
|
183
|
-
finalizeInboundContext: vi.fn(
|
|
184
|
-
(ctx: Record<string, unknown>) => ctx,
|
|
185
|
-
) as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
|
|
186
|
-
formatAgentEnvelope:
|
|
187
|
-
mockFormatAgentEnvelope as unknown as PluginRuntime["channel"]["reply"]["formatAgentEnvelope"],
|
|
188
|
-
formatInboundEnvelope:
|
|
189
|
-
mockFormatInboundEnvelope as unknown as PluginRuntime["channel"]["reply"]["formatInboundEnvelope"],
|
|
121
|
+
formatAgentEnvelope: mockFormatAgentEnvelope,
|
|
122
|
+
formatInboundEnvelope: mockFormatInboundEnvelope,
|
|
190
123
|
resolveEnvelopeFormatOptions:
|
|
191
124
|
mockResolveEnvelopeFormatOptions as unknown as PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"],
|
|
192
125
|
},
|
|
@@ -195,99 +128,33 @@ function createMockRuntime(): PluginRuntime {
|
|
|
195
128
|
mockResolveAgentRoute as unknown as PluginRuntime["channel"]["routing"]["resolveAgentRoute"],
|
|
196
129
|
},
|
|
197
130
|
pairing: {
|
|
198
|
-
buildPairingReply:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
mockReadAllowFromStore as unknown as PluginRuntime["channel"]["pairing"]["readAllowFromStore"],
|
|
202
|
-
upsertPairingRequest:
|
|
203
|
-
mockUpsertPairingRequest as unknown as PluginRuntime["channel"]["pairing"]["upsertPairingRequest"],
|
|
131
|
+
buildPairingReply: mockBuildPairingReply,
|
|
132
|
+
readAllowFromStore: mockReadAllowFromStore,
|
|
133
|
+
upsertPairingRequest: mockUpsertPairingRequest,
|
|
204
134
|
},
|
|
205
135
|
media: {
|
|
206
|
-
fetchRemoteMedia:
|
|
207
|
-
vi.fn() as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"],
|
|
208
136
|
saveMediaBuffer:
|
|
209
137
|
mockSaveMediaBuffer as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
|
|
210
138
|
},
|
|
211
139
|
session: {
|
|
212
|
-
resolveStorePath:
|
|
213
|
-
|
|
214
|
-
readSessionUpdatedAt:
|
|
215
|
-
mockReadSessionUpdatedAt as unknown as PluginRuntime["channel"]["session"]["readSessionUpdatedAt"],
|
|
216
|
-
recordInboundSession:
|
|
217
|
-
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
|
|
218
|
-
recordSessionMetaFromInbound:
|
|
219
|
-
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordSessionMetaFromInbound"],
|
|
220
|
-
updateLastRoute:
|
|
221
|
-
vi.fn() as unknown as PluginRuntime["channel"]["session"]["updateLastRoute"],
|
|
140
|
+
resolveStorePath: mockResolveStorePath,
|
|
141
|
+
readSessionUpdatedAt: mockReadSessionUpdatedAt,
|
|
222
142
|
},
|
|
223
143
|
mentions: {
|
|
224
|
-
buildMentionRegexes:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
mockMatchesMentionPatterns as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionPatterns"],
|
|
228
|
-
matchesMentionWithExplicit:
|
|
229
|
-
mockMatchesMentionWithExplicit as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionWithExplicit"],
|
|
230
|
-
},
|
|
231
|
-
reactions: {
|
|
232
|
-
shouldAckReaction,
|
|
233
|
-
removeAckReactionAfterReply,
|
|
144
|
+
buildMentionRegexes: mockBuildMentionRegexes,
|
|
145
|
+
matchesMentionPatterns: mockMatchesMentionPatterns,
|
|
146
|
+
matchesMentionWithExplicit: mockMatchesMentionWithExplicit,
|
|
234
147
|
},
|
|
235
148
|
groups: {
|
|
236
149
|
resolveGroupPolicy:
|
|
237
150
|
mockResolveGroupPolicy as unknown as PluginRuntime["channel"]["groups"]["resolveGroupPolicy"],
|
|
238
|
-
resolveRequireMention:
|
|
239
|
-
mockResolveRequireMention as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"],
|
|
240
|
-
},
|
|
241
|
-
debounce: {
|
|
242
|
-
// Create a pass-through debouncer that immediately calls onFlush
|
|
243
|
-
createInboundDebouncer: vi.fn(
|
|
244
|
-
(params: { onFlush: (items: unknown[]) => Promise<void> }) => ({
|
|
245
|
-
enqueue: async (item: unknown) => {
|
|
246
|
-
await params.onFlush([item]);
|
|
247
|
-
},
|
|
248
|
-
flushKey: vi.fn(),
|
|
249
|
-
}),
|
|
250
|
-
) as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"],
|
|
251
|
-
resolveInboundDebounceMs: vi.fn(
|
|
252
|
-
() => 0,
|
|
253
|
-
) as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"],
|
|
151
|
+
resolveRequireMention: mockResolveRequireMention,
|
|
254
152
|
},
|
|
255
153
|
commands: {
|
|
256
|
-
resolveCommandAuthorizedFromAuthorizers:
|
|
257
|
-
mockResolveCommandAuthorizedFromAuthorizers as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
|
|
258
|
-
isControlCommandMessage:
|
|
259
|
-
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["isControlCommandMessage"],
|
|
260
|
-
shouldComputeCommandAuthorized:
|
|
261
|
-
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldComputeCommandAuthorized"],
|
|
262
|
-
shouldHandleTextCommands:
|
|
263
|
-
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldHandleTextCommands"],
|
|
154
|
+
resolveCommandAuthorizedFromAuthorizers: mockResolveCommandAuthorizedFromAuthorizers,
|
|
264
155
|
},
|
|
265
|
-
discord: {} as PluginRuntime["channel"]["discord"],
|
|
266
|
-
activity: {} as PluginRuntime["channel"]["activity"],
|
|
267
|
-
line: {} as PluginRuntime["channel"]["line"],
|
|
268
|
-
slack: {} as PluginRuntime["channel"]["slack"],
|
|
269
|
-
telegram: {} as PluginRuntime["channel"]["telegram"],
|
|
270
|
-
signal: {} as PluginRuntime["channel"]["signal"],
|
|
271
|
-
imessage: {} as PluginRuntime["channel"]["imessage"],
|
|
272
|
-
whatsapp: {} as PluginRuntime["channel"]["whatsapp"],
|
|
273
156
|
},
|
|
274
|
-
|
|
275
|
-
shouldLogVerbose: vi.fn(
|
|
276
|
-
() => false,
|
|
277
|
-
) as unknown as PluginRuntime["logging"]["shouldLogVerbose"],
|
|
278
|
-
getChildLogger: vi.fn(() => ({
|
|
279
|
-
info: vi.fn(),
|
|
280
|
-
warn: vi.fn(),
|
|
281
|
-
error: vi.fn(),
|
|
282
|
-
debug: vi.fn(),
|
|
283
|
-
})) as unknown as PluginRuntime["logging"]["getChildLogger"],
|
|
284
|
-
},
|
|
285
|
-
state: {
|
|
286
|
-
resolveStateDir: vi.fn(
|
|
287
|
-
() => "/tmp/openclaw",
|
|
288
|
-
) as unknown as PluginRuntime["state"]["resolveStateDir"],
|
|
289
|
-
},
|
|
290
|
-
};
|
|
157
|
+
});
|
|
291
158
|
}
|
|
292
159
|
|
|
293
160
|
function createMockAccount(
|
|
@@ -394,573 +261,6 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
394
261
|
unregister?.();
|
|
395
262
|
});
|
|
396
263
|
|
|
397
|
-
describe("webhook parsing + auth handling", () => {
|
|
398
|
-
it("rejects non-POST requests", async () => {
|
|
399
|
-
const account = createMockAccount();
|
|
400
|
-
const config: OpenClawConfig = {};
|
|
401
|
-
const core = createMockRuntime();
|
|
402
|
-
setBlueBubblesRuntime(core);
|
|
403
|
-
|
|
404
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
405
|
-
account,
|
|
406
|
-
config,
|
|
407
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
408
|
-
core,
|
|
409
|
-
path: "/bluebubbles-webhook",
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
const req = createMockRequest("GET", "/bluebubbles-webhook", {});
|
|
413
|
-
const res = createMockResponse();
|
|
414
|
-
|
|
415
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
416
|
-
|
|
417
|
-
expect(handled).toBe(true);
|
|
418
|
-
expect(res.statusCode).toBe(405);
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
it("accepts POST requests with valid JSON payload", async () => {
|
|
422
|
-
const account = createMockAccount();
|
|
423
|
-
const config: OpenClawConfig = {};
|
|
424
|
-
const core = createMockRuntime();
|
|
425
|
-
setBlueBubblesRuntime(core);
|
|
426
|
-
|
|
427
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
428
|
-
account,
|
|
429
|
-
config,
|
|
430
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
431
|
-
core,
|
|
432
|
-
path: "/bluebubbles-webhook",
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
const payload = {
|
|
436
|
-
type: "new-message",
|
|
437
|
-
data: {
|
|
438
|
-
text: "hello",
|
|
439
|
-
handle: { address: "+15551234567" },
|
|
440
|
-
isGroup: false,
|
|
441
|
-
isFromMe: false,
|
|
442
|
-
guid: "msg-1",
|
|
443
|
-
date: Date.now(),
|
|
444
|
-
},
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
448
|
-
const res = createMockResponse();
|
|
449
|
-
|
|
450
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
451
|
-
|
|
452
|
-
expect(handled).toBe(true);
|
|
453
|
-
expect(res.statusCode).toBe(200);
|
|
454
|
-
expect(res.body).toBe("ok");
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
it("rejects requests with invalid JSON", async () => {
|
|
458
|
-
const account = createMockAccount();
|
|
459
|
-
const config: OpenClawConfig = {};
|
|
460
|
-
const core = createMockRuntime();
|
|
461
|
-
setBlueBubblesRuntime(core);
|
|
462
|
-
|
|
463
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
464
|
-
account,
|
|
465
|
-
config,
|
|
466
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
467
|
-
core,
|
|
468
|
-
path: "/bluebubbles-webhook",
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook", "invalid json {{");
|
|
472
|
-
const res = createMockResponse();
|
|
473
|
-
|
|
474
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
475
|
-
|
|
476
|
-
expect(handled).toBe(true);
|
|
477
|
-
expect(res.statusCode).toBe(400);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it("accepts URL-encoded payload wrappers", async () => {
|
|
481
|
-
const account = createMockAccount();
|
|
482
|
-
const config: OpenClawConfig = {};
|
|
483
|
-
const core = createMockRuntime();
|
|
484
|
-
setBlueBubblesRuntime(core);
|
|
485
|
-
|
|
486
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
487
|
-
account,
|
|
488
|
-
config,
|
|
489
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
490
|
-
core,
|
|
491
|
-
path: "/bluebubbles-webhook",
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
const payload = {
|
|
495
|
-
type: "new-message",
|
|
496
|
-
data: {
|
|
497
|
-
text: "hello",
|
|
498
|
-
handle: { address: "+15551234567" },
|
|
499
|
-
isGroup: false,
|
|
500
|
-
isFromMe: false,
|
|
501
|
-
guid: "msg-1",
|
|
502
|
-
date: Date.now(),
|
|
503
|
-
},
|
|
504
|
-
};
|
|
505
|
-
const encodedBody = new URLSearchParams({
|
|
506
|
-
payload: JSON.stringify(payload),
|
|
507
|
-
}).toString();
|
|
508
|
-
|
|
509
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook", encodedBody);
|
|
510
|
-
const res = createMockResponse();
|
|
511
|
-
|
|
512
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
513
|
-
|
|
514
|
-
expect(handled).toBe(true);
|
|
515
|
-
expect(res.statusCode).toBe(200);
|
|
516
|
-
expect(res.body).toBe("ok");
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
it("returns 408 when request body times out (Slow-Loris protection)", async () => {
|
|
520
|
-
vi.useFakeTimers();
|
|
521
|
-
try {
|
|
522
|
-
const account = createMockAccount();
|
|
523
|
-
const config: OpenClawConfig = {};
|
|
524
|
-
const core = createMockRuntime();
|
|
525
|
-
setBlueBubblesRuntime(core);
|
|
526
|
-
|
|
527
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
528
|
-
account,
|
|
529
|
-
config,
|
|
530
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
531
|
-
core,
|
|
532
|
-
path: "/bluebubbles-webhook",
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
// Create a request that never sends data or ends (simulates slow-loris)
|
|
536
|
-
const req = new EventEmitter() as IncomingMessage;
|
|
537
|
-
req.method = "POST";
|
|
538
|
-
req.url = "/bluebubbles-webhook";
|
|
539
|
-
req.headers = {};
|
|
540
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
541
|
-
remoteAddress: "127.0.0.1",
|
|
542
|
-
};
|
|
543
|
-
req.destroy = vi.fn();
|
|
544
|
-
|
|
545
|
-
const res = createMockResponse();
|
|
546
|
-
|
|
547
|
-
const handledPromise = handleBlueBubblesWebhookRequest(req, res);
|
|
548
|
-
|
|
549
|
-
// Advance past the 30s timeout
|
|
550
|
-
await vi.advanceTimersByTimeAsync(31_000);
|
|
551
|
-
|
|
552
|
-
const handled = await handledPromise;
|
|
553
|
-
expect(handled).toBe(true);
|
|
554
|
-
expect(res.statusCode).toBe(408);
|
|
555
|
-
expect(req.destroy).toHaveBeenCalled();
|
|
556
|
-
} finally {
|
|
557
|
-
vi.useRealTimers();
|
|
558
|
-
}
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
it("authenticates via password query parameter", async () => {
|
|
562
|
-
const account = createMockAccount({ password: "secret-token" });
|
|
563
|
-
const config: OpenClawConfig = {};
|
|
564
|
-
const core = createMockRuntime();
|
|
565
|
-
setBlueBubblesRuntime(core);
|
|
566
|
-
|
|
567
|
-
// Mock non-localhost request
|
|
568
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook?password=secret-token", {
|
|
569
|
-
type: "new-message",
|
|
570
|
-
data: {
|
|
571
|
-
text: "hello",
|
|
572
|
-
handle: { address: "+15551234567" },
|
|
573
|
-
isGroup: false,
|
|
574
|
-
isFromMe: false,
|
|
575
|
-
guid: "msg-1",
|
|
576
|
-
},
|
|
577
|
-
});
|
|
578
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
579
|
-
remoteAddress: "192.168.1.100",
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
583
|
-
account,
|
|
584
|
-
config,
|
|
585
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
586
|
-
core,
|
|
587
|
-
path: "/bluebubbles-webhook",
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
const res = createMockResponse();
|
|
591
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
592
|
-
|
|
593
|
-
expect(handled).toBe(true);
|
|
594
|
-
expect(res.statusCode).toBe(200);
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
it("authenticates via x-password header", async () => {
|
|
598
|
-
const account = createMockAccount({ password: "secret-token" });
|
|
599
|
-
const config: OpenClawConfig = {};
|
|
600
|
-
const core = createMockRuntime();
|
|
601
|
-
setBlueBubblesRuntime(core);
|
|
602
|
-
|
|
603
|
-
const req = createMockRequest(
|
|
604
|
-
"POST",
|
|
605
|
-
"/bluebubbles-webhook",
|
|
606
|
-
{
|
|
607
|
-
type: "new-message",
|
|
608
|
-
data: {
|
|
609
|
-
text: "hello",
|
|
610
|
-
handle: { address: "+15551234567" },
|
|
611
|
-
isGroup: false,
|
|
612
|
-
isFromMe: false,
|
|
613
|
-
guid: "msg-1",
|
|
614
|
-
},
|
|
615
|
-
},
|
|
616
|
-
{ "x-password": "secret-token" },
|
|
617
|
-
);
|
|
618
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
619
|
-
remoteAddress: "192.168.1.100",
|
|
620
|
-
};
|
|
621
|
-
|
|
622
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
623
|
-
account,
|
|
624
|
-
config,
|
|
625
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
626
|
-
core,
|
|
627
|
-
path: "/bluebubbles-webhook",
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
const res = createMockResponse();
|
|
631
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
632
|
-
|
|
633
|
-
expect(handled).toBe(true);
|
|
634
|
-
expect(res.statusCode).toBe(200);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
it("rejects unauthorized requests with wrong password", async () => {
|
|
638
|
-
const account = createMockAccount({ password: "secret-token" });
|
|
639
|
-
const config: OpenClawConfig = {};
|
|
640
|
-
const core = createMockRuntime();
|
|
641
|
-
setBlueBubblesRuntime(core);
|
|
642
|
-
|
|
643
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook?password=wrong-token", {
|
|
644
|
-
type: "new-message",
|
|
645
|
-
data: {
|
|
646
|
-
text: "hello",
|
|
647
|
-
handle: { address: "+15551234567" },
|
|
648
|
-
isGroup: false,
|
|
649
|
-
isFromMe: false,
|
|
650
|
-
guid: "msg-1",
|
|
651
|
-
},
|
|
652
|
-
});
|
|
653
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
654
|
-
remoteAddress: "192.168.1.100",
|
|
655
|
-
};
|
|
656
|
-
|
|
657
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
658
|
-
account,
|
|
659
|
-
config,
|
|
660
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
661
|
-
core,
|
|
662
|
-
path: "/bluebubbles-webhook",
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
const res = createMockResponse();
|
|
666
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
667
|
-
|
|
668
|
-
expect(handled).toBe(true);
|
|
669
|
-
expect(res.statusCode).toBe(401);
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
it("rejects ambiguous routing when multiple targets match the same password", async () => {
|
|
673
|
-
const accountA = createMockAccount({ password: "secret-token" });
|
|
674
|
-
const accountB = createMockAccount({ password: "secret-token" });
|
|
675
|
-
const config: OpenClawConfig = {};
|
|
676
|
-
const core = createMockRuntime();
|
|
677
|
-
setBlueBubblesRuntime(core);
|
|
678
|
-
|
|
679
|
-
const sinkA = vi.fn();
|
|
680
|
-
const sinkB = vi.fn();
|
|
681
|
-
|
|
682
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook?password=secret-token", {
|
|
683
|
-
type: "new-message",
|
|
684
|
-
data: {
|
|
685
|
-
text: "hello",
|
|
686
|
-
handle: { address: "+15551234567" },
|
|
687
|
-
isGroup: false,
|
|
688
|
-
isFromMe: false,
|
|
689
|
-
guid: "msg-1",
|
|
690
|
-
},
|
|
691
|
-
});
|
|
692
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
693
|
-
remoteAddress: "192.168.1.100",
|
|
694
|
-
};
|
|
695
|
-
|
|
696
|
-
const unregisterA = registerBlueBubblesWebhookTarget({
|
|
697
|
-
account: accountA,
|
|
698
|
-
config,
|
|
699
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
700
|
-
core,
|
|
701
|
-
path: "/bluebubbles-webhook",
|
|
702
|
-
statusSink: sinkA,
|
|
703
|
-
});
|
|
704
|
-
const unregisterB = registerBlueBubblesWebhookTarget({
|
|
705
|
-
account: accountB,
|
|
706
|
-
config,
|
|
707
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
708
|
-
core,
|
|
709
|
-
path: "/bluebubbles-webhook",
|
|
710
|
-
statusSink: sinkB,
|
|
711
|
-
});
|
|
712
|
-
unregister = () => {
|
|
713
|
-
unregisterA();
|
|
714
|
-
unregisterB();
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
const res = createMockResponse();
|
|
718
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
719
|
-
|
|
720
|
-
expect(handled).toBe(true);
|
|
721
|
-
expect(res.statusCode).toBe(401);
|
|
722
|
-
expect(sinkA).not.toHaveBeenCalled();
|
|
723
|
-
expect(sinkB).not.toHaveBeenCalled();
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
it("ignores targets without passwords when a password-authenticated target matches", async () => {
|
|
727
|
-
const accountStrict = createMockAccount({ password: "secret-token" });
|
|
728
|
-
const accountWithoutPassword = createMockAccount({ password: undefined });
|
|
729
|
-
const config: OpenClawConfig = {};
|
|
730
|
-
const core = createMockRuntime();
|
|
731
|
-
setBlueBubblesRuntime(core);
|
|
732
|
-
|
|
733
|
-
const sinkStrict = vi.fn();
|
|
734
|
-
const sinkWithoutPassword = vi.fn();
|
|
735
|
-
|
|
736
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook?password=secret-token", {
|
|
737
|
-
type: "new-message",
|
|
738
|
-
data: {
|
|
739
|
-
text: "hello",
|
|
740
|
-
handle: { address: "+15551234567" },
|
|
741
|
-
isGroup: false,
|
|
742
|
-
isFromMe: false,
|
|
743
|
-
guid: "msg-1",
|
|
744
|
-
},
|
|
745
|
-
});
|
|
746
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
747
|
-
remoteAddress: "192.168.1.100",
|
|
748
|
-
};
|
|
749
|
-
|
|
750
|
-
const unregisterStrict = registerBlueBubblesWebhookTarget({
|
|
751
|
-
account: accountStrict,
|
|
752
|
-
config,
|
|
753
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
754
|
-
core,
|
|
755
|
-
path: "/bluebubbles-webhook",
|
|
756
|
-
statusSink: sinkStrict,
|
|
757
|
-
});
|
|
758
|
-
const unregisterNoPassword = registerBlueBubblesWebhookTarget({
|
|
759
|
-
account: accountWithoutPassword,
|
|
760
|
-
config,
|
|
761
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
762
|
-
core,
|
|
763
|
-
path: "/bluebubbles-webhook",
|
|
764
|
-
statusSink: sinkWithoutPassword,
|
|
765
|
-
});
|
|
766
|
-
unregister = () => {
|
|
767
|
-
unregisterStrict();
|
|
768
|
-
unregisterNoPassword();
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
const res = createMockResponse();
|
|
772
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
773
|
-
|
|
774
|
-
expect(handled).toBe(true);
|
|
775
|
-
expect(res.statusCode).toBe(200);
|
|
776
|
-
expect(sinkStrict).toHaveBeenCalledTimes(1);
|
|
777
|
-
expect(sinkWithoutPassword).not.toHaveBeenCalled();
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
it("requires authentication for loopback requests when password is configured", async () => {
|
|
781
|
-
const account = createMockAccount({ password: "secret-token" });
|
|
782
|
-
const config: OpenClawConfig = {};
|
|
783
|
-
const core = createMockRuntime();
|
|
784
|
-
setBlueBubblesRuntime(core);
|
|
785
|
-
for (const remoteAddress of ["127.0.0.1", "::1", "::ffff:127.0.0.1"]) {
|
|
786
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook", {
|
|
787
|
-
type: "new-message",
|
|
788
|
-
data: {
|
|
789
|
-
text: "hello",
|
|
790
|
-
handle: { address: "+15551234567" },
|
|
791
|
-
isGroup: false,
|
|
792
|
-
isFromMe: false,
|
|
793
|
-
guid: "msg-1",
|
|
794
|
-
},
|
|
795
|
-
});
|
|
796
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
797
|
-
remoteAddress,
|
|
798
|
-
};
|
|
799
|
-
|
|
800
|
-
const loopbackUnregister = registerBlueBubblesWebhookTarget({
|
|
801
|
-
account,
|
|
802
|
-
config,
|
|
803
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
804
|
-
core,
|
|
805
|
-
path: "/bluebubbles-webhook",
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
const res = createMockResponse();
|
|
809
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
810
|
-
expect(handled).toBe(true);
|
|
811
|
-
expect(res.statusCode).toBe(401);
|
|
812
|
-
|
|
813
|
-
loopbackUnregister();
|
|
814
|
-
}
|
|
815
|
-
});
|
|
816
|
-
|
|
817
|
-
it("rejects targets without passwords for loopback and proxied-looking requests", async () => {
|
|
818
|
-
const account = createMockAccount({ password: undefined });
|
|
819
|
-
const config: OpenClawConfig = {};
|
|
820
|
-
const core = createMockRuntime();
|
|
821
|
-
setBlueBubblesRuntime(core);
|
|
822
|
-
|
|
823
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
824
|
-
account,
|
|
825
|
-
config,
|
|
826
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
827
|
-
core,
|
|
828
|
-
path: "/bluebubbles-webhook",
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
const headerVariants: Record<string, string>[] = [
|
|
832
|
-
{ host: "localhost" },
|
|
833
|
-
{ host: "localhost", "x-forwarded-for": "203.0.113.10" },
|
|
834
|
-
{ host: "localhost", forwarded: "for=203.0.113.10;proto=https;host=example.com" },
|
|
835
|
-
];
|
|
836
|
-
for (const headers of headerVariants) {
|
|
837
|
-
const req = createMockRequest(
|
|
838
|
-
"POST",
|
|
839
|
-
"/bluebubbles-webhook",
|
|
840
|
-
{
|
|
841
|
-
type: "new-message",
|
|
842
|
-
data: {
|
|
843
|
-
text: "hello",
|
|
844
|
-
handle: { address: "+15551234567" },
|
|
845
|
-
isGroup: false,
|
|
846
|
-
isFromMe: false,
|
|
847
|
-
guid: "msg-1",
|
|
848
|
-
},
|
|
849
|
-
},
|
|
850
|
-
headers,
|
|
851
|
-
);
|
|
852
|
-
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
853
|
-
remoteAddress: "127.0.0.1",
|
|
854
|
-
};
|
|
855
|
-
const res = createMockResponse();
|
|
856
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
857
|
-
expect(handled).toBe(true);
|
|
858
|
-
expect(res.statusCode).toBe(401);
|
|
859
|
-
}
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
it("ignores unregistered webhook paths", async () => {
|
|
863
|
-
const req = createMockRequest("POST", "/unregistered-path", {});
|
|
864
|
-
const res = createMockResponse();
|
|
865
|
-
|
|
866
|
-
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
|
867
|
-
|
|
868
|
-
expect(handled).toBe(false);
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
it("parses chatId when provided as a string (webhook variant)", async () => {
|
|
872
|
-
const { resolveChatGuidForTarget } = await import("./send.js");
|
|
873
|
-
vi.mocked(resolveChatGuidForTarget).mockClear();
|
|
874
|
-
|
|
875
|
-
const account = createMockAccount({ groupPolicy: "open" });
|
|
876
|
-
const config: OpenClawConfig = {};
|
|
877
|
-
const core = createMockRuntime();
|
|
878
|
-
setBlueBubblesRuntime(core);
|
|
879
|
-
|
|
880
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
881
|
-
account,
|
|
882
|
-
config,
|
|
883
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
884
|
-
core,
|
|
885
|
-
path: "/bluebubbles-webhook",
|
|
886
|
-
});
|
|
887
|
-
|
|
888
|
-
const payload = {
|
|
889
|
-
type: "new-message",
|
|
890
|
-
data: {
|
|
891
|
-
text: "hello from group",
|
|
892
|
-
handle: { address: "+15551234567" },
|
|
893
|
-
isGroup: true,
|
|
894
|
-
isFromMe: false,
|
|
895
|
-
guid: "msg-1",
|
|
896
|
-
chatId: "123",
|
|
897
|
-
date: Date.now(),
|
|
898
|
-
},
|
|
899
|
-
};
|
|
900
|
-
|
|
901
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
902
|
-
const res = createMockResponse();
|
|
903
|
-
|
|
904
|
-
await handleBlueBubblesWebhookRequest(req, res);
|
|
905
|
-
await flushAsync();
|
|
906
|
-
|
|
907
|
-
expect(resolveChatGuidForTarget).toHaveBeenCalledWith(
|
|
908
|
-
expect.objectContaining({
|
|
909
|
-
target: { kind: "chat_id", chatId: 123 },
|
|
910
|
-
}),
|
|
911
|
-
);
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
it("extracts chatGuid from nested chat object fields (webhook variant)", async () => {
|
|
915
|
-
const { sendMessageBlueBubbles, resolveChatGuidForTarget } = await import("./send.js");
|
|
916
|
-
vi.mocked(sendMessageBlueBubbles).mockClear();
|
|
917
|
-
vi.mocked(resolveChatGuidForTarget).mockClear();
|
|
918
|
-
|
|
919
|
-
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
|
920
|
-
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
const account = createMockAccount({ groupPolicy: "open" });
|
|
924
|
-
const config: OpenClawConfig = {};
|
|
925
|
-
const core = createMockRuntime();
|
|
926
|
-
setBlueBubblesRuntime(core);
|
|
927
|
-
|
|
928
|
-
unregister = registerBlueBubblesWebhookTarget({
|
|
929
|
-
account,
|
|
930
|
-
config,
|
|
931
|
-
runtime: { log: vi.fn(), error: vi.fn() },
|
|
932
|
-
core,
|
|
933
|
-
path: "/bluebubbles-webhook",
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
const payload = {
|
|
937
|
-
type: "new-message",
|
|
938
|
-
data: {
|
|
939
|
-
text: "hello from group",
|
|
940
|
-
handle: { address: "+15551234567" },
|
|
941
|
-
isGroup: true,
|
|
942
|
-
isFromMe: false,
|
|
943
|
-
guid: "msg-1",
|
|
944
|
-
chat: { chatGuid: "iMessage;+;chat123456" },
|
|
945
|
-
date: Date.now(),
|
|
946
|
-
},
|
|
947
|
-
};
|
|
948
|
-
|
|
949
|
-
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
950
|
-
const res = createMockResponse();
|
|
951
|
-
|
|
952
|
-
await handleBlueBubblesWebhookRequest(req, res);
|
|
953
|
-
await flushAsync();
|
|
954
|
-
|
|
955
|
-
expect(resolveChatGuidForTarget).not.toHaveBeenCalled();
|
|
956
|
-
expect(sendMessageBlueBubbles).toHaveBeenCalledWith(
|
|
957
|
-
"chat_guid:iMessage;+;chat123456",
|
|
958
|
-
expect.any(String),
|
|
959
|
-
expect.any(Object),
|
|
960
|
-
);
|
|
961
|
-
});
|
|
962
|
-
});
|
|
963
|
-
|
|
964
264
|
describe("DM pairing behavior vs allowFrom", () => {
|
|
965
265
|
it("allows DM from sender in allowFrom list", async () => {
|
|
966
266
|
const account = createMockAccount({
|
|
@@ -2467,6 +1767,7 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2467
1767
|
|
|
2468
1768
|
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
|
2469
1769
|
await params.dispatcherOptions.onReplyStart?.();
|
|
1770
|
+
return EMPTY_DISPATCH_RESULT;
|
|
2470
1771
|
});
|
|
2471
1772
|
|
|
2472
1773
|
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
@@ -2517,6 +1818,7 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2517
1818
|
await params.dispatcherOptions.onReplyStart?.();
|
|
2518
1819
|
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
|
|
2519
1820
|
await params.dispatcherOptions.onIdle?.();
|
|
1821
|
+
return EMPTY_DISPATCH_RESULT;
|
|
2520
1822
|
});
|
|
2521
1823
|
|
|
2522
1824
|
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
@@ -2562,7 +1864,9 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2562
1864
|
},
|
|
2563
1865
|
};
|
|
2564
1866
|
|
|
2565
|
-
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(
|
|
1867
|
+
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(
|
|
1868
|
+
async () => EMPTY_DISPATCH_RESULT,
|
|
1869
|
+
);
|
|
2566
1870
|
|
|
2567
1871
|
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
2568
1872
|
const res = createMockResponse();
|
|
@@ -2584,6 +1888,7 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2584
1888
|
|
|
2585
1889
|
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
|
2586
1890
|
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
|
|
1891
|
+
return EMPTY_DISPATCH_RESULT;
|
|
2587
1892
|
});
|
|
2588
1893
|
|
|
2589
1894
|
const account = createMockAccount();
|
|
@@ -2635,6 +1940,7 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2635
1940
|
|
|
2636
1941
|
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
|
2637
1942
|
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
|
|
1943
|
+
return EMPTY_DISPATCH_RESULT;
|
|
2638
1944
|
});
|
|
2639
1945
|
|
|
2640
1946
|
const account = createMockAccount();
|
|
@@ -2707,6 +2013,7 @@ describe("BlueBubbles webhook monitor", () => {
|
|
|
2707
2013
|
|
|
2708
2014
|
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
|
2709
2015
|
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
|
|
2016
|
+
return EMPTY_DISPATCH_RESULT;
|
|
2710
2017
|
});
|
|
2711
2018
|
|
|
2712
2019
|
const account = createMockAccount();
|