mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 23:02:02 +03:00
refactor(imessage): extract RPC notification parsing
This commit is contained in:
@@ -23,6 +23,7 @@ beforeAll(async () => {
|
|||||||
|
|
||||||
const replyMock = getReplyMock();
|
const replyMock = getReplyMock();
|
||||||
const sendMock = getSendMock();
|
const sendMock = getSendMock();
|
||||||
|
const readAllowFromStoreMock = getReadAllowFromStoreMock();
|
||||||
const upsertPairingRequestMock = getUpsertPairingRequestMock();
|
const upsertPairingRequestMock = getUpsertPairingRequestMock();
|
||||||
|
|
||||||
type TestConfig = {
|
type TestConfig = {
|
||||||
@@ -36,24 +37,42 @@ function getConfig(): TestConfig {
|
|||||||
return getConfigMock() as unknown as TestConfig;
|
return getConfigMock() as unknown as TestConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function notifyMessage(message: unknown) {
|
||||||
|
getNotificationHandler()?.({
|
||||||
|
method: "message",
|
||||||
|
params: { message },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeMonitor() {
|
||||||
|
for (let i = 0; i < 20; i += 1) {
|
||||||
|
const close = getCloseResolve();
|
||||||
|
if (close) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await flush();
|
||||||
|
}
|
||||||
|
throw new Error("imessage test harness: closeResolve not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMonitor() {
|
||||||
|
return monitorIMessageProvider();
|
||||||
|
}
|
||||||
|
|
||||||
describe("monitorIMessageProvider", () => {
|
describe("monitorIMessageProvider", () => {
|
||||||
it("ignores malformed rpc message payloads", async () => {
|
it("ignores malformed rpc message payloads", async () => {
|
||||||
const run = monitorIMessageProvider();
|
const run = startMonitor();
|
||||||
await waitForSubscribe();
|
await waitForSubscribe();
|
||||||
|
|
||||||
getNotificationHandler()?.({
|
notifyMessage({
|
||||||
method: "message",
|
id: 1,
|
||||||
params: {
|
sender: { nested: "not-a-string" },
|
||||||
message: {
|
text: "hello",
|
||||||
id: 1,
|
|
||||||
sender: { nested: "not-a-string" },
|
|
||||||
text: "hello",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await flush();
|
await flush();
|
||||||
getCloseResolve()?.();
|
await closeMonitor();
|
||||||
await run;
|
await run;
|
||||||
|
|
||||||
expect(replyMock).not.toHaveBeenCalled();
|
expect(replyMock).not.toHaveBeenCalled();
|
||||||
@@ -403,7 +422,7 @@ describe("monitorIMessageProvider", () => {
|
|||||||
channels: {
|
channels: {
|
||||||
...config.channels,
|
...config.channels,
|
||||||
imessage: {
|
imessage: {
|
||||||
...config.channels?.imessage,
|
...config.channels.imessage,
|
||||||
dmPolicy: "pairing",
|
dmPolicy: "pairing",
|
||||||
allowFrom: [],
|
allowFrom: [],
|
||||||
groupPolicy: "allowlist",
|
groupPolicy: "allowlist",
|
||||||
@@ -411,26 +430,21 @@ describe("monitorIMessageProvider", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
getReadAllowFromStoreMock().mockResolvedValue(["+15550003333"]);
|
readAllowFromStoreMock.mockResolvedValue(["+15550003333"]);
|
||||||
const run = monitorIMessageProvider();
|
const run = startMonitor();
|
||||||
await waitForSubscribe();
|
await waitForSubscribe();
|
||||||
|
|
||||||
getNotificationHandler()?.({
|
notifyMessage({
|
||||||
method: "message",
|
id: 30,
|
||||||
params: {
|
chat_id: 909,
|
||||||
message: {
|
sender: "+15550003333",
|
||||||
id: 30,
|
is_from_me: false,
|
||||||
chat_id: 909,
|
text: "@openclaw hi from paired sender",
|
||||||
sender: "+15550003333",
|
is_group: true,
|
||||||
is_from_me: false,
|
|
||||||
text: "@openclaw hi from paired sender",
|
|
||||||
is_group: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await flush();
|
await flush();
|
||||||
getCloseResolve()?.();
|
await closeMonitor();
|
||||||
await run;
|
await run;
|
||||||
|
|
||||||
expect(replyMock).not.toHaveBeenCalled();
|
expect(replyMock).not.toHaveBeenCalled();
|
||||||
@@ -444,7 +458,7 @@ describe("monitorIMessageProvider", () => {
|
|||||||
channels: {
|
channels: {
|
||||||
...config.channels,
|
...config.channels,
|
||||||
imessage: {
|
imessage: {
|
||||||
...config.channels?.imessage,
|
...config.channels.imessage,
|
||||||
dmPolicy: "pairing",
|
dmPolicy: "pairing",
|
||||||
allowFrom: [],
|
allowFrom: [],
|
||||||
groupPolicy: "allowlist",
|
groupPolicy: "allowlist",
|
||||||
@@ -452,26 +466,21 @@ describe("monitorIMessageProvider", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
getReadAllowFromStoreMock().mockResolvedValue(["+15550003333"]);
|
readAllowFromStoreMock.mockResolvedValue(["+15550003333"]);
|
||||||
const run = monitorIMessageProvider();
|
const run = startMonitor();
|
||||||
await waitForSubscribe();
|
await waitForSubscribe();
|
||||||
|
|
||||||
getNotificationHandler()?.({
|
notifyMessage({
|
||||||
method: "message",
|
id: 31,
|
||||||
params: {
|
chat_id: 202,
|
||||||
message: {
|
sender: "+15550003333",
|
||||||
id: 31,
|
is_from_me: false,
|
||||||
chat_id: 202,
|
text: "@openclaw hi from paired sender",
|
||||||
sender: "+15550003333",
|
is_group: true,
|
||||||
is_from_me: false,
|
|
||||||
text: "@openclaw hi from paired sender",
|
|
||||||
is_group: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await flush();
|
await flush();
|
||||||
getCloseResolve()?.();
|
await closeMonitor();
|
||||||
await run;
|
await run;
|
||||||
|
|
||||||
expect(replyMock).not.toHaveBeenCalled();
|
expect(replyMock).not.toHaveBeenCalled();
|
||||||
@@ -485,7 +494,7 @@ describe("monitorIMessageProvider", () => {
|
|||||||
channels: {
|
channels: {
|
||||||
...config.channels,
|
...config.channels,
|
||||||
imessage: {
|
imessage: {
|
||||||
...config.channels?.imessage,
|
...config.channels.imessage,
|
||||||
dmPolicy: "pairing",
|
dmPolicy: "pairing",
|
||||||
allowFrom: [],
|
allowFrom: [],
|
||||||
groupPolicy: "allowlist",
|
groupPolicy: "allowlist",
|
||||||
@@ -493,26 +502,21 @@ describe("monitorIMessageProvider", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
getReadAllowFromStoreMock().mockResolvedValue(["+15550003333"]);
|
readAllowFromStoreMock.mockResolvedValue(["+15550003333"]);
|
||||||
const run = monitorIMessageProvider();
|
const run = startMonitor();
|
||||||
await waitForSubscribe();
|
await waitForSubscribe();
|
||||||
|
|
||||||
getNotificationHandler()?.({
|
notifyMessage({
|
||||||
method: "message",
|
id: 32,
|
||||||
params: {
|
chat_id: 202,
|
||||||
message: {
|
sender: "+15550003333",
|
||||||
id: 32,
|
is_from_me: false,
|
||||||
chat_id: 202,
|
text: "/status",
|
||||||
sender: "+15550003333",
|
is_group: true,
|
||||||
is_from_me: false,
|
|
||||||
text: "/status",
|
|
||||||
is_group: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await flush();
|
await flush();
|
||||||
getCloseResolve()?.();
|
await closeMonitor();
|
||||||
await run;
|
await run;
|
||||||
|
|
||||||
expect(replyMock).not.toHaveBeenCalled();
|
expect(replyMock).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
normalizeIMessageHandle,
|
normalizeIMessageHandle,
|
||||||
} from "../targets.js";
|
} from "../targets.js";
|
||||||
import { deliverReplies } from "./deliver.js";
|
import { deliverReplies } from "./deliver.js";
|
||||||
|
import { parseIMessageNotification } from "./parse-notification.js";
|
||||||
import { normalizeAllowList, resolveRuntime } from "./runtime.js";
|
import { normalizeAllowList, resolveRuntime } from "./runtime.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,88 +112,6 @@ function describeReplyContext(message: IMessagePayload): IMessageReplyContext |
|
|||||||
return { body, id, sender };
|
return { body, id, sender };
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOptionalString(value: unknown): value is string | null | undefined {
|
|
||||||
return value === undefined || value === null || typeof value === "string";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOptionalStringOrNumber(value: unknown): value is string | number | null | undefined {
|
|
||||||
return (
|
|
||||||
value === undefined || value === null || typeof value === "string" || typeof value === "number"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOptionalNumber(value: unknown): value is number | null | undefined {
|
|
||||||
return value === undefined || value === null || typeof value === "number";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOptionalBoolean(value: unknown): value is boolean | null | undefined {
|
|
||||||
return value === undefined || value === null || typeof value === "boolean";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOptionalStringArray(value: unknown): value is string[] | null | undefined {
|
|
||||||
return (
|
|
||||||
value === undefined ||
|
|
||||||
value === null ||
|
|
||||||
(Array.isArray(value) && value.every((entry) => typeof entry === "string"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOptionalAttachments(value: unknown): value is IMessagePayload["attachments"] {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return value.every((attachment) => {
|
|
||||||
if (!isRecord(attachment)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
isOptionalString(attachment.original_path) &&
|
|
||||||
isOptionalString(attachment.mime_type) &&
|
|
||||||
isOptionalBoolean(attachment.missing)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseIMessageNotification(raw: unknown): IMessagePayload | null {
|
|
||||||
if (!isRecord(raw)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const maybeMessage = raw.message;
|
|
||||||
if (!isRecord(maybeMessage)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message: IMessagePayload = maybeMessage;
|
|
||||||
if (
|
|
||||||
!isOptionalNumber(message.id) ||
|
|
||||||
!isOptionalNumber(message.chat_id) ||
|
|
||||||
!isOptionalString(message.sender) ||
|
|
||||||
!isOptionalBoolean(message.is_from_me) ||
|
|
||||||
!isOptionalString(message.text) ||
|
|
||||||
!isOptionalStringOrNumber(message.reply_to_id) ||
|
|
||||||
!isOptionalString(message.reply_to_text) ||
|
|
||||||
!isOptionalString(message.reply_to_sender) ||
|
|
||||||
!isOptionalString(message.created_at) ||
|
|
||||||
!isOptionalAttachments(message.attachments) ||
|
|
||||||
!isOptionalString(message.chat_identifier) ||
|
|
||||||
!isOptionalString(message.chat_guid) ||
|
|
||||||
!isOptionalString(message.chat_name) ||
|
|
||||||
!isOptionalStringArray(message.participants) ||
|
|
||||||
!isOptionalBoolean(message.is_group)
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache for recently sent messages, used for echo detection.
|
* Cache for recently sent messages, used for echo detection.
|
||||||
* Keys are scoped by conversation (accountId:target) so the same text in different chats is not conflated.
|
* Keys are scoped by conversation (accountId:target) so the same text in different chats is not conflated.
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import type { IMessagePayload } from "./types.js";
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOptionalString(value: unknown): value is string | null | undefined {
|
||||||
|
return value === undefined || value === null || typeof value === "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOptionalStringOrNumber(value: unknown): value is string | number | null | undefined {
|
||||||
|
return (
|
||||||
|
value === undefined || value === null || typeof value === "string" || typeof value === "number"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOptionalNumber(value: unknown): value is number | null | undefined {
|
||||||
|
return value === undefined || value === null || typeof value === "number";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOptionalBoolean(value: unknown): value is boolean | null | undefined {
|
||||||
|
return value === undefined || value === null || typeof value === "boolean";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOptionalStringArray(value: unknown): value is string[] | null | undefined {
|
||||||
|
return (
|
||||||
|
value === undefined ||
|
||||||
|
value === null ||
|
||||||
|
(Array.isArray(value) && value.every((entry) => typeof entry === "string"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOptionalAttachments(value: unknown): value is IMessagePayload["attachments"] {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return value.every((attachment) => {
|
||||||
|
if (!isRecord(attachment)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
isOptionalString(attachment.original_path) &&
|
||||||
|
isOptionalString(attachment.mime_type) &&
|
||||||
|
isOptionalBoolean(attachment.missing)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseIMessageNotification(raw: unknown): IMessagePayload | null {
|
||||||
|
if (!isRecord(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const maybeMessage = raw.message;
|
||||||
|
if (!isRecord(maybeMessage)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message: IMessagePayload = maybeMessage;
|
||||||
|
if (
|
||||||
|
!isOptionalNumber(message.id) ||
|
||||||
|
!isOptionalNumber(message.chat_id) ||
|
||||||
|
!isOptionalString(message.sender) ||
|
||||||
|
!isOptionalBoolean(message.is_from_me) ||
|
||||||
|
!isOptionalString(message.text) ||
|
||||||
|
!isOptionalStringOrNumber(message.reply_to_id) ||
|
||||||
|
!isOptionalString(message.reply_to_text) ||
|
||||||
|
!isOptionalString(message.reply_to_sender) ||
|
||||||
|
!isOptionalString(message.created_at) ||
|
||||||
|
!isOptionalAttachments(message.attachments) ||
|
||||||
|
!isOptionalString(message.chat_identifier) ||
|
||||||
|
!isOptionalString(message.chat_guid) ||
|
||||||
|
!isOptionalString(message.chat_name) ||
|
||||||
|
!isOptionalStringArray(message.participants) ||
|
||||||
|
!isOptionalBoolean(message.is_group)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user