import fs from "node:fs"; import type { ClawdbotConfig } from "../../config/config.js"; import { listDiscordAccountIds, resolveDiscordAccount, } from "../../discord/accounts.js"; import { listIMessageAccountIds, resolveIMessageAccount, } from "../../imessage/accounts.js"; import { resolveMSTeamsCredentials } from "../../msteams/token.js"; import { listSignalAccountIds, resolveSignalAccount, } from "../../signal/accounts.js"; import { listSlackAccountIds, resolveSlackAccount, } from "../../slack/accounts.js"; import { listTelegramAccountIds, resolveTelegramAccount, } from "../../telegram/accounts.js"; import { normalizeE164 } from "../../utils.js"; import { listWhatsAppAccountIds, resolveWhatsAppAccount, } from "../../web/accounts.js"; import { getWebAuthAgeMs, readWebSelfId, webAuthExists, } from "../../web/session.js"; import { formatAge } from "./format.js"; export type ProviderRow = { provider: string; enabled: boolean; state: "ok" | "setup" | "warn" | "off"; detail: string; }; function summarizeSources(sources: Array): { label: string; parts: string[]; } { const counts = new Map(); for (const s of sources) { const key = s?.trim() ? s.trim() : "unknown"; counts.set(key, (counts.get(key) ?? 0) + 1); } const parts = [...counts.entries()] .sort((a, b) => b[1] - a[1]) .map(([key, n]) => `${key}${n > 1 ? `×${n}` : ""}`); const label = parts.length > 0 ? parts.join("+") : "unknown"; return { label, parts }; } function existsSyncMaybe(p: string | undefined): boolean | null { const path = p?.trim() || ""; if (!path) return null; try { return fs.existsSync(path); } catch { return null; } } export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{ rows: ProviderRow[]; details: Array<{ title: string; columns: string[]; rows: Array>; }>; }> { const rows: ProviderRow[] = []; const details: Array<{ title: string; columns: string[]; rows: Array>; }> = []; // WhatsApp const waEnabled = cfg.web?.enabled !== false; const waLinked = waEnabled ? await webAuthExists().catch(() => false) : false; const waAuthAgeMs = waLinked ? getWebAuthAgeMs() : null; const waSelf = waLinked ? readWebSelfId().e164 : undefined; const waAccounts = waLinked ? listWhatsAppAccountIds(cfg).map((accountId) => resolveWhatsAppAccount({ cfg, accountId }), ) : []; rows.push({ provider: "WhatsApp", enabled: waEnabled, state: !waEnabled ? "off" : waLinked ? "ok" : "setup", detail: waEnabled ? waLinked ? `linked${waSelf ? ` ${waSelf}` : ""}${waAuthAgeMs ? ` · auth ${formatAge(waAuthAgeMs)}` : ""} · accounts ${waAccounts.length || 1}` : "not linked (run clawdbot login)" : "disabled", }); if (waLinked) { const waRows = waAccounts.length > 0 ? waAccounts : [resolveWhatsAppAccount({ cfg })]; details.push({ title: "WhatsApp accounts", columns: ["Account", "Status", "Notes"], rows: waRows.map((account) => { const allowFrom = (account.allowFrom ?? cfg.whatsapp?.allowFrom ?? []) .map(normalizeE164) .filter(Boolean) .slice(0, 3); const dmPolicy = account.dmPolicy ?? cfg.whatsapp?.dmPolicy ?? "pairing"; const notes: string[] = []; if (!account.enabled) notes.push("disabled"); if (account.selfChatMode) notes.push("self-chat"); notes.push(`dm:${dmPolicy}`); if (allowFrom.length) notes.push(`allow:${allowFrom.join(",")}`); return { Account: account.name?.trim() ? `${account.accountId} (${account.name.trim()})` : account.accountId, Status: account.enabled ? "OK" : "OFF", Notes: notes.join(" · "), }; }), }); } // Telegram const tgEnabled = cfg.telegram?.enabled !== false; const tgAccounts = listTelegramAccountIds(cfg).map((accountId) => resolveTelegramAccount({ cfg, accountId }), ); const tgEnabledAccounts = tgAccounts.filter((a) => a.enabled); const tgTokenAccounts = tgEnabledAccounts.filter((a) => a.token?.trim()); const tgSources = summarizeSources(tgTokenAccounts.map((a) => a.tokenSource)); const tgMissingFiles: string[] = []; const tgGlobalTokenFileExists = existsSyncMaybe(cfg.telegram?.tokenFile); if ( tgEnabled && cfg.telegram?.tokenFile?.trim() && tgGlobalTokenFileExists === false ) { tgMissingFiles.push("telegram.tokenFile"); } for (const accountId of listTelegramAccountIds(cfg)) { const tokenFile = cfg.telegram?.accounts?.[accountId]?.tokenFile?.trim() || ""; const ok = existsSyncMaybe(tokenFile); if (tgEnabled && tokenFile && ok === false) { tgMissingFiles.push(`telegram.accounts.${accountId}.tokenFile`); } } const tgMisconfigured = tgMissingFiles.length > 0; rows.push({ provider: "Telegram", enabled: tgEnabled, state: !tgEnabled ? "off" : tgMisconfigured ? "warn" : tgTokenAccounts.length > 0 ? "ok" : "setup", detail: tgEnabled ? tgMisconfigured ? `token file missing (${tgMissingFiles[0]})` : tgTokenAccounts.length > 0 ? `bot token ${tgSources.label} · accounts ${tgTokenAccounts.length}/${tgEnabledAccounts.length || 1}` : "no bot token (TELEGRAM_BOT_TOKEN / telegram.botToken)" : "disabled", }); // Discord const dcEnabled = cfg.discord?.enabled !== false; const dcAccounts = listDiscordAccountIds(cfg).map((accountId) => resolveDiscordAccount({ cfg, accountId }), ); const dcEnabledAccounts = dcAccounts.filter((a) => a.enabled); const dcTokenAccounts = dcEnabledAccounts.filter((a) => a.token?.trim()); const dcSources = summarizeSources(dcTokenAccounts.map((a) => a.tokenSource)); rows.push({ provider: "Discord", enabled: dcEnabled, state: !dcEnabled ? "off" : dcTokenAccounts.length > 0 ? "ok" : "setup", detail: dcEnabled ? dcTokenAccounts.length > 0 ? `bot token ${dcSources.label} · accounts ${dcTokenAccounts.length}/${dcEnabledAccounts.length || 1}` : "no bot token (DISCORD_BOT_TOKEN / discord.token)" : "disabled", }); // Slack const slEnabled = cfg.slack?.enabled !== false; const slAccounts = listSlackAccountIds(cfg).map((accountId) => resolveSlackAccount({ cfg, accountId }), ); const slEnabledAccounts = slAccounts.filter((a) => a.enabled); const slReady = slEnabledAccounts.filter( (a) => Boolean(a.botToken?.trim()) && Boolean(a.appToken?.trim()), ); const slPartial = slEnabledAccounts.filter( (a) => (a.botToken?.trim() && !a.appToken?.trim()) || (!a.botToken?.trim() && a.appToken?.trim()), ); const slHasAnyToken = slEnabledAccounts.some( (a) => Boolean(a.botToken?.trim()) || Boolean(a.appToken?.trim()), ); const slBotSources = summarizeSources( slReady.map((a) => a.botTokenSource ?? "none"), ); const slAppSources = summarizeSources( slReady.map((a) => a.appTokenSource ?? "none"), ); rows.push({ provider: "Slack", enabled: slEnabled, state: !slEnabled ? "off" : slPartial.length > 0 ? "warn" : slReady.length > 0 ? "ok" : "setup", detail: slEnabled ? slPartial.length > 0 ? `partial tokens (need bot+app) · accounts ${slPartial.length}` : slReady.length > 0 ? `tokens ok (bot ${slBotSources.label}, app ${slAppSources.label}) · accounts ${slReady.length}/${slEnabledAccounts.length || 1}` : slHasAnyToken ? "tokens incomplete (need bot+app)" : "no tokens (SLACK_BOT_TOKEN + SLACK_APP_TOKEN)" : "disabled", }); // Signal const siEnabled = cfg.signal?.enabled !== false; const siAccounts = listSignalAccountIds(cfg).map((accountId) => resolveSignalAccount({ cfg, accountId }), ); const siEnabledAccounts = siAccounts.filter((a) => a.enabled); const siConfiguredAccounts = siEnabledAccounts.filter((a) => a.configured); rows.push({ provider: "Signal", enabled: siEnabled, state: !siEnabled ? "off" : siConfiguredAccounts.length > 0 ? "ok" : "setup", detail: siEnabled ? siConfiguredAccounts.length > 0 ? `configured · accounts ${siConfiguredAccounts.length}/${siEnabledAccounts.length || 1}` : "default config (no overrides)" : "disabled", }); // iMessage const imEnabled = cfg.imessage?.enabled !== false; const imAccounts = listIMessageAccountIds(cfg).map((accountId) => resolveIMessageAccount({ cfg, accountId }), ); const imEnabledAccounts = imAccounts.filter((a) => a.enabled); const imConfiguredAccounts = imEnabledAccounts.filter((a) => a.configured); rows.push({ provider: "iMessage", enabled: imEnabled, state: !imEnabled ? "off" : imConfiguredAccounts.length > 0 ? "ok" : "setup", detail: imEnabled ? imConfiguredAccounts.length > 0 ? `configured · accounts ${imConfiguredAccounts.length}/${imEnabledAccounts.length || 1}` : "default config (no overrides)" : "disabled", }); // MS Teams const msEnabled = cfg.msteams?.enabled !== false; const msCreds = resolveMSTeamsCredentials(cfg.msteams); const msAppId = cfg.msteams?.appId?.trim() || process.env.MSTEAMS_APP_ID?.trim(); const msAppPassword = cfg.msteams?.appPassword?.trim() || process.env.MSTEAMS_APP_PASSWORD?.trim(); const msTenantId = cfg.msteams?.tenantId?.trim() || process.env.MSTEAMS_TENANT_ID?.trim(); const msMissing = [ !msAppId ? "appId" : null, !msAppPassword ? "appPassword" : null, !msTenantId ? "tenantId" : null, ].filter(Boolean) as string[]; const msAnyPresent = Boolean(msAppId || msAppPassword || msTenantId); rows.push({ provider: "MS Teams", enabled: msEnabled, state: !msEnabled ? "off" : msCreds ? "ok" : msAnyPresent ? "warn" : "setup", detail: msEnabled ? msCreds ? "credentials set" : msAnyPresent ? `credentials incomplete (missing ${msMissing.join(", ")})` : "no credentials (MSTEAMS_APP_ID / _PASSWORD / _TENANT_ID)" : "disabled", }); return { rows, details, }; }