feat: implement reply context handling in BlueBubbles messaging, enhancing message formatting and metadata resolution
This commit is contained in:
committed by
Peter Steinberger
parent
20bc89d96c
commit
e5514d4854
@@ -495,4 +495,43 @@ describe("monitorIMessageProvider", () => {
|
|||||||
expect(body).toContain("Test Group id:99");
|
expect(body).toContain("Test Group id:99");
|
||||||
expect(body).toContain("+15550001111: @clawd hi");
|
expect(body).toContain("+15550001111: @clawd hi");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes reply context when imessage reply metadata is present", async () => {
|
||||||
|
const run = monitorIMessageProvider();
|
||||||
|
await waitForSubscribe();
|
||||||
|
|
||||||
|
notificationHandler?.({
|
||||||
|
method: "message",
|
||||||
|
params: {
|
||||||
|
message: {
|
||||||
|
id: 12,
|
||||||
|
chat_id: 55,
|
||||||
|
sender: "+15550001111",
|
||||||
|
is_from_me: false,
|
||||||
|
text: "replying now",
|
||||||
|
is_group: false,
|
||||||
|
reply_to_id: 9001,
|
||||||
|
reply_to_text: "original message",
|
||||||
|
reply_to_sender: "+15559998888",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flush();
|
||||||
|
closeResolve?.();
|
||||||
|
await run;
|
||||||
|
|
||||||
|
expect(replyMock).toHaveBeenCalled();
|
||||||
|
const ctx = replyMock.mock.calls[0]?.[0] as {
|
||||||
|
Body?: string;
|
||||||
|
ReplyToId?: string;
|
||||||
|
ReplyToBody?: string;
|
||||||
|
ReplyToSender?: string;
|
||||||
|
};
|
||||||
|
expect(ctx.ReplyToId).toBe("9001");
|
||||||
|
expect(ctx.ReplyToBody).toBe("original message");
|
||||||
|
expect(ctx.ReplyToSender).toBe("+15559998888");
|
||||||
|
expect(String(ctx.Body ?? "")).toContain("[Replying to +15559998888 id:9001]");
|
||||||
|
expect(String(ctx.Body ?? "")).toContain("original message");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -92,6 +92,29 @@ async function detectRemoteHostFromCliPath(cliPath: string): Promise<string | un
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IMessageReplyContext = {
|
||||||
|
id?: string;
|
||||||
|
body: string;
|
||||||
|
sender?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeReplyField(value: unknown): string | undefined {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
if (typeof value === "number") return String(value);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeReplyContext(message: IMessagePayload): IMessageReplyContext | null {
|
||||||
|
const body = normalizeReplyField(message.reply_to_text);
|
||||||
|
if (!body) return null;
|
||||||
|
const id = normalizeReplyField(message.reply_to_id);
|
||||||
|
const sender = normalizeReplyField(message.reply_to_sender);
|
||||||
|
return { body, id, sender };
|
||||||
|
}
|
||||||
|
|
||||||
export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): Promise<void> {
|
export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): Promise<void> {
|
||||||
const runtime = resolveRuntime(opts);
|
const runtime = resolveRuntime(opts);
|
||||||
const cfg = opts.config ?? loadConfig();
|
const cfg = opts.config ?? loadConfig();
|
||||||
@@ -324,6 +347,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
|||||||
const placeholder = kind ? `<media:${kind}>` : attachments?.length ? "<media:attachment>" : "";
|
const placeholder = kind ? `<media:${kind}>` : attachments?.length ? "<media:attachment>" : "";
|
||||||
const bodyText = messageText || placeholder;
|
const bodyText = messageText || placeholder;
|
||||||
if (!bodyText) return;
|
if (!bodyText) return;
|
||||||
|
const replyContext = describeReplyContext(message);
|
||||||
const createdAt = message.created_at ? Date.parse(message.created_at) : undefined;
|
const createdAt = message.created_at ? Date.parse(message.created_at) : undefined;
|
||||||
const historyKey = isGroup
|
const historyKey = isGroup
|
||||||
? String(chatId ?? chatGuid ?? chatIdentifier ?? "unknown")
|
? String(chatId ?? chatGuid ?? chatIdentifier ?? "unknown")
|
||||||
@@ -414,11 +438,16 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
|||||||
storePath,
|
storePath,
|
||||||
sessionKey: route.sessionKey,
|
sessionKey: route.sessionKey,
|
||||||
});
|
});
|
||||||
|
const replySuffix = replyContext
|
||||||
|
? `\n\n[Replying to ${replyContext.sender ?? "unknown sender"}${
|
||||||
|
replyContext.id ? ` id:${replyContext.id}` : ""
|
||||||
|
}]\n${replyContext.body}\n[/Replying]`
|
||||||
|
: "";
|
||||||
const body = formatInboundEnvelope({
|
const body = formatInboundEnvelope({
|
||||||
channel: "iMessage",
|
channel: "iMessage",
|
||||||
from: fromLabel,
|
from: fromLabel,
|
||||||
timestamp: createdAt,
|
timestamp: createdAt,
|
||||||
body: bodyText,
|
body: `${bodyText}${replySuffix}`,
|
||||||
chatType: isGroup ? "group" : "direct",
|
chatType: isGroup ? "group" : "direct",
|
||||||
sender: { name: senderNormalized, id: sender },
|
sender: { name: senderNormalized, id: sender },
|
||||||
previousTimestamp,
|
previousTimestamp,
|
||||||
@@ -462,6 +491,9 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
|||||||
Provider: "imessage",
|
Provider: "imessage",
|
||||||
Surface: "imessage",
|
Surface: "imessage",
|
||||||
MessageSid: message.id ? String(message.id) : undefined,
|
MessageSid: message.id ? String(message.id) : undefined,
|
||||||
|
ReplyToId: replyContext?.id,
|
||||||
|
ReplyToBody: replyContext?.body,
|
||||||
|
ReplyToSender: replyContext?.sender,
|
||||||
Timestamp: createdAt,
|
Timestamp: createdAt,
|
||||||
MediaPath: mediaPath,
|
MediaPath: mediaPath,
|
||||||
MediaType: mediaType,
|
MediaType: mediaType,
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export type IMessagePayload = {
|
|||||||
sender?: string | null;
|
sender?: string | null;
|
||||||
is_from_me?: boolean | null;
|
is_from_me?: boolean | null;
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
|
reply_to_id?: number | string | null;
|
||||||
|
reply_to_text?: string | null;
|
||||||
|
reply_to_sender?: string | null;
|
||||||
created_at?: string | null;
|
created_at?: string | null;
|
||||||
attachments?: IMessageAttachment[] | null;
|
attachments?: IMessageAttachment[] | null;
|
||||||
chat_identifier?: string | null;
|
chat_identifier?: string | null;
|
||||||
|
|||||||
@@ -65,4 +65,11 @@ describe("sendMessageIMessage", () => {
|
|||||||
expect(params.file).toBe("/tmp/imessage-media.jpg");
|
expect(params.file).toBe("/tmp/imessage-media.jpg");
|
||||||
expect(params.text).toBe("<media:image>");
|
expect(params.text).toBe("<media:image>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns message id when rpc provides one", async () => {
|
||||||
|
requestMock.mockResolvedValue({ ok: true, id: 123 });
|
||||||
|
const { sendMessageIMessage } = await loadSendMessageIMessage();
|
||||||
|
const result = await sendMessageIMessage("chat_id:7", "hello");
|
||||||
|
expect(result.messageId).toBe("123");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,6 +23,18 @@ export type IMessageSendResult = {
|
|||||||
messageId: string;
|
messageId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveMessageId(result: Record<string, unknown> | null | undefined): string | null {
|
||||||
|
if (!result) return null;
|
||||||
|
const raw =
|
||||||
|
(typeof result.messageId === "string" && result.messageId.trim()) ||
|
||||||
|
(typeof result.message_id === "string" && result.message_id.trim()) ||
|
||||||
|
(typeof result.id === "string" && result.id.trim()) ||
|
||||||
|
(typeof result.guid === "string" && result.guid.trim()) ||
|
||||||
|
(typeof result.message_id === "number" ? String(result.message_id) : null) ||
|
||||||
|
(typeof result.id === "number" ? String(result.id) : null);
|
||||||
|
return raw ? String(raw).trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveAttachment(
|
async function resolveAttachment(
|
||||||
mediaUrl: string,
|
mediaUrl: string,
|
||||||
maxBytes: number,
|
maxBytes: number,
|
||||||
@@ -97,11 +109,12 @@ export async function sendMessageIMessage(
|
|||||||
const client = opts.client ?? (await createIMessageRpcClient({ cliPath, dbPath }));
|
const client = opts.client ?? (await createIMessageRpcClient({ cliPath, dbPath }));
|
||||||
const shouldClose = !opts.client;
|
const shouldClose = !opts.client;
|
||||||
try {
|
try {
|
||||||
const result = await client.request<{ ok?: boolean }>("send", params, {
|
const result = await client.request<Record<string, unknown>>("send", params, {
|
||||||
timeoutMs: opts.timeoutMs,
|
timeoutMs: opts.timeoutMs,
|
||||||
});
|
});
|
||||||
|
const resolvedId = resolveMessageId(result);
|
||||||
return {
|
return {
|
||||||
messageId: result?.ok ? "ok" : "unknown",
|
messageId: resolvedId ?? (result?.ok ? "ok" : "unknown"),
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
if (shouldClose) {
|
if (shouldClose) {
|
||||||
|
|||||||
Reference in New Issue
Block a user