BlueBubbles: accept webhook payloads with missing handles
This commit is contained in:
parent
4f700e96af
commit
96c985400d
@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory/QMD: add optional `memory.qmd.mcporter` search routing so QMD `query/search/vsearch` can run through mcporter keep-alive flows (including multi-collection paths) to reduce cold starts, while keeping searches on agent-scoped QMD state for consistent recall. (#19617) Thanks @nicole-luxe and @vignesh07.
|
||||
- Chat/UI: strip inline reply/audio directive tags (`[[reply_to_current]]`, `[[reply_to:<id>]]`, `[[audio_as_voice]]`) from displayed chat history, live chat event output, and session preview snippets so control tags no longer leak into user-visible surfaces.
|
||||
- BlueBubbles/DM history: restore DM backfill context with account-scoped rolling history, bounded backfill retries, and safer history payload limits. (#20302) Thanks @Ryan-Haines.
|
||||
- BlueBubbles/Webhooks: accept inbound/reaction webhook payloads when BlueBubbles omits `handle` but provides DM `chatGuid`, and harden payload extraction for array/string-wrapped message bodies so valid webhook events no longer get rejected as unparseable. (#23275) Thanks @toph31.
|
||||
- Security/Config: block prototype-key traversal during config merge patch and legacy migration merge helpers (`__proto__`, `constructor`, `prototype`) to prevent prototype pollution during config mutation flows. (#22968) Thanks @Clawborn.
|
||||
- Security/Shell env: validate login-shell executable paths for shell-env fallback (`/etc/shells` + trusted prefixes) and block `SHELL` in dangerous env override policy paths so untrusted shell-path injection falls back safely to `/bin/sh`. Thanks @athuljayaram for reporting.
|
||||
- Security/Config: make parsed chat allowlist checks fail closed when `allowFrom` is empty, restoring expected DM/pairing gating.
|
||||
|
||||
78
extensions/bluebubbles/src/monitor-normalize.test.ts
Normal file
78
extensions/bluebubbles/src/monitor-normalize.test.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeWebhookMessage, normalizeWebhookReaction } from "./monitor-normalize.js";
|
||||
|
||||
describe("normalizeWebhookMessage", () => {
|
||||
it("falls back to DM chatGuid handle when sender handle is missing", () => {
|
||||
const result = normalizeWebhookMessage({
|
||||
type: "new-message",
|
||||
data: {
|
||||
guid: "msg-1",
|
||||
text: "hello",
|
||||
isGroup: false,
|
||||
isFromMe: false,
|
||||
handle: null,
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.senderId).toBe("+15551234567");
|
||||
expect(result?.chatGuid).toBe("iMessage;-;+15551234567");
|
||||
});
|
||||
|
||||
it("does not infer sender from group chatGuid when sender handle is missing", () => {
|
||||
const result = normalizeWebhookMessage({
|
||||
type: "new-message",
|
||||
data: {
|
||||
guid: "msg-1",
|
||||
text: "hello group",
|
||||
isGroup: true,
|
||||
isFromMe: false,
|
||||
handle: null,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("accepts array-wrapped payload data", () => {
|
||||
const result = normalizeWebhookMessage({
|
||||
type: "new-message",
|
||||
data: [
|
||||
{
|
||||
guid: "msg-1",
|
||||
text: "hello",
|
||||
handle: { address: "+15551234567" },
|
||||
isGroup: false,
|
||||
isFromMe: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.senderId).toBe("+15551234567");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeWebhookReaction", () => {
|
||||
it("falls back to DM chatGuid handle when reaction sender handle is missing", () => {
|
||||
const result = normalizeWebhookReaction({
|
||||
type: "updated-message",
|
||||
data: {
|
||||
guid: "msg-2",
|
||||
associatedMessageGuid: "p:0/msg-1",
|
||||
associatedMessageType: 2000,
|
||||
isGroup: false,
|
||||
isFromMe: false,
|
||||
handle: null,
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.senderId).toBe("+15551234567");
|
||||
expect(result?.messageId).toBe("p:0/msg-1");
|
||||
expect(result?.action).toBe("added");
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
import { normalizeBlueBubblesHandle } from "./targets.js";
|
||||
import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js";
|
||||
import type { BlueBubblesAttachment } from "./types.js";
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
@ -629,18 +629,42 @@ export function parseTapbackText(params: {
|
||||
}
|
||||
|
||||
function extractMessagePayload(payload: Record<string, unknown>): Record<string, unknown> | null {
|
||||
const parseRecord = (value: unknown): Record<string, unknown> | null => {
|
||||
const record = asRecord(value);
|
||||
if (record) {
|
||||
return record;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const entry of value) {
|
||||
const parsedEntry = parseRecord(entry);
|
||||
if (parsedEntry) {
|
||||
return parsedEntry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return parseRecord(JSON.parse(trimmed));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const dataRaw = payload.data ?? payload.payload ?? payload.event;
|
||||
const data =
|
||||
asRecord(dataRaw) ??
|
||||
(typeof dataRaw === "string" ? (asRecord(JSON.parse(dataRaw)) ?? null) : null);
|
||||
const data = parseRecord(dataRaw);
|
||||
const messageRaw = payload.message ?? data?.message ?? data;
|
||||
const message =
|
||||
asRecord(messageRaw) ??
|
||||
(typeof messageRaw === "string" ? (asRecord(JSON.parse(messageRaw)) ?? null) : null);
|
||||
if (!message) {
|
||||
return null;
|
||||
const message = parseRecord(messageRaw);
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
return message;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeWebhookMessage(
|
||||
@ -700,7 +724,10 @@ export function normalizeWebhookMessage(
|
||||
: timestampRaw * 1000
|
||||
: undefined;
|
||||
|
||||
const normalizedSender = normalizeBlueBubblesHandle(senderId);
|
||||
// BlueBubbles may omit `handle` in webhook payloads; for DM chat GUIDs we can still infer sender.
|
||||
const senderFallbackFromChatGuid =
|
||||
!senderId && !isGroup && chatGuid ? extractHandleFromChatGuid(chatGuid) : null;
|
||||
const normalizedSender = normalizeBlueBubblesHandle(senderId || senderFallbackFromChatGuid || "");
|
||||
if (!normalizedSender) {
|
||||
return null;
|
||||
}
|
||||
@ -774,7 +801,9 @@ export function normalizeWebhookReaction(
|
||||
: timestampRaw * 1000
|
||||
: undefined;
|
||||
|
||||
const normalizedSender = normalizeBlueBubblesHandle(senderId);
|
||||
const senderFallbackFromChatGuid =
|
||||
!senderId && !isGroup && chatGuid ? extractHandleFromChatGuid(chatGuid) : null;
|
||||
const normalizedSender = normalizeBlueBubblesHandle(senderId || senderFallbackFromChatGuid || "");
|
||||
if (!normalizedSender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user