fix: harden message aborts + bluebubbles dm create (#1751) (thanks @tyler6204)
This commit is contained in:
@@ -24,6 +24,7 @@ Docs: https://docs.clawd.bot
|
||||
- Telegram: add verbose raw-update logging for inbound Telegram updates. (#1597) Thanks @rohannagpal.
|
||||
|
||||
### Fixes
|
||||
- BlueBubbles: route phone-number targets to DMs, avoid leaking routing IDs, and auto-create missing DMs (Private API required). (#1751) Thanks @tyler6204. https://docs.clawd.bot/channels/bluebubbles
|
||||
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
|
||||
- Web UI: hide internal `message_id` hints in chat bubbles.
|
||||
- Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.
|
||||
|
||||
@@ -213,6 +213,7 @@ Prefer `chat_guid` for stable routing:
|
||||
- `chat_id:123`
|
||||
- `chat_identifier:...`
|
||||
- Direct handles: `+15555550123`, `user@example.com`
|
||||
- If a direct handle does not have an existing DM chat, Clawdbot will create one via `POST /api/v1/chat/new`. This requires the BlueBubbles Private API to be enabled.
|
||||
|
||||
## Security
|
||||
- Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`. Requests from `localhost` are also accepted.
|
||||
|
||||
@@ -385,14 +385,14 @@ describe("send", () => {
|
||||
).rejects.toThrow("password is required");
|
||||
});
|
||||
|
||||
it("throws when chatGuid cannot be resolved", async () => {
|
||||
it("throws when chatGuid cannot be resolved for non-handle targets", async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ data: [] }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
sendMessageBlueBubbles("+15559999999", "Hello", {
|
||||
sendMessageBlueBubbles("chat_id:999", "Hello", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test",
|
||||
}),
|
||||
@@ -439,6 +439,57 @@ describe("send", () => {
|
||||
expect(body.method).toBeUndefined();
|
||||
});
|
||||
|
||||
it("creates a new chat when handle target is missing", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ data: [] }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
JSON.stringify({
|
||||
data: { guid: "new-msg-guid" },
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const result = await sendMessageBlueBubbles("+15550009999", "Hello new chat", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test",
|
||||
});
|
||||
|
||||
expect(result.messageId).toBe("new-msg-guid");
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
|
||||
const createCall = mockFetch.mock.calls[1];
|
||||
expect(createCall[0]).toContain("/api/v1/chat/new");
|
||||
const body = JSON.parse(createCall[1].body);
|
||||
expect(body.addresses).toEqual(["+15550009999"]);
|
||||
expect(body.message).toBe("Hello new chat");
|
||||
});
|
||||
|
||||
it("throws when creating a new chat requires Private API", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ data: [] }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 403,
|
||||
text: () => Promise.resolve("Private API not enabled"),
|
||||
});
|
||||
|
||||
await expect(
|
||||
sendMessageBlueBubbles("+15550008888", "Hello", {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test",
|
||||
}),
|
||||
).rejects.toThrow("Private API must be enabled");
|
||||
});
|
||||
|
||||
it("uses private-api when reply metadata is present", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
|
||||
@@ -321,6 +321,44 @@ describe("runMessageAction context isolation", () => {
|
||||
}),
|
||||
).rejects.toThrow(/Cross-context messaging denied/);
|
||||
});
|
||||
|
||||
it("aborts send when abortSignal is already aborted", async () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
await expect(
|
||||
runMessageAction({
|
||||
cfg: slackConfig,
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "slack",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
},
|
||||
dryRun: true,
|
||||
abortSignal: controller.signal,
|
||||
}),
|
||||
).rejects.toMatchObject({ name: "AbortError" });
|
||||
});
|
||||
|
||||
it("aborts broadcast when abortSignal is already aborted", async () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
await expect(
|
||||
runMessageAction({
|
||||
cfg: slackConfig,
|
||||
action: "broadcast",
|
||||
params: {
|
||||
targets: ["channel:C12345678"],
|
||||
channel: "slack",
|
||||
message: "hi",
|
||||
},
|
||||
dryRun: true,
|
||||
abortSignal: controller.signal,
|
||||
}),
|
||||
).rejects.toMatchObject({ name: "AbortError" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("runMessageAction sendAttachment hydration", () => {
|
||||
|
||||
@@ -526,6 +526,7 @@ async function handleBroadcastAction(
|
||||
input: RunMessageActionParams,
|
||||
params: Record<string, unknown>,
|
||||
): Promise<MessageActionRunResult> {
|
||||
throwIfAborted(input.abortSignal);
|
||||
const broadcastEnabled = input.cfg.tools?.message?.broadcast?.enabled !== false;
|
||||
if (!broadcastEnabled) {
|
||||
throw new Error("Broadcast is disabled. Set tools.message.broadcast.enabled to true.");
|
||||
@@ -550,8 +551,11 @@ async function handleBroadcastAction(
|
||||
error?: string;
|
||||
result?: MessageSendResult;
|
||||
}> = [];
|
||||
const isAbortError = (err: unknown): boolean => err instanceof Error && err.name === "AbortError";
|
||||
for (const targetChannel of targetChannels) {
|
||||
throwIfAborted(input.abortSignal);
|
||||
for (const target of rawTargets) {
|
||||
throwIfAborted(input.abortSignal);
|
||||
try {
|
||||
const resolved = await resolveChannelTarget({
|
||||
cfg: input.cfg,
|
||||
@@ -575,6 +579,7 @@ async function handleBroadcastAction(
|
||||
result: sendResult.kind === "send" ? sendResult.sendResult : undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) throw err;
|
||||
results.push({
|
||||
channel: targetChannel,
|
||||
to: target,
|
||||
|
||||
Reference in New Issue
Block a user