From 8b4bcc6b7a39cca865235fe95578ee5a199c808e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 7 Jan 2026 23:52:20 +0000 Subject: [PATCH] refactor: centralize message provider normalization --- src/commands/agent-via-gateway.ts | 9 ++------- src/commands/agent.ts | 18 +++++++++--------- src/commands/send.ts | 4 ++-- src/gateway/hooks.ts | 29 ++++++++++++++++------------- src/gateway/server-methods/agent.ts | 10 ++-------- src/gateway/server-methods/send.ts | 7 +++---- src/gateway/server.agent.test.ts | 20 +++++++++++--------- src/utils/message-provider.ts | 16 ++++++++++++++++ 8 files changed, 61 insertions(+), 52 deletions(-) create mode 100644 src/utils/message-provider.ts diff --git a/src/commands/agent-via-gateway.ts b/src/commands/agent-via-gateway.ts index b841b5da5..6071714c6 100644 --- a/src/commands/agent-via-gateway.ts +++ b/src/commands/agent-via-gateway.ts @@ -7,6 +7,7 @@ import { } from "../config/sessions.js"; import { callGateway, randomIdempotencyKey } from "../gateway/call.js"; import type { RuntimeEnv } from "../runtime.js"; +import { normalizeMessageProvider } from "../utils/message-provider.js"; import { agentCommand } from "./agent.js"; type AgentGatewayResult = { @@ -85,12 +86,6 @@ function parseTimeoutSeconds(opts: { return raw; } -function normalizeProvider(raw?: string): string | undefined { - const normalized = raw?.trim().toLowerCase(); - if (!normalized) return undefined; - return normalized === "imsg" ? "imessage" : normalized; -} - function formatPayloadForLog(payload: { text?: string; mediaUrls?: string[]; @@ -127,7 +122,7 @@ export async function agentViaGatewayCommand( sessionId: opts.sessionId, }); - const provider = normalizeProvider(opts.provider) ?? "whatsapp"; + const provider = normalizeMessageProvider(opts.provider) ?? "whatsapp"; const idempotencyKey = opts.runId?.trim() || randomIdempotencyKey(); const response = await callGateway({ diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 6386befe0..0bbd5fcac 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -56,6 +56,10 @@ import { import { resolveOutboundTarget } from "../infra/outbound/targets.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { resolveSendPolicy } from "../sessions/send-policy.js"; +import { + normalizeMessageProvider, + resolveMessageProvider, +} from "../utils/message-provider.js"; import { normalizeE164 } from "../utils.js"; type AgentCommandOpts = { @@ -395,13 +399,10 @@ export async function agentCommand( let fallbackProvider = provider; let fallbackModel = model; try { - const messageProvider = - opts.messageProvider?.trim().toLowerCase() || - (() => { - const raw = opts.provider?.trim().toLowerCase(); - if (!raw) return undefined; - return raw === "imsg" ? "imessage" : raw; - })(); + const messageProvider = resolveMessageProvider( + opts.messageProvider, + opts.provider, + ); const fallbackResult = await runWithModelFallback({ cfg, provider, @@ -514,9 +515,8 @@ export async function agentCommand( const payloads = result.payloads ?? []; const deliver = opts.deliver === true; const bestEffortDeliver = opts.bestEffortDeliver === true; - const deliveryProviderRaw = (opts.provider ?? "whatsapp").toLowerCase(); const deliveryProvider = - deliveryProviderRaw === "imsg" ? "imessage" : deliveryProviderRaw; + normalizeMessageProvider(opts.provider) ?? "whatsapp"; const logDeliveryError = (err: unknown) => { const message = `Delivery failed (${deliveryProvider}${deliveryTarget ? ` to ${deliveryTarget}` : ""}): ${String(err)}`; diff --git a/src/commands/send.ts b/src/commands/send.ts index 8896d5357..7a5cbfc24 100644 --- a/src/commands/send.ts +++ b/src/commands/send.ts @@ -11,6 +11,7 @@ import { } from "../infra/outbound/format.js"; import { resolveOutboundTarget } from "../infra/outbound/targets.js"; import type { RuntimeEnv } from "../runtime.js"; +import { normalizeMessageProvider } from "../utils/message-provider.js"; export async function sendCommand( opts: { @@ -26,8 +27,7 @@ export async function sendCommand( deps: CliDeps, runtime: RuntimeEnv, ) { - const providerRaw = (opts.provider ?? "whatsapp").toLowerCase(); - const provider = providerRaw === "imsg" ? "imessage" : providerRaw; + const provider = normalizeMessageProvider(opts.provider) ?? "whatsapp"; if (opts.dryRun) { runtime.log( diff --git a/src/gateway/hooks.ts b/src/gateway/hooks.ts index 0cfc5490b..542d53a17 100644 --- a/src/gateway/hooks.ts +++ b/src/gateway/hooks.ts @@ -1,6 +1,7 @@ import { randomUUID } from "node:crypto"; import type { IncomingMessage } from "node:http"; import type { ClawdbotConfig } from "../config/config.js"; +import { normalizeMessageProvider } from "../utils/message-provider.js"; import { type HookMappingResolved, resolveHookMappings, @@ -174,20 +175,22 @@ export function normalizeAgentPayload( ? sessionKeyRaw.trim() : `hook:${idFactory()}`; const providerRaw = payload.provider; + const providerNormalized = + typeof providerRaw === "string" + ? normalizeMessageProvider(providerRaw) + : undefined; const provider = - providerRaw === "whatsapp" || - providerRaw === "telegram" || - providerRaw === "discord" || - providerRaw === "slack" || - providerRaw === "signal" || - providerRaw === "imessage" || - providerRaw === "last" - ? providerRaw - : providerRaw === "imsg" - ? "imessage" - : providerRaw === undefined - ? "last" - : null; + providerNormalized === "whatsapp" || + providerNormalized === "telegram" || + providerNormalized === "discord" || + providerNormalized === "slack" || + providerNormalized === "signal" || + providerNormalized === "imessage" || + providerNormalized === "last" + ? providerNormalized + : providerRaw === undefined + ? "last" + : null; if (provider === null) { return { ok: false, diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index cdf55d8e5..790929fcc 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -11,6 +11,7 @@ import { import { registerAgentRunContext } from "../../infra/agent-events.js"; import { defaultRuntime } from "../../runtime.js"; import { resolveSendPolicy } from "../../sessions/send-policy.js"; +import { normalizeMessageProvider } from "../../utils/message-provider.js"; import { normalizeE164 } from "../../utils.js"; import { type AgentWaitParams, @@ -138,15 +139,8 @@ export const agentHandlers: GatewayRequestHandlers = { const runId = idem; - const requestedProviderRaw = - typeof request.provider === "string" ? request.provider.trim() : ""; - const requestedProviderNormalized = requestedProviderRaw - ? requestedProviderRaw.toLowerCase() - : "last"; const requestedProvider = - requestedProviderNormalized === "imsg" - ? "imessage" - : requestedProviderNormalized; + normalizeMessageProvider(request.provider) ?? "last"; const lastProvider = sessionEntry?.lastProvider; const lastTo = diff --git a/src/gateway/server-methods/send.ts b/src/gateway/server-methods/send.ts index 9e9f1e277..08301d968 100644 --- a/src/gateway/server-methods/send.ts +++ b/src/gateway/server-methods/send.ts @@ -6,6 +6,7 @@ import { sendMessageSignal } from "../../signal/index.js"; import { sendMessageSlack } from "../../slack/send.js"; import { sendMessageTelegram } from "../../telegram/send.js"; import { resolveTelegramToken } from "../../telegram/token.js"; +import { normalizeMessageProvider } from "../../utils/message-provider.js"; import { resolveDefaultWhatsAppAccountId } from "../../web/accounts.js"; import { sendMessageWhatsApp, sendPollWhatsApp } from "../../web/outbound.js"; import { @@ -51,8 +52,7 @@ export const sendHandlers: GatewayRequestHandlers = { } const to = request.to.trim(); const message = request.message.trim(); - const providerRaw = (request.provider ?? "whatsapp").toLowerCase(); - const provider = providerRaw === "imsg" ? "imessage" : providerRaw; + const provider = normalizeMessageProvider(request.provider) ?? "whatsapp"; try { if (provider === "telegram") { const cfg = loadConfig(); @@ -220,8 +220,7 @@ export const sendHandlers: GatewayRequestHandlers = { return; } const to = request.to.trim(); - const providerRaw = (request.provider ?? "whatsapp").toLowerCase(); - const provider = providerRaw === "imsg" ? "imessage" : providerRaw; + const provider = normalizeMessageProvider(request.provider) ?? "whatsapp"; if (provider !== "whatsapp" && provider !== "discord") { respond( false, diff --git a/src/gateway/server.agent.test.ts b/src/gateway/server.agent.test.ts index fb94ec16b..6f47d962a 100644 --- a/src/gateway/server.agent.test.ts +++ b/src/gateway/server.agent.test.ts @@ -21,6 +21,11 @@ import { installGatewayTestHooks(); +function expectProviders(call: Record, provider: string) { + expect(call.provider).toBe(provider); + expect(call.messageProvider).toBe(provider); +} + describe("gateway server agent", () => { test("agent falls back to allowFrom when lastTo is stale", async () => { testState.allowFrom = ["+436769770569"]; @@ -57,8 +62,7 @@ describe("gateway server agent", () => { const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; - expect(call.provider).toBe("whatsapp"); - expect(call.messageProvider).toBe("whatsapp"); + expectProviders(call, "whatsapp"); expect(call.to).toBe("+436769770569"); expect(call.sessionId).toBe("sess-main-stale"); @@ -138,7 +142,7 @@ describe("gateway server agent", () => { const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; - expect(call.provider).toBe("whatsapp"); + expectProviders(call, "whatsapp"); expect(call.messageProvider).toBe("whatsapp"); expect(call.to).toBe("+1555"); expect(call.deliver).toBe(true); @@ -183,8 +187,7 @@ describe("gateway server agent", () => { const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; - expect(call.provider).toBe("telegram"); - expect(call.messageProvider).toBe("telegram"); + expectProviders(call, "telegram"); expect(call.to).toBe("123"); expect(call.deliver).toBe(true); expect(call.bestEffortDeliver).toBe(true); @@ -228,8 +231,7 @@ describe("gateway server agent", () => { const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; - expect(call.provider).toBe("discord"); - expect(call.messageProvider).toBe("discord"); + expectProviders(call, "discord"); expect(call.to).toBe("channel:discord-123"); expect(call.deliver).toBe(true); expect(call.bestEffortDeliver).toBe(true); @@ -273,7 +275,7 @@ describe("gateway server agent", () => { const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; - expect(call.provider).toBe("signal"); + expectProviders(call, "signal"); expect(call.to).toBe("+15551234567"); expect(call.deliver).toBe(true); expect(call.bestEffortDeliver).toBe(true); @@ -318,7 +320,7 @@ describe("gateway server agent", () => { const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; - expect(call.provider).toBe("whatsapp"); + expectProviders(call, "whatsapp"); expect(call.to).toBe("+1555"); expect(call.deliver).toBe(true); expect(call.bestEffortDeliver).toBe(true); diff --git a/src/utils/message-provider.ts b/src/utils/message-provider.ts new file mode 100644 index 000000000..2a1c7ab02 --- /dev/null +++ b/src/utils/message-provider.ts @@ -0,0 +1,16 @@ +export function normalizeMessageProvider( + raw?: string | null, +): string | undefined { + const normalized = raw?.trim().toLowerCase(); + if (!normalized) return undefined; + return normalized === "imsg" ? "imessage" : normalized; +} + +export function resolveMessageProvider( + primary?: string | null, + fallback?: string | null, +): string | undefined { + return ( + normalizeMessageProvider(primary) ?? normalizeMessageProvider(fallback) + ); +}