fix: include sender info for iMessage/Signal group messages
This commit is contained in:
@@ -184,4 +184,77 @@ describe("dispatchReplyFromConfig", () => {
|
|||||||
|
|
||||||
expect(replyResolver).toHaveBeenCalledTimes(1);
|
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("appends sender meta for non-direct chats when missing", async () => {
|
||||||
|
mocks.tryFastAbortFromMessage.mockResolvedValue({
|
||||||
|
handled: false,
|
||||||
|
aborted: false,
|
||||||
|
});
|
||||||
|
const cfg = {} as ClawdbotConfig;
|
||||||
|
const dispatcher = createDispatcher();
|
||||||
|
const ctx: MsgContext = {
|
||||||
|
Provider: "imessage",
|
||||||
|
ChatType: "group",
|
||||||
|
Body: "[iMessage group:1] hello",
|
||||||
|
SenderName: "+15555550123",
|
||||||
|
SenderId: "+15555550123",
|
||||||
|
};
|
||||||
|
|
||||||
|
const replyResolver = vi.fn(async (resolvedCtx: MsgContext) => {
|
||||||
|
expect(resolvedCtx.Body).toContain("\n[from: +15555550123]");
|
||||||
|
return { text: "ok" } satisfies ReplyPayload;
|
||||||
|
});
|
||||||
|
|
||||||
|
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
|
||||||
|
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not append sender meta when Body already includes a from line", async () => {
|
||||||
|
mocks.tryFastAbortFromMessage.mockResolvedValue({
|
||||||
|
handled: false,
|
||||||
|
aborted: false,
|
||||||
|
});
|
||||||
|
const cfg = {} as ClawdbotConfig;
|
||||||
|
const dispatcher = createDispatcher();
|
||||||
|
const ctx: MsgContext = {
|
||||||
|
Provider: "whatsapp",
|
||||||
|
ChatType: "group",
|
||||||
|
Body: "[WhatsApp group:1] hello\\n[from: Bob (+222)]",
|
||||||
|
SenderName: "Bob",
|
||||||
|
SenderId: "+222",
|
||||||
|
};
|
||||||
|
|
||||||
|
const replyResolver = vi.fn(async (resolvedCtx: MsgContext) => {
|
||||||
|
expect(resolvedCtx.Body.match(/\\n\[from:/g)?.length ?? 0).toBe(1);
|
||||||
|
return { text: "ok" } satisfies ReplyPayload;
|
||||||
|
});
|
||||||
|
|
||||||
|
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
|
||||||
|
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not append sender meta for other providers (scope is signal/imessage only)", async () => {
|
||||||
|
mocks.tryFastAbortFromMessage.mockResolvedValue({
|
||||||
|
handled: false,
|
||||||
|
aborted: false,
|
||||||
|
});
|
||||||
|
const cfg = {} as ClawdbotConfig;
|
||||||
|
const dispatcher = createDispatcher();
|
||||||
|
const ctx: MsgContext = {
|
||||||
|
Provider: "slack",
|
||||||
|
OriginatingChannel: "slack",
|
||||||
|
ChatType: "group",
|
||||||
|
Body: "[Slack #room 2026-01-01T00:00Z] hi",
|
||||||
|
SenderName: "Bob",
|
||||||
|
SenderId: "U123",
|
||||||
|
};
|
||||||
|
|
||||||
|
const replyResolver = vi.fn(async (resolvedCtx: MsgContext) => {
|
||||||
|
expect(resolvedCtx.Body).not.toContain("[from:");
|
||||||
|
return { text: "ok" } satisfies ReplyPayload;
|
||||||
|
});
|
||||||
|
|
||||||
|
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
|
||||||
|
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
}): Promise<DispatchFromConfigResult> {
|
}): Promise<DispatchFromConfigResult> {
|
||||||
const { ctx, cfg, dispatcher } = params;
|
const { ctx, cfg, dispatcher } = params;
|
||||||
|
|
||||||
|
maybeAppendSenderMeta(ctx);
|
||||||
|
|
||||||
if (shouldSkipDuplicateInbound(ctx)) {
|
if (shouldSkipDuplicateInbound(ctx)) {
|
||||||
return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
|
return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
|
||||||
}
|
}
|
||||||
@@ -160,3 +162,40 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
counts.final += routedFinalCount;
|
counts.final += routedFinalCount;
|
||||||
return { queuedFinal, counts };
|
return { queuedFinal, counts };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maybeAppendSenderMeta(ctx: MsgContext): void {
|
||||||
|
if (!ctx.Body?.trim()) return;
|
||||||
|
if (ctx.ChatType !== "group") return;
|
||||||
|
if (!shouldInjectSenderMeta(ctx)) return;
|
||||||
|
if (hasSenderMetaLine(ctx.Body)) return;
|
||||||
|
|
||||||
|
const senderLabel = formatSenderLabel(ctx);
|
||||||
|
if (!senderLabel) return;
|
||||||
|
|
||||||
|
const lineBreak = resolveBodyLineBreak(ctx.Body);
|
||||||
|
ctx.Body = `${ctx.Body}${lineBreak}[from: ${senderLabel}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldInjectSenderMeta(ctx: MsgContext): boolean {
|
||||||
|
const origin = (ctx.OriginatingChannel ?? ctx.Provider ?? "").toLowerCase();
|
||||||
|
return origin === "imessage" || origin === "signal";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBodyLineBreak(body: string): string {
|
||||||
|
if (body.includes("\n")) return "\n";
|
||||||
|
if (body.includes("\\n")) return "\\n";
|
||||||
|
return "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSenderMetaLine(body: string): boolean {
|
||||||
|
return /(^|\n|\\n)\[from:/i.test(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSenderLabel(ctx: MsgContext): string | null {
|
||||||
|
const senderName = ctx.SenderName?.trim();
|
||||||
|
const senderId = ctx.SenderId?.trim();
|
||||||
|
if (senderName && senderId && senderName !== senderId) {
|
||||||
|
return `${senderName} (${senderId})`;
|
||||||
|
}
|
||||||
|
return senderName ?? senderId ?? null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
|||||||
const senderRaw = message.sender ?? "";
|
const senderRaw = message.sender ?? "";
|
||||||
const sender = senderRaw.trim();
|
const sender = senderRaw.trim();
|
||||||
if (!sender) return;
|
if (!sender) return;
|
||||||
|
const senderNormalized = normalizeIMessageHandle(sender);
|
||||||
if (message.is_from_me) return;
|
if (message.is_from_me) return;
|
||||||
|
|
||||||
const chatId = message.chat_id ?? undefined;
|
const chatId = message.chat_id ?? undefined;
|
||||||
@@ -346,7 +347,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
|||||||
historyKey,
|
historyKey,
|
||||||
limit: historyLimit,
|
limit: historyLimit,
|
||||||
entry: {
|
entry: {
|
||||||
sender: normalizeIMessageHandle(sender),
|
sender: senderNormalized,
|
||||||
body: bodyText,
|
body: bodyText,
|
||||||
timestamp: createdAt,
|
timestamp: createdAt,
|
||||||
messageId: message.id ? String(message.id) : undefined,
|
messageId: message.id ? String(message.id) : undefined,
|
||||||
@@ -359,7 +360,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
|||||||
const chatTarget = formatIMessageChatTarget(chatId);
|
const chatTarget = formatIMessageChatTarget(chatId);
|
||||||
const fromLabel = isGroup
|
const fromLabel = isGroup
|
||||||
? `${message.chat_name || "iMessage Group"} id:${chatId ?? "unknown"}`
|
? `${message.chat_name || "iMessage Group"} id:${chatId ?? "unknown"}`
|
||||||
: `${normalizeIMessageHandle(sender)} id:${sender}`;
|
: `${senderNormalized} id:${sender}`;
|
||||||
const body = formatAgentEnvelope({
|
const body = formatAgentEnvelope({
|
||||||
channel: "iMessage",
|
channel: "iMessage",
|
||||||
from: fromLabel,
|
from: fromLabel,
|
||||||
@@ -397,7 +398,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
|||||||
ChatType: isGroup ? "group" : "direct",
|
ChatType: isGroup ? "group" : "direct",
|
||||||
GroupSubject: isGroup ? (message.chat_name ?? undefined) : undefined,
|
GroupSubject: isGroup ? (message.chat_name ?? undefined) : undefined,
|
||||||
GroupMembers: isGroup ? (message.participants ?? []).filter(Boolean).join(", ") : undefined,
|
GroupMembers: isGroup ? (message.participants ?? []).filter(Boolean).join(", ") : undefined,
|
||||||
SenderName: sender,
|
SenderName: senderNormalized,
|
||||||
SenderId: sender,
|
SenderId: sender,
|
||||||
Provider: "imessage",
|
Provider: "imessage",
|
||||||
Surface: "imessage",
|
Surface: "imessage",
|
||||||
|
|||||||
Reference in New Issue
Block a user