Files
clawdbot/src/gateway/server-methods/providers.ts
2026-01-04 14:38:51 +00:00

274 lines
9.8 KiB
TypeScript

import type { ClawdbotConfig } from "../../config/config.js";
import {
loadConfig,
readConfigFileSnapshot,
writeConfigFile,
} from "../../config/config.js";
import { type DiscordProbe, probeDiscord } from "../../discord/probe.js";
import { type IMessageProbe, probeIMessage } from "../../imessage/probe.js";
import { webAuthExists } from "../../providers/web/index.js";
import { probeSignal, type SignalProbe } from "../../signal/probe.js";
import { probeSlack, type SlackProbe } from "../../slack/probe.js";
import {
resolveSlackAppToken,
resolveSlackBotToken,
} from "../../slack/token.js";
import { probeTelegram, type TelegramProbe } from "../../telegram/probe.js";
import { resolveTelegramToken } from "../../telegram/token.js";
import { getWebAuthAgeMs, readWebSelfId } from "../../web/session.js";
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateProvidersStatusParams,
} from "../protocol/index.js";
import { formatForLog } from "../ws-log.js";
import type { GatewayRequestHandlers } from "./types.js";
export const providersHandlers: GatewayRequestHandlers = {
"providers.status": async ({ params, respond, context }) => {
if (!validateProvidersStatusParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid providers.status params: ${formatValidationErrors(validateProvidersStatusParams.errors)}`,
),
);
return;
}
const probe = (params as { probe?: boolean }).probe === true;
const timeoutMsRaw = (params as { timeoutMs?: unknown }).timeoutMs;
const timeoutMs =
typeof timeoutMsRaw === "number" ? Math.max(1000, timeoutMsRaw) : 10_000;
const cfg = loadConfig();
const telegramCfg = cfg.telegram;
const telegramEnabled =
Boolean(telegramCfg) && telegramCfg?.enabled !== false;
const { token: telegramToken, source: tokenSource } = telegramEnabled
? resolveTelegramToken(cfg)
: { token: "", source: "none" as const };
let telegramProbe: TelegramProbe | undefined;
let lastProbeAt: number | null = null;
if (probe && telegramToken && telegramEnabled) {
telegramProbe = await probeTelegram(
telegramToken,
timeoutMs,
telegramCfg?.proxy,
);
lastProbeAt = Date.now();
}
const discordCfg = cfg.discord;
const discordEnabled = Boolean(discordCfg) && discordCfg?.enabled !== false;
const discordEnvToken = discordEnabled
? process.env.DISCORD_BOT_TOKEN?.trim()
: "";
const discordConfigToken = discordEnabled ? discordCfg?.token?.trim() : "";
const discordToken = discordEnvToken || discordConfigToken || "";
const discordTokenSource = discordEnvToken
? "env"
: discordConfigToken
? "config"
: "none";
let discordProbe: DiscordProbe | undefined;
let discordLastProbeAt: number | null = null;
if (probe && discordToken && discordEnabled) {
discordProbe = await probeDiscord(discordToken, timeoutMs);
discordLastProbeAt = Date.now();
}
const slackCfg = cfg.slack;
const slackEnabled = slackCfg?.enabled !== false;
const slackBotEnvToken = slackEnabled
? resolveSlackBotToken(process.env.SLACK_BOT_TOKEN)
: undefined;
const slackBotConfigToken = slackEnabled
? resolveSlackBotToken(slackCfg?.botToken)
: undefined;
const slackBotToken = slackBotEnvToken ?? slackBotConfigToken ?? "";
const slackBotTokenSource = slackBotEnvToken
? "env"
: slackBotConfigToken
? "config"
: "none";
const slackAppEnvToken = slackEnabled
? resolveSlackAppToken(process.env.SLACK_APP_TOKEN)
: undefined;
const slackAppConfigToken = slackEnabled
? resolveSlackAppToken(slackCfg?.appToken)
: undefined;
const slackAppToken = slackAppEnvToken ?? slackAppConfigToken ?? "";
const slackAppTokenSource = slackAppEnvToken
? "env"
: slackAppConfigToken
? "config"
: "none";
const slackConfigured =
slackEnabled && Boolean(slackBotToken) && Boolean(slackAppToken);
let slackProbe: SlackProbe | undefined;
let slackLastProbeAt: number | null = null;
if (probe && slackConfigured) {
slackProbe = await probeSlack(slackBotToken, timeoutMs);
slackLastProbeAt = Date.now();
}
const signalCfg = cfg.signal;
const signalEnabled = signalCfg?.enabled !== false;
const signalHost = signalCfg?.httpHost?.trim() || "127.0.0.1";
const signalPort = signalCfg?.httpPort ?? 8080;
const signalBaseUrl =
signalCfg?.httpUrl?.trim() || `http://${signalHost}:${signalPort}`;
const signalConfigured =
Boolean(signalCfg) &&
signalEnabled &&
Boolean(
signalCfg?.account?.trim() ||
signalCfg?.httpUrl?.trim() ||
signalCfg?.cliPath?.trim() ||
signalCfg?.httpHost?.trim() ||
typeof signalCfg?.httpPort === "number" ||
typeof signalCfg?.autoStart === "boolean",
);
let signalProbe: SignalProbe | undefined;
let signalLastProbeAt: number | null = null;
if (probe && signalConfigured) {
signalProbe = await probeSignal(signalBaseUrl, timeoutMs);
signalLastProbeAt = Date.now();
}
const imessageCfg = cfg.imessage;
const imessageEnabled = imessageCfg?.enabled !== false;
const imessageConfigured = Boolean(imessageCfg) && imessageEnabled;
let imessageProbe: IMessageProbe | undefined;
let imessageLastProbeAt: number | null = null;
if (probe && imessageConfigured) {
imessageProbe = await probeIMessage(timeoutMs);
imessageLastProbeAt = Date.now();
}
const linked = await webAuthExists();
const authAgeMs = getWebAuthAgeMs();
const self = readWebSelfId();
const runtime = context.getRuntimeSnapshot();
respond(
true,
{
ts: Date.now(),
whatsapp: {
configured: linked,
linked,
authAgeMs,
self,
running: runtime.whatsapp.running,
connected: runtime.whatsapp.connected,
lastConnectedAt: runtime.whatsapp.lastConnectedAt ?? null,
lastDisconnect: runtime.whatsapp.lastDisconnect ?? null,
reconnectAttempts: runtime.whatsapp.reconnectAttempts,
lastMessageAt: runtime.whatsapp.lastMessageAt ?? null,
lastEventAt: runtime.whatsapp.lastEventAt ?? null,
lastError: runtime.whatsapp.lastError ?? null,
},
telegram: {
configured: telegramEnabled && Boolean(telegramToken),
tokenSource,
running: runtime.telegram.running,
mode: runtime.telegram.mode ?? null,
lastStartAt: runtime.telegram.lastStartAt ?? null,
lastStopAt: runtime.telegram.lastStopAt ?? null,
lastError: runtime.telegram.lastError ?? null,
probe: telegramProbe,
lastProbeAt,
},
discord: {
configured: discordEnabled && Boolean(discordToken),
tokenSource: discordTokenSource,
running: runtime.discord.running,
lastStartAt: runtime.discord.lastStartAt ?? null,
lastStopAt: runtime.discord.lastStopAt ?? null,
lastError: runtime.discord.lastError ?? null,
probe: discordProbe,
lastProbeAt: discordLastProbeAt,
},
slack: {
configured: slackConfigured,
botTokenSource: slackBotTokenSource,
appTokenSource: slackAppTokenSource,
running: runtime.slack.running,
lastStartAt: runtime.slack.lastStartAt ?? null,
lastStopAt: runtime.slack.lastStopAt ?? null,
lastError: runtime.slack.lastError ?? null,
probe: slackProbe,
lastProbeAt: slackLastProbeAt,
},
signal: {
configured: signalConfigured,
baseUrl: signalBaseUrl,
running: runtime.signal.running,
lastStartAt: runtime.signal.lastStartAt ?? null,
lastStopAt: runtime.signal.lastStopAt ?? null,
lastError: runtime.signal.lastError ?? null,
probe: signalProbe,
lastProbeAt: signalLastProbeAt,
},
imessage: {
configured: imessageConfigured,
running: runtime.imessage.running,
lastStartAt: runtime.imessage.lastStartAt ?? null,
lastStopAt: runtime.imessage.lastStopAt ?? null,
lastError: runtime.imessage.lastError ?? null,
cliPath: runtime.imessage.cliPath ?? null,
dbPath: runtime.imessage.dbPath ?? null,
probe: imessageProbe,
lastProbeAt: imessageLastProbeAt,
},
},
undefined,
);
},
"telegram.logout": async ({ respond, context }) => {
try {
await context.stopTelegramProvider();
const snapshot = await readConfigFileSnapshot();
if (!snapshot.valid) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
"config invalid; fix it before logging out",
),
);
return;
}
const cfg = snapshot.config ?? {};
const envToken = process.env.TELEGRAM_BOT_TOKEN?.trim() ?? "";
const hadToken = Boolean(cfg.telegram?.botToken);
const nextTelegram = cfg.telegram ? { ...cfg.telegram } : undefined;
if (nextTelegram) {
delete nextTelegram.botToken;
}
const nextCfg = { ...cfg } as ClawdbotConfig;
if (nextTelegram && Object.keys(nextTelegram).length > 0) {
nextCfg.telegram = nextTelegram;
} else {
delete nextCfg.telegram;
}
await writeConfigFile(nextCfg);
respond(
true,
{ cleared: hadToken, envToken: Boolean(envToken) },
undefined,
);
} catch (err) {
respond(
false,
undefined,
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
);
}
},
};