455 lines
17 KiB
TypeScript
455 lines
17 KiB
TypeScript
import type { ClawdbotConfig } from "../../config/config.js";
|
|
import {
|
|
loadConfig,
|
|
readConfigFileSnapshot,
|
|
writeConfigFile,
|
|
} from "../../config/config.js";
|
|
import {
|
|
listDiscordAccountIds,
|
|
resolveDefaultDiscordAccountId,
|
|
resolveDiscordAccount,
|
|
} from "../../discord/accounts.js";
|
|
import { type DiscordProbe, probeDiscord } from "../../discord/probe.js";
|
|
import {
|
|
listIMessageAccountIds,
|
|
resolveDefaultIMessageAccountId,
|
|
resolveIMessageAccount,
|
|
} from "../../imessage/accounts.js";
|
|
import { type IMessageProbe, probeIMessage } from "../../imessage/probe.js";
|
|
import {
|
|
listSignalAccountIds,
|
|
resolveDefaultSignalAccountId,
|
|
resolveSignalAccount,
|
|
} from "../../signal/accounts.js";
|
|
import { probeSignal, type SignalProbe } from "../../signal/probe.js";
|
|
import {
|
|
listSlackAccountIds,
|
|
resolveDefaultSlackAccountId,
|
|
resolveSlackAccount,
|
|
} from "../../slack/accounts.js";
|
|
import { probeSlack, type SlackProbe } from "../../slack/probe.js";
|
|
import {
|
|
listTelegramAccountIds,
|
|
resolveDefaultTelegramAccountId,
|
|
resolveTelegramAccount,
|
|
} from "../../telegram/accounts.js";
|
|
import { probeTelegram, type TelegramProbe } from "../../telegram/probe.js";
|
|
import {
|
|
listEnabledWhatsAppAccounts,
|
|
resolveDefaultWhatsAppAccountId,
|
|
} from "../../web/accounts.js";
|
|
import {
|
|
getWebAuthAgeMs,
|
|
readWebSelfId,
|
|
webAuthExists,
|
|
} 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 runtime = context.getRuntimeSnapshot();
|
|
|
|
const defaultTelegramAccountId = resolveDefaultTelegramAccountId(cfg);
|
|
const defaultDiscordAccountId = resolveDefaultDiscordAccountId(cfg);
|
|
const defaultSlackAccountId = resolveDefaultSlackAccountId(cfg);
|
|
const defaultSignalAccountId = resolveDefaultSignalAccountId(cfg);
|
|
const defaultIMessageAccountId = resolveDefaultIMessageAccountId(cfg);
|
|
|
|
const telegramAccounts = await Promise.all(
|
|
listTelegramAccountIds(cfg).map(async (accountId) => {
|
|
const account = resolveTelegramAccount({ cfg, accountId });
|
|
const rt =
|
|
runtime.telegramAccounts?.[account.accountId] ??
|
|
(account.accountId === defaultTelegramAccountId
|
|
? runtime.telegram
|
|
: undefined);
|
|
const configured = Boolean(account.token);
|
|
let telegramProbe: TelegramProbe | undefined;
|
|
let lastProbeAt: number | null = null;
|
|
if (probe && configured && account.enabled) {
|
|
telegramProbe = await probeTelegram(
|
|
account.token,
|
|
timeoutMs,
|
|
account.config.proxy,
|
|
);
|
|
lastProbeAt = Date.now();
|
|
}
|
|
const groups =
|
|
cfg.telegram?.accounts?.[account.accountId]?.groups ?? cfg.telegram?.groups;
|
|
const allowUnmentionedGroups =
|
|
Boolean(groups?.["*"] && (groups["*"] as { requireMention?: boolean }).requireMention === false) ||
|
|
Object.entries(groups ?? {}).some(
|
|
([key, value]) =>
|
|
key !== "*" &&
|
|
Boolean(value) &&
|
|
typeof value === "object" &&
|
|
(value as { requireMention?: boolean }).requireMention === false,
|
|
);
|
|
return {
|
|
accountId: account.accountId,
|
|
name: account.name,
|
|
enabled: account.enabled,
|
|
configured,
|
|
tokenSource: account.tokenSource,
|
|
running: rt?.running ?? false,
|
|
mode: rt?.mode ?? (account.config.webhookUrl ? "webhook" : "polling"),
|
|
lastStartAt: rt?.lastStartAt ?? null,
|
|
lastStopAt: rt?.lastStopAt ?? null,
|
|
lastError: rt?.lastError ?? null,
|
|
probe: telegramProbe,
|
|
lastProbeAt,
|
|
allowUnmentionedGroups,
|
|
};
|
|
}),
|
|
);
|
|
const defaultTelegramAccount =
|
|
telegramAccounts.find(
|
|
(account) => account.accountId === defaultTelegramAccountId,
|
|
) ?? telegramAccounts[0];
|
|
|
|
const discordAccounts = await Promise.all(
|
|
listDiscordAccountIds(cfg).map(async (accountId) => {
|
|
const account = resolveDiscordAccount({ cfg, accountId });
|
|
const rt =
|
|
runtime.discordAccounts?.[account.accountId] ??
|
|
(account.accountId === defaultDiscordAccountId
|
|
? runtime.discord
|
|
: undefined);
|
|
const configured = Boolean(account.token);
|
|
let discordProbe: DiscordProbe | undefined;
|
|
let lastProbeAt: number | null = null;
|
|
if (probe && configured && account.enabled) {
|
|
discordProbe = await probeDiscord(account.token, timeoutMs, {
|
|
includeApplication: true,
|
|
});
|
|
lastProbeAt = Date.now();
|
|
}
|
|
return {
|
|
accountId: account.accountId,
|
|
name: account.name,
|
|
enabled: account.enabled,
|
|
configured,
|
|
tokenSource: account.tokenSource,
|
|
bot: rt?.bot ?? null,
|
|
application: rt?.application ?? null,
|
|
running: rt?.running ?? false,
|
|
lastStartAt: rt?.lastStartAt ?? null,
|
|
lastStopAt: rt?.lastStopAt ?? null,
|
|
lastError: rt?.lastError ?? null,
|
|
probe: discordProbe,
|
|
lastProbeAt,
|
|
};
|
|
}),
|
|
);
|
|
const defaultDiscordAccount =
|
|
discordAccounts.find(
|
|
(account) => account.accountId === defaultDiscordAccountId,
|
|
) ?? discordAccounts[0];
|
|
|
|
const slackAccounts = await Promise.all(
|
|
listSlackAccountIds(cfg).map(async (accountId) => {
|
|
const account = resolveSlackAccount({ cfg, accountId });
|
|
const rt =
|
|
runtime.slackAccounts?.[account.accountId] ??
|
|
(account.accountId === defaultSlackAccountId
|
|
? runtime.slack
|
|
: undefined);
|
|
const configured = Boolean(account.botToken && account.appToken);
|
|
let slackProbe: SlackProbe | undefined;
|
|
let lastProbeAt: number | null = null;
|
|
if (probe && configured && account.enabled && account.botToken) {
|
|
slackProbe = await probeSlack(account.botToken, timeoutMs);
|
|
lastProbeAt = Date.now();
|
|
}
|
|
return {
|
|
accountId: account.accountId,
|
|
name: account.name,
|
|
enabled: account.enabled,
|
|
configured,
|
|
botTokenSource: account.botTokenSource,
|
|
appTokenSource: account.appTokenSource,
|
|
running: rt?.running ?? false,
|
|
lastStartAt: rt?.lastStartAt ?? null,
|
|
lastStopAt: rt?.lastStopAt ?? null,
|
|
lastError: rt?.lastError ?? null,
|
|
probe: slackProbe,
|
|
lastProbeAt,
|
|
};
|
|
}),
|
|
);
|
|
const defaultSlackAccount =
|
|
slackAccounts.find(
|
|
(account) => account.accountId === defaultSlackAccountId,
|
|
) ?? slackAccounts[0];
|
|
|
|
const signalAccounts = await Promise.all(
|
|
listSignalAccountIds(cfg).map(async (accountId) => {
|
|
const account = resolveSignalAccount({ cfg, accountId });
|
|
const rt =
|
|
runtime.signalAccounts?.[account.accountId] ??
|
|
(account.accountId === defaultSignalAccountId
|
|
? runtime.signal
|
|
: undefined);
|
|
const configured = account.configured;
|
|
let signalProbe: SignalProbe | undefined;
|
|
let lastProbeAt: number | null = null;
|
|
if (probe && configured && account.enabled) {
|
|
signalProbe = await probeSignal(account.baseUrl, timeoutMs);
|
|
lastProbeAt = Date.now();
|
|
}
|
|
return {
|
|
accountId: account.accountId,
|
|
name: account.name,
|
|
enabled: account.enabled,
|
|
configured,
|
|
baseUrl: account.baseUrl,
|
|
running: rt?.running ?? false,
|
|
lastStartAt: rt?.lastStartAt ?? null,
|
|
lastStopAt: rt?.lastStopAt ?? null,
|
|
lastError: rt?.lastError ?? null,
|
|
probe: signalProbe,
|
|
lastProbeAt,
|
|
};
|
|
}),
|
|
);
|
|
const defaultSignalAccount =
|
|
signalAccounts.find(
|
|
(account) => account.accountId === defaultSignalAccountId,
|
|
) ?? signalAccounts[0];
|
|
|
|
const imessageBaseConfigured = Boolean(cfg.imessage);
|
|
let imessageProbe: IMessageProbe | undefined;
|
|
let imessageLastProbeAt: number | null = null;
|
|
if (probe && imessageBaseConfigured) {
|
|
imessageProbe = await probeIMessage(timeoutMs);
|
|
imessageLastProbeAt = Date.now();
|
|
}
|
|
const imessageAccounts = listIMessageAccountIds(cfg).map((accountId) => {
|
|
const account = resolveIMessageAccount({ cfg, accountId });
|
|
const rt =
|
|
runtime.imessageAccounts?.[account.accountId] ??
|
|
(account.accountId === defaultIMessageAccountId
|
|
? runtime.imessage
|
|
: undefined);
|
|
return {
|
|
accountId: account.accountId,
|
|
name: account.name,
|
|
enabled: account.enabled,
|
|
configured: imessageBaseConfigured,
|
|
running: rt?.running ?? false,
|
|
lastStartAt: rt?.lastStartAt ?? null,
|
|
lastStopAt: rt?.lastStopAt ?? null,
|
|
lastError: rt?.lastError ?? null,
|
|
cliPath: rt?.cliPath ?? account.config.cliPath ?? null,
|
|
dbPath: rt?.dbPath ?? account.config.dbPath ?? null,
|
|
probe: imessageProbe,
|
|
lastProbeAt: imessageLastProbeAt,
|
|
};
|
|
});
|
|
const defaultIMessageAccount =
|
|
imessageAccounts.find(
|
|
(account) => account.accountId === defaultIMessageAccountId,
|
|
) ?? imessageAccounts[0];
|
|
const defaultWhatsAppAccountId = resolveDefaultWhatsAppAccountId(cfg);
|
|
const enabledWhatsAppAccounts = listEnabledWhatsAppAccounts(cfg);
|
|
const defaultWhatsAppAccount =
|
|
enabledWhatsAppAccounts.find(
|
|
(account) => account.accountId === defaultWhatsAppAccountId,
|
|
) ?? enabledWhatsAppAccounts[0];
|
|
const linked = defaultWhatsAppAccount
|
|
? await webAuthExists(defaultWhatsAppAccount.authDir)
|
|
: false;
|
|
const authAgeMs = defaultWhatsAppAccount
|
|
? getWebAuthAgeMs(defaultWhatsAppAccount.authDir)
|
|
: null;
|
|
const self = defaultWhatsAppAccount
|
|
? readWebSelfId(defaultWhatsAppAccount.authDir)
|
|
: { e164: null, jid: null };
|
|
|
|
const defaultWhatsAppStatus = {
|
|
running: false,
|
|
connected: false,
|
|
reconnectAttempts: 0,
|
|
lastConnectedAt: null,
|
|
lastDisconnect: null,
|
|
lastMessageAt: null,
|
|
lastEventAt: null,
|
|
lastError: null,
|
|
} as const;
|
|
const whatsappAccounts = await Promise.all(
|
|
enabledWhatsAppAccounts.map(async (account) => {
|
|
const rt =
|
|
runtime.whatsappAccounts?.[account.accountId] ??
|
|
defaultWhatsAppStatus;
|
|
return {
|
|
accountId: account.accountId,
|
|
enabled: account.enabled,
|
|
linked: await webAuthExists(account.authDir),
|
|
authAgeMs: getWebAuthAgeMs(account.authDir),
|
|
self: readWebSelfId(account.authDir),
|
|
running: rt.running,
|
|
connected: rt.connected,
|
|
lastConnectedAt: rt.lastConnectedAt ?? null,
|
|
lastDisconnect: rt.lastDisconnect ?? null,
|
|
reconnectAttempts: rt.reconnectAttempts,
|
|
lastMessageAt: rt.lastMessageAt ?? null,
|
|
lastEventAt: rt.lastEventAt ?? null,
|
|
lastError: rt.lastError ?? null,
|
|
};
|
|
}),
|
|
);
|
|
|
|
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,
|
|
},
|
|
whatsappAccounts,
|
|
whatsappDefaultAccountId: defaultWhatsAppAccountId,
|
|
telegram: {
|
|
configured: defaultTelegramAccount?.configured ?? false,
|
|
tokenSource: defaultTelegramAccount?.tokenSource ?? "none",
|
|
running: defaultTelegramAccount?.running ?? false,
|
|
mode: defaultTelegramAccount?.mode ?? null,
|
|
lastStartAt: defaultTelegramAccount?.lastStartAt ?? null,
|
|
lastStopAt: defaultTelegramAccount?.lastStopAt ?? null,
|
|
lastError: defaultTelegramAccount?.lastError ?? null,
|
|
probe: defaultTelegramAccount?.probe,
|
|
lastProbeAt: defaultTelegramAccount?.lastProbeAt ?? null,
|
|
},
|
|
telegramAccounts,
|
|
telegramDefaultAccountId: defaultTelegramAccountId,
|
|
discord: {
|
|
configured: defaultDiscordAccount?.configured ?? false,
|
|
tokenSource: defaultDiscordAccount?.tokenSource ?? "none",
|
|
running: defaultDiscordAccount?.running ?? false,
|
|
lastStartAt: defaultDiscordAccount?.lastStartAt ?? null,
|
|
lastStopAt: defaultDiscordAccount?.lastStopAt ?? null,
|
|
lastError: defaultDiscordAccount?.lastError ?? null,
|
|
probe: defaultDiscordAccount?.probe,
|
|
lastProbeAt: defaultDiscordAccount?.lastProbeAt ?? null,
|
|
},
|
|
discordAccounts,
|
|
discordDefaultAccountId: defaultDiscordAccountId,
|
|
slack: {
|
|
configured: defaultSlackAccount?.configured ?? false,
|
|
botTokenSource: defaultSlackAccount?.botTokenSource ?? "none",
|
|
appTokenSource: defaultSlackAccount?.appTokenSource ?? "none",
|
|
running: defaultSlackAccount?.running ?? false,
|
|
lastStartAt: defaultSlackAccount?.lastStartAt ?? null,
|
|
lastStopAt: defaultSlackAccount?.lastStopAt ?? null,
|
|
lastError: defaultSlackAccount?.lastError ?? null,
|
|
probe: defaultSlackAccount?.probe,
|
|
lastProbeAt: defaultSlackAccount?.lastProbeAt ?? null,
|
|
},
|
|
slackAccounts,
|
|
slackDefaultAccountId: defaultSlackAccountId,
|
|
signal: {
|
|
configured: defaultSignalAccount?.configured ?? false,
|
|
baseUrl: defaultSignalAccount?.baseUrl ?? null,
|
|
running: defaultSignalAccount?.running ?? false,
|
|
lastStartAt: defaultSignalAccount?.lastStartAt ?? null,
|
|
lastStopAt: defaultSignalAccount?.lastStopAt ?? null,
|
|
lastError: defaultSignalAccount?.lastError ?? null,
|
|
probe: defaultSignalAccount?.probe,
|
|
lastProbeAt: defaultSignalAccount?.lastProbeAt ?? null,
|
|
},
|
|
signalAccounts,
|
|
signalDefaultAccountId: defaultSignalAccountId,
|
|
imessage: {
|
|
configured: defaultIMessageAccount?.configured ?? false,
|
|
running: defaultIMessageAccount?.running ?? false,
|
|
lastStartAt: defaultIMessageAccount?.lastStartAt ?? null,
|
|
lastStopAt: defaultIMessageAccount?.lastStopAt ?? null,
|
|
lastError: defaultIMessageAccount?.lastError ?? null,
|
|
cliPath: defaultIMessageAccount?.cliPath ?? null,
|
|
dbPath: defaultIMessageAccount?.dbPath ?? null,
|
|
probe: defaultIMessageAccount?.probe,
|
|
lastProbeAt: defaultIMessageAccount?.lastProbeAt ?? null,
|
|
},
|
|
imessageAccounts,
|
|
imessageDefaultAccountId: defaultIMessageAccountId,
|
|
},
|
|
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)),
|
|
);
|
|
}
|
|
},
|
|
};
|