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("+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> {
|
||||
const runtime = resolveRuntime(opts);
|
||||
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 bodyText = messageText || placeholder;
|
||||
if (!bodyText) return;
|
||||
const replyContext = describeReplyContext(message);
|
||||
const createdAt = message.created_at ? Date.parse(message.created_at) : undefined;
|
||||
const historyKey = isGroup
|
||||
? String(chatId ?? chatGuid ?? chatIdentifier ?? "unknown")
|
||||
@@ -414,11 +438,16 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
storePath,
|
||||
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({
|
||||
channel: "iMessage",
|
||||
from: fromLabel,
|
||||
timestamp: createdAt,
|
||||
body: bodyText,
|
||||
body: `${bodyText}${replySuffix}`,
|
||||
chatType: isGroup ? "group" : "direct",
|
||||
sender: { name: senderNormalized, id: sender },
|
||||
previousTimestamp,
|
||||
@@ -462,6 +491,9 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
Provider: "imessage",
|
||||
Surface: "imessage",
|
||||
MessageSid: message.id ? String(message.id) : undefined,
|
||||
ReplyToId: replyContext?.id,
|
||||
ReplyToBody: replyContext?.body,
|
||||
ReplyToSender: replyContext?.sender,
|
||||
Timestamp: createdAt,
|
||||
MediaPath: mediaPath,
|
||||
MediaType: mediaType,
|
||||
|
||||
@@ -13,6 +13,9 @@ export type IMessagePayload = {
|
||||
sender?: string | null;
|
||||
is_from_me?: boolean | null;
|
||||
text?: string | null;
|
||||
reply_to_id?: number | string | null;
|
||||
reply_to_text?: string | null;
|
||||
reply_to_sender?: string | null;
|
||||
created_at?: string | null;
|
||||
attachments?: IMessageAttachment[] | null;
|
||||
chat_identifier?: string | null;
|
||||
|
||||
@@ -65,4 +65,11 @@ describe("sendMessageIMessage", () => {
|
||||
expect(params.file).toBe("/tmp/imessage-media.jpg");
|
||||
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;
|
||||
};
|
||||
|
||||
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(
|
||||
mediaUrl: string,
|
||||
maxBytes: number,
|
||||
@@ -97,11 +109,12 @@ export async function sendMessageIMessage(
|
||||
const client = opts.client ?? (await createIMessageRpcClient({ cliPath, dbPath }));
|
||||
const shouldClose = !opts.client;
|
||||
try {
|
||||
const result = await client.request<{ ok?: boolean }>("send", params, {
|
||||
const result = await client.request<Record<string, unknown>>("send", params, {
|
||||
timeoutMs: opts.timeoutMs,
|
||||
});
|
||||
const resolvedId = resolveMessageId(result);
|
||||
return {
|
||||
messageId: result?.ok ? "ok" : "unknown",
|
||||
messageId: resolvedId ?? (result?.ok ? "ok" : "unknown"),
|
||||
};
|
||||
} finally {
|
||||
if (shouldClose) {
|
||||
|
||||
Reference in New Issue
Block a user