Files
clawdbot/src/gateway/server-methods/send.ts
2026-01-06 18:33:37 +00:00

295 lines
9.0 KiB
TypeScript

import { loadConfig } from "../../config/config.js";
import { sendMessageDiscord, sendPollDiscord } from "../../discord/index.js";
import { shouldLogVerbose } from "../../globals.js";
import { sendMessageIMessage } from "../../imessage/index.js";
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 { resolveDefaultWhatsAppAccountId } from "../../web/accounts.js";
import { sendMessageWhatsApp, sendPollWhatsApp } from "../../web/outbound.js";
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validatePollParams,
validateSendParams,
} from "../protocol/index.js";
import { formatForLog } from "../ws-log.js";
import type { GatewayRequestHandlers } from "./types.js";
export const sendHandlers: GatewayRequestHandlers = {
send: async ({ params, respond, context }) => {
const p = params as Record<string, unknown>;
if (!validateSendParams(p)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid send params: ${formatValidationErrors(validateSendParams.errors)}`,
),
);
return;
}
const request = p as {
to: string;
message: string;
mediaUrl?: string;
gifPlayback?: boolean;
provider?: string;
accountId?: string;
idempotencyKey: string;
};
const idem = request.idempotencyKey;
const cached = context.dedupe.get(`send:${idem}`);
if (cached) {
respond(cached.ok, cached.payload, cached.error, {
cached: true,
});
return;
}
const to = request.to.trim();
const message = request.message.trim();
const providerRaw = (request.provider ?? "whatsapp").toLowerCase();
const provider = providerRaw === "imsg" ? "imessage" : providerRaw;
try {
if (provider === "telegram") {
const cfg = loadConfig();
const { token } = resolveTelegramToken(cfg);
const result = await sendMessageTelegram(to, message, {
mediaUrl: request.mediaUrl,
verbose: shouldLogVerbose(),
token: token || undefined,
});
const payload = {
runId: idem,
messageId: result.messageId,
chatId: result.chatId,
provider,
};
context.dedupe.set(`send:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined, { provider });
} else if (provider === "discord") {
const result = await sendMessageDiscord(to, message, {
mediaUrl: request.mediaUrl,
token: process.env.DISCORD_BOT_TOKEN,
});
const payload = {
runId: idem,
messageId: result.messageId,
channelId: result.channelId,
provider,
};
context.dedupe.set(`send:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined, { provider });
} else if (provider === "slack") {
const result = await sendMessageSlack(to, message, {
mediaUrl: request.mediaUrl,
});
const payload = {
runId: idem,
messageId: result.messageId,
channelId: result.channelId,
provider,
};
context.dedupe.set(`send:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined, { provider });
} else if (provider === "signal") {
const cfg = loadConfig();
const host = cfg.signal?.httpHost?.trim() || "127.0.0.1";
const port = cfg.signal?.httpPort ?? 8080;
const baseUrl = cfg.signal?.httpUrl?.trim() || `http://${host}:${port}`;
const result = await sendMessageSignal(to, message, {
mediaUrl: request.mediaUrl,
baseUrl,
account: cfg.signal?.account,
});
const payload = {
runId: idem,
messageId: result.messageId,
provider,
};
context.dedupe.set(`send:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined, { provider });
} else if (provider === "imessage") {
const cfg = loadConfig();
const result = await sendMessageIMessage(to, message, {
mediaUrl: request.mediaUrl,
cliPath: cfg.imessage?.cliPath,
dbPath: cfg.imessage?.dbPath,
maxBytes: cfg.imessage?.mediaMaxMb
? cfg.imessage.mediaMaxMb * 1024 * 1024
: undefined,
});
const payload = {
runId: idem,
messageId: result.messageId,
provider,
};
context.dedupe.set(`send:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined, { provider });
} else {
const cfg = loadConfig();
const accountId =
typeof request.accountId === "string" &&
request.accountId.trim().length > 0
? request.accountId.trim()
: resolveDefaultWhatsAppAccountId(cfg);
const result = await sendMessageWhatsApp(to, message, {
mediaUrl: request.mediaUrl,
verbose: shouldLogVerbose(),
gifPlayback: request.gifPlayback,
accountId,
});
const payload = {
runId: idem,
messageId: result.messageId,
toJid: result.toJid ?? `${to}@s.whatsapp.net`,
provider,
};
context.dedupe.set(`send:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined, { provider });
}
} catch (err) {
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
context.dedupe.set(`send:${idem}`, {
ts: Date.now(),
ok: false,
error,
});
respond(false, undefined, error, {
provider,
error: formatForLog(err),
});
}
},
poll: async ({ params, respond, context }) => {
const p = params as Record<string, unknown>;
if (!validatePollParams(p)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid poll params: ${formatValidationErrors(validatePollParams.errors)}`,
),
);
return;
}
const request = p as {
to: string;
question: string;
options: string[];
maxSelections?: number;
durationHours?: number;
provider?: string;
accountId?: string;
idempotencyKey: string;
};
const idem = request.idempotencyKey;
const cached = context.dedupe.get(`poll:${idem}`);
if (cached) {
respond(cached.ok, cached.payload, cached.error, {
cached: true,
});
return;
}
const to = request.to.trim();
const providerRaw = (request.provider ?? "whatsapp").toLowerCase();
const provider = providerRaw === "imsg" ? "imessage" : providerRaw;
if (provider !== "whatsapp" && provider !== "discord") {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`unsupported poll provider: ${provider}`,
),
);
return;
}
const poll = {
question: request.question,
options: request.options,
maxSelections: request.maxSelections,
durationHours: request.durationHours,
};
try {
if (provider === "discord") {
const result = await sendPollDiscord(to, poll);
const payload = {
runId: idem,
messageId: result.messageId,
channelId: result.channelId,
provider,
};
context.dedupe.set(`poll:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined, { provider });
} else {
const cfg = loadConfig();
const accountId =
typeof request.accountId === "string" &&
request.accountId.trim().length > 0
? request.accountId.trim()
: resolveDefaultWhatsAppAccountId(cfg);
const result = await sendPollWhatsApp(to, poll, {
verbose: shouldLogVerbose(),
accountId,
});
const payload = {
runId: idem,
messageId: result.messageId,
toJid: result.toJid ?? `${to}@s.whatsapp.net`,
provider,
};
context.dedupe.set(`poll:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined, { provider });
}
} catch (err) {
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
context.dedupe.set(`poll:${idem}`, {
ts: Date.now(),
ok: false,
error,
});
respond(false, undefined, error, {
provider,
error: formatForLog(err),
});
}
},
};