mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-29 16:54:30 +00:00
BlueBubbles: gate self-chat cache to confirmed outbound sends
This commit is contained in:
@@ -504,9 +504,6 @@ export async function processMessage(
|
||||
};
|
||||
|
||||
if (message.fromMe) {
|
||||
if (isSelfChatMessage) {
|
||||
rememberBlueBubblesSelfChatCopy(selfChatLookup);
|
||||
}
|
||||
// Cache from-me messages so reply context can resolve sender/body.
|
||||
cacheInboundMessage();
|
||||
if (cacheMessageId) {
|
||||
@@ -518,6 +515,9 @@ export async function processMessage(
|
||||
body: rawBody,
|
||||
});
|
||||
if (pending) {
|
||||
if (isSelfChatMessage) {
|
||||
rememberBlueBubblesSelfChatCopy(selfChatLookup);
|
||||
}
|
||||
const displayId = getShortIdForUuid(cacheMessageId) || cacheMessageId;
|
||||
const previewSource = pending.snippetRaw || rawBody;
|
||||
const preview = previewSource
|
||||
|
||||
@@ -85,4 +85,35 @@ describe("BlueBubbles self-chat cache", () => {
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not collide long texts that differ only in the middle", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-03-07T00:00:00Z"));
|
||||
|
||||
const prefix = "a".repeat(256);
|
||||
const suffix = "b".repeat(256);
|
||||
const longBodyA = `${prefix}${"x".repeat(300)}${suffix}`;
|
||||
const longBodyB = `${prefix}${"y".repeat(300)}${suffix}`;
|
||||
|
||||
rememberBlueBubblesSelfChatCopy({
|
||||
...directLookup,
|
||||
body: longBodyA,
|
||||
timestamp: 123,
|
||||
});
|
||||
|
||||
expect(
|
||||
hasBlueBubblesSelfChatCopy({
|
||||
...directLookup,
|
||||
body: longBodyA,
|
||||
timestamp: 123,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasBlueBubblesSelfChatCopy({
|
||||
...directLookup,
|
||||
body: longBodyB,
|
||||
timestamp: 123,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,8 +16,6 @@ type SelfChatLookup = SelfChatCacheKeyParts & {
|
||||
const SELF_CHAT_TTL_MS = 10_000;
|
||||
const MAX_SELF_CHAT_CACHE_ENTRIES = 512;
|
||||
const CLEANUP_MIN_INTERVAL_MS = 1_000;
|
||||
const DIGEST_TEXT_HEAD_CHARS = 256;
|
||||
const DIGEST_TEXT_TAIL_CHARS = 256;
|
||||
const cache = new Map<string, number>();
|
||||
let lastCleanupAt = 0;
|
||||
|
||||
@@ -33,15 +31,8 @@ function isUsableTimestamp(timestamp: number | undefined): timestamp is number {
|
||||
return typeof timestamp === "number" && Number.isFinite(timestamp);
|
||||
}
|
||||
|
||||
function buildDigestSource(text: string): string {
|
||||
if (text.length <= DIGEST_TEXT_HEAD_CHARS + DIGEST_TEXT_TAIL_CHARS) {
|
||||
return text;
|
||||
}
|
||||
return `${text.slice(0, DIGEST_TEXT_HEAD_CHARS)}:${text.length}:${text.slice(-DIGEST_TEXT_TAIL_CHARS)}`;
|
||||
}
|
||||
|
||||
function digestText(text: string): string {
|
||||
return createHash("sha256").update(buildDigestSource(text)).digest("base64url");
|
||||
return createHash("sha256").update(text).digest("base64url");
|
||||
}
|
||||
|
||||
function trimOrUndefined(value?: string | null): string | undefined {
|
||||
|
||||
@@ -2680,12 +2680,20 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("drops reflected self-chat duplicates after seeing the fromMe copy", async () => {
|
||||
it("drops reflected self-chat duplicates after a confirmed assistant outbound", async () => {
|
||||
const account = createMockAccount({ dmPolicy: "open" });
|
||||
const config: OpenClawConfig = {};
|
||||
const core = createMockRuntime();
|
||||
setBlueBubblesRuntime(core);
|
||||
|
||||
const { sendMessageBlueBubbles } = await import("./send.js");
|
||||
vi.mocked(sendMessageBlueBubbles).mockResolvedValueOnce({ messageId: "ok" });
|
||||
|
||||
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
||||
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
|
||||
return EMPTY_DISPATCH_RESULT;
|
||||
});
|
||||
|
||||
unregister = registerBlueBubblesWebhookTarget({
|
||||
account,
|
||||
config,
|
||||
@@ -2695,10 +2703,32 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
const timestamp = Date.now();
|
||||
const inboundPayload = {
|
||||
type: "new-message",
|
||||
data: {
|
||||
text: "hello",
|
||||
handle: { address: "+15551234567" },
|
||||
isGroup: false,
|
||||
isFromMe: false,
|
||||
guid: "msg-self-0",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: timestamp,
|
||||
},
|
||||
};
|
||||
|
||||
await handleBlueBubblesWebhookRequest(
|
||||
createMockRequest("POST", "/bluebubbles-webhook", inboundPayload),
|
||||
createMockResponse(),
|
||||
);
|
||||
await flushAsync();
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
||||
mockDispatchReplyWithBufferedBlockDispatcher.mockClear();
|
||||
|
||||
const fromMePayload = {
|
||||
type: "new-message",
|
||||
data: {
|
||||
text: "loop me",
|
||||
text: "replying now",
|
||||
handle: { address: "+15551234567" },
|
||||
isGroup: false,
|
||||
isFromMe: true,
|
||||
@@ -2714,12 +2744,10 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
);
|
||||
await flushAsync();
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
|
||||
const reflectedPayload = {
|
||||
type: "new-message",
|
||||
data: {
|
||||
text: "loop me",
|
||||
text: "replying now",
|
||||
handle: { address: "+15551234567" },
|
||||
isGroup: false,
|
||||
isFromMe: false,
|
||||
@@ -2894,6 +2922,64 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not drop user-authored self-chat prompts without a confirmed assistant outbound", async () => {
|
||||
const account = createMockAccount({ dmPolicy: "open" });
|
||||
const config: OpenClawConfig = {};
|
||||
const core = createMockRuntime();
|
||||
setBlueBubblesRuntime(core);
|
||||
|
||||
unregister = registerBlueBubblesWebhookTarget({
|
||||
account,
|
||||
config,
|
||||
runtime: { log: vi.fn(), error: vi.fn() },
|
||||
core,
|
||||
path: "/bluebubbles-webhook",
|
||||
});
|
||||
|
||||
const timestamp = Date.now();
|
||||
const fromMePayload = {
|
||||
type: "new-message",
|
||||
data: {
|
||||
text: "user-authored self prompt",
|
||||
handle: { address: "+15551234567" },
|
||||
isGroup: false,
|
||||
isFromMe: true,
|
||||
guid: "msg-self-user-1",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: timestamp,
|
||||
},
|
||||
};
|
||||
|
||||
await handleBlueBubblesWebhookRequest(
|
||||
createMockRequest("POST", "/bluebubbles-webhook", fromMePayload),
|
||||
createMockResponse(),
|
||||
);
|
||||
await flushAsync();
|
||||
|
||||
mockDispatchReplyWithBufferedBlockDispatcher.mockClear();
|
||||
|
||||
const reflectedPayload = {
|
||||
type: "new-message",
|
||||
data: {
|
||||
text: "user-authored self prompt",
|
||||
handle: { address: "+15551234567" },
|
||||
isGroup: false,
|
||||
isFromMe: false,
|
||||
guid: "msg-self-user-2",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: timestamp,
|
||||
},
|
||||
};
|
||||
|
||||
await handleBlueBubblesWebhookRequest(
|
||||
createMockRequest("POST", "/bluebubbles-webhook", reflectedPayload),
|
||||
createMockResponse(),
|
||||
);
|
||||
await flushAsync();
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not treat chatGuid-inferred sender ids as self-chat evidence", async () => {
|
||||
const account = createMockAccount({ dmPolicy: "open" });
|
||||
const config: OpenClawConfig = {};
|
||||
|
||||
Reference in New Issue
Block a user