import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import type { ChannelId } from "../../channels/plugins/types.js"; import type { MoltbotConfig } from "../../config/config.js"; import { loadConfig } from "../../config/config.js"; import { callGateway, randomIdempotencyKey } from "../../gateway/call.js"; import type { PollInput } from "../../polls.js"; import { normalizePollInput } from "../../polls.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, type GatewayClientMode, type GatewayClientName, } from "../../utils/message-channel.js"; import { resolveMessageChannelSelection } from "./channel-selection.js"; import { deliverOutboundPayloads, type OutboundDeliveryResult, type OutboundSendDeps, } from "./deliver.js"; import { normalizeReplyPayloadsForDelivery } from "./payloads.js"; import type { OutboundChannel } from "./targets.js"; import { resolveOutboundTarget } from "./targets.js"; export type MessageGatewayOptions = { url?: string; token?: string; timeoutMs?: number; clientName?: GatewayClientName; clientDisplayName?: string; mode?: GatewayClientMode; }; type MessageSendParams = { to: string; content: string; channel?: string; mediaUrl?: string; mediaUrls?: string[]; gifPlayback?: boolean; accountId?: string; dryRun?: boolean; bestEffort?: boolean; deps?: OutboundSendDeps; cfg?: MoltbotConfig; gateway?: MessageGatewayOptions; idempotencyKey?: string; mirror?: { sessionKey: string; agentId?: string; text?: string; mediaUrls?: string[]; }; abortSignal?: AbortSignal; }; export type MessageSendResult = { channel: string; to: string; via: "direct" | "gateway"; mediaUrl: string | null; mediaUrls?: string[]; result?: OutboundDeliveryResult | { messageId: string }; dryRun?: boolean; }; type MessagePollParams = { to: string; question: string; options: string[]; maxSelections?: number; durationHours?: number; channel?: string; dryRun?: boolean; cfg?: MoltbotConfig; gateway?: MessageGatewayOptions; idempotencyKey?: string; }; export type MessagePollResult = { channel: string; to: string; question: string; options: string[]; maxSelections: number; durationHours: number | null; via: "gateway"; result?: { messageId: string; toJid?: string; channelId?: string; conversationId?: string; pollId?: string; }; dryRun?: boolean; }; function resolveGatewayOptions(opts?: MessageGatewayOptions) { return { url: opts?.url, token: opts?.token, timeoutMs: typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) ? Math.max(1, Math.floor(opts.timeoutMs)) : 10_000, clientName: opts?.clientName ?? GATEWAY_CLIENT_NAMES.CLI, clientDisplayName: opts?.clientDisplayName, mode: opts?.mode ?? GATEWAY_CLIENT_MODES.CLI, }; } export async function sendMessage(params: MessageSendParams): Promise { const cfg = params.cfg ?? loadConfig(); const channel = params.channel?.trim() ? normalizeChannelId(params.channel) : (await resolveMessageChannelSelection({ cfg })).channel; if (!channel) { throw new Error(`Unknown channel: ${params.channel}`); } const plugin = getChannelPlugin(channel as ChannelId); if (!plugin) { throw new Error(`Unknown channel: ${channel}`); } const deliveryMode = plugin.outbound?.deliveryMode ?? "direct"; const normalizedPayloads = normalizeReplyPayloadsForDelivery([ { text: params.content, mediaUrl: params.mediaUrl, mediaUrls: params.mediaUrls, }, ]); const mirrorText = normalizedPayloads .map((payload) => payload.text) .filter(Boolean) .join("\n"); const mirrorMediaUrls = normalizedPayloads.flatMap( (payload) => payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []), ); const primaryMediaUrl = mirrorMediaUrls[0] ?? params.mediaUrl ?? null; if (params.dryRun) { return { channel, to: params.to, via: deliveryMode === "gateway" ? "gateway" : "direct", mediaUrl: primaryMediaUrl, mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : undefined, dryRun: true, }; } if (deliveryMode !== "gateway") { const outboundChannel = channel as Exclude; const resolvedTarget = resolveOutboundTarget({ channel: outboundChannel, to: params.to, cfg, accountId: params.accountId, mode: "explicit", }); if (!resolvedTarget.ok) throw resolvedTarget.error; const results = await deliverOutboundPayloads({ cfg, channel: outboundChannel, to: resolvedTarget.to, accountId: params.accountId, payloads: normalizedPayloads, gifPlayback: params.gifPlayback, deps: params.deps, bestEffort: params.bestEffort, abortSignal: params.abortSignal, mirror: params.mirror ? { ...params.mirror, text: mirrorText || params.content, mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : undefined, } : undefined, }); return { channel, to: params.to, via: "direct", mediaUrl: primaryMediaUrl, mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : undefined, result: results.at(-1), }; } const gateway = resolveGatewayOptions(params.gateway); const result = await callGateway<{ messageId: string }>({ url: gateway.url, token: gateway.token, method: "send", params: { to: params.to, message: params.content, mediaUrl: params.mediaUrl, mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : params.mediaUrls, gifPlayback: params.gifPlayback, accountId: params.accountId, channel, sessionKey: params.mirror?.sessionKey, idempotencyKey: params.idempotencyKey ?? randomIdempotencyKey(), }, timeoutMs: gateway.timeoutMs, clientName: gateway.clientName, clientDisplayName: gateway.clientDisplayName, mode: gateway.mode, }); return { channel, to: params.to, via: "gateway", mediaUrl: primaryMediaUrl, mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : undefined, result, }; } export async function sendPoll(params: MessagePollParams): Promise { const cfg = params.cfg ?? loadConfig(); const channel = params.channel?.trim() ? normalizeChannelId(params.channel) : (await resolveMessageChannelSelection({ cfg })).channel; if (!channel) { throw new Error(`Unknown channel: ${params.channel}`); } const pollInput: PollInput = { question: params.question, options: params.options, maxSelections: params.maxSelections, durationHours: params.durationHours, }; const plugin = getChannelPlugin(channel as ChannelId); const outbound = plugin?.outbound; if (!outbound?.sendPoll) { throw new Error(`Unsupported poll channel: ${channel}`); } const normalized = outbound.pollMaxOptions ? normalizePollInput(pollInput, { maxOptions: outbound.pollMaxOptions }) : normalizePollInput(pollInput); if (params.dryRun) { return { channel, to: params.to, question: normalized.question, options: normalized.options, maxSelections: normalized.maxSelections, durationHours: normalized.durationHours ?? null, via: "gateway", dryRun: true, }; } const gateway = resolveGatewayOptions(params.gateway); const result = await callGateway<{ messageId: string; toJid?: string; channelId?: string; conversationId?: string; pollId?: string; }>({ url: gateway.url, token: gateway.token, method: "poll", params: { to: params.to, question: normalized.question, options: normalized.options, maxSelections: normalized.maxSelections, durationHours: normalized.durationHours, channel, idempotencyKey: params.idempotencyKey ?? randomIdempotencyKey(), }, timeoutMs: gateway.timeoutMs, clientName: gateway.clientName, clientDisplayName: gateway.clientDisplayName, mode: gateway.mode, }); return { channel, to: params.to, question: normalized.question, options: normalized.options, maxSelections: normalized.maxSelections, durationHours: normalized.durationHours ?? null, via: "gateway", result, }; }