mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-17 12:11:20 +00:00
fix: preserve iMessage self-chat aliases (#61619) (thanks @neeravmakwana)
* fix(imessage): avoid DM self-chat false positives * fix(imessage): treat blank destination caller id as missing * fix(imessage): preserve alias self-chat * fix: preserve iMessage self-chat aliases (#61619) (thanks @neeravmakwana) --------- Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- iMessage/self-chat: distinguish normal DM outbound rows from true self-chat using `destination_caller_id` plus chat participants, while preserving multi-handle self-chat aliases so outbound DM replies stop looping back as inbound messages. (#61619) Thanks @neeravmakwana.
|
||||
- fix(browser): auto-generate browser control auth token for none/trusted-proxy modes [AI]. (#63280) Thanks @pgondhi987.
|
||||
- fix(exec): replace TOCTOU check-then-read with atomic pinned-fd open in script preflight [AI]. (#62333) Thanks @pgondhi987.
|
||||
- WhatsApp/auto-reply: keep inbound reply, media, and composing sends on the current socket across reconnects, wait through reconnect gaps, and retry timeout-only send failures without dropping the active socket ref. (#62892) Thanks @mcaxtr.
|
||||
|
||||
@@ -104,6 +104,22 @@ describe("imessage monitor gating + envelope builders", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("parseIMessageNotification preserves destination_caller_id metadata", () => {
|
||||
expect(
|
||||
parseIMessageNotification({
|
||||
message: {
|
||||
id: 1,
|
||||
sender: "+15550001111",
|
||||
destination_caller_id: "+15550002222",
|
||||
is_from_me: true,
|
||||
text: "hello",
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
destination_caller_id: "+15550002222",
|
||||
});
|
||||
});
|
||||
|
||||
it("drops group messages without mention by default", () => {
|
||||
const decision = resolve({
|
||||
message: {
|
||||
|
||||
@@ -170,6 +170,7 @@ export function resolveIMessageInboundDecision(params: {
|
||||
const chatId = params.message.chat_id ?? undefined;
|
||||
const chatGuid = params.message.chat_guid ?? undefined;
|
||||
const chatIdentifier = params.message.chat_identifier ?? undefined;
|
||||
const destinationCallerId = params.message.destination_caller_id ?? undefined;
|
||||
const createdAt = params.message.created_at ? Date.parse(params.message.created_at) : undefined;
|
||||
const messageText = params.messageText.trim();
|
||||
const bodyText = params.bodyText.trim();
|
||||
@@ -203,14 +204,23 @@ export function resolveIMessageInboundDecision(params: {
|
||||
text: bodyText,
|
||||
createdAt,
|
||||
};
|
||||
// Self-chat detection: in self-chat, sender == chat_identifier (both are the
|
||||
// user's own handle). When is_from_me=true in self-chat, the message could be
|
||||
// either: (a) a real user message typed by the user, or (b) an agent reply
|
||||
// echo reflected back by iMessage. We must distinguish them.
|
||||
const chatIdentifierNormalized = normalizeIMessageHandle(chatIdentifier ?? "") || undefined;
|
||||
const destinationCallerIdNormalized =
|
||||
normalizeIMessageHandle(destinationCallerId ?? "") || undefined;
|
||||
const chatParticipantHandles = new Set(
|
||||
(params.message.participants ?? [])
|
||||
.map((participant) => normalizeIMessageHandle(participant))
|
||||
.filter((participant): participant is string => participant.length > 0),
|
||||
);
|
||||
const matchesSelfChatDestination =
|
||||
destinationCallerIdNormalized == null ||
|
||||
destinationCallerIdNormalized === senderNormalized ||
|
||||
chatParticipantHandles.has(destinationCallerIdNormalized);
|
||||
const isSelfChat =
|
||||
!isGroup &&
|
||||
chatIdentifier != null &&
|
||||
normalizeIMessageHandle(sender) === normalizeIMessageHandle(chatIdentifier);
|
||||
chatIdentifierNormalized != null &&
|
||||
senderNormalized === chatIdentifierNormalized &&
|
||||
matchesSelfChatDestination;
|
||||
// Track whether we already processed the is_from_me=true self-chat path.
|
||||
// When true, the selfChatCache.has() check below must be skipped — we just
|
||||
// called remember() and would immediately match our own entry.
|
||||
|
||||
@@ -61,6 +61,7 @@ export function parseIMessageNotification(raw: unknown): IMessagePayload | null
|
||||
!isOptionalString(message.guid) ||
|
||||
!isOptionalNumber(message.chat_id) ||
|
||||
!isOptionalString(message.sender) ||
|
||||
!isOptionalString(message.destination_caller_id) ||
|
||||
!isOptionalBoolean(message.is_from_me) ||
|
||||
!isOptionalString(message.text) ||
|
||||
!isOptionalStringOrNumber(message.reply_to_id) ||
|
||||
|
||||
@@ -344,7 +344,6 @@ describe("self-chat is_from_me=true handling (Bruce Phase 2 fix)", () => {
|
||||
});
|
||||
|
||||
it("processes real user self-chat message (is_from_me=true, no echo cache match)", () => {
|
||||
// User sends "Hello" to themselves — is_from_me=true, sender==chat_identifier
|
||||
const echoCache = createSentMessageCache();
|
||||
const selfChatCache = createSelfChatCache();
|
||||
|
||||
@@ -354,6 +353,7 @@ describe("self-chat is_from_me=true handling (Bruce Phase 2 fix)", () => {
|
||||
id: 123703,
|
||||
sender: "+15551234567",
|
||||
chat_identifier: "+15551234567",
|
||||
destination_caller_id: "+15551234567",
|
||||
text: "Hello this is a test message",
|
||||
is_from_me: true,
|
||||
is_group: false,
|
||||
@@ -365,7 +365,57 @@ describe("self-chat is_from_me=true handling (Bruce Phase 2 fix)", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// Real user message — should be dispatched, not dropped
|
||||
expect(decision.kind).toBe("dispatch");
|
||||
});
|
||||
|
||||
it("treats blank destination_caller_id as missing for real self-chat", () => {
|
||||
const echoCache = createSentMessageCache();
|
||||
const selfChatCache = createSelfChatCache();
|
||||
|
||||
const decision = resolveIMessageInboundDecision(
|
||||
createParams({
|
||||
message: {
|
||||
id: 123704,
|
||||
sender: "+15551234567",
|
||||
chat_identifier: "+15551234567",
|
||||
destination_caller_id: "",
|
||||
text: "Hello this is a test message",
|
||||
is_from_me: true,
|
||||
is_group: false,
|
||||
},
|
||||
messageText: "Hello this is a test message",
|
||||
bodyText: "Hello this is a test message",
|
||||
echoCache,
|
||||
selfChatCache,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(decision.kind).toBe("dispatch");
|
||||
});
|
||||
|
||||
it("preserves self-chat when destination_caller_id is another local handle", () => {
|
||||
const echoCache = createSentMessageCache();
|
||||
const selfChatCache = createSelfChatCache();
|
||||
|
||||
const decision = resolveIMessageInboundDecision(
|
||||
createParams({
|
||||
message: {
|
||||
id: 123705,
|
||||
sender: "+15551234567",
|
||||
chat_identifier: "+15551234567",
|
||||
destination_caller_id: "me@icloud.com",
|
||||
participants: ["+15551234567", "me@icloud.com"],
|
||||
text: "Hello from my other local handle",
|
||||
is_from_me: true,
|
||||
is_group: false,
|
||||
},
|
||||
messageText: "Hello from my other local handle",
|
||||
bodyText: "Hello from my other local handle",
|
||||
echoCache,
|
||||
selfChatCache,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(decision.kind).toBe("dispatch");
|
||||
});
|
||||
|
||||
@@ -575,7 +625,36 @@ describe("self-chat is_from_me=true handling (Bruce Phase 2 fix)", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// sender != chat_identifier → not self-chat → dropped as "from me"
|
||||
expect(decision).toEqual({ kind: "drop", reason: "from me" });
|
||||
});
|
||||
|
||||
it("uses destination_caller_id to avoid DM self-chat false positives", () => {
|
||||
const echoCache = createSentMessageCache();
|
||||
const selfChatCache = createSelfChatCache();
|
||||
|
||||
echoCache.remember("default:imessage:+15551234567", {
|
||||
text: "Clean outbound text",
|
||||
messageId: "p:0/GUID-outbound",
|
||||
});
|
||||
|
||||
const decision = resolveIMessageInboundDecision(
|
||||
createParams({
|
||||
message: {
|
||||
id: 10001,
|
||||
sender: "+15551234567",
|
||||
chat_identifier: "+15551234567",
|
||||
destination_caller_id: "+15550001111",
|
||||
text: "<22>\u0001corrupted stored text",
|
||||
is_from_me: true,
|
||||
is_group: false,
|
||||
},
|
||||
messageText: "<22>\u0001corrupted stored text",
|
||||
bodyText: "<22>\u0001corrupted stored text",
|
||||
echoCache,
|
||||
selfChatCache,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(decision).toEqual({ kind: "drop", reason: "from me" });
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export type IMessagePayload = {
|
||||
guid?: string | null;
|
||||
chat_id?: number | null;
|
||||
sender?: string | null;
|
||||
destination_caller_id?: string | null;
|
||||
is_from_me?: boolean | null;
|
||||
text?: string | null;
|
||||
reply_to_id?: number | string | null;
|
||||
|
||||
Reference in New Issue
Block a user