openclaw/src/commands/status-all/providers.ts
2026-01-11 01:05:06 +01:00

325 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string | undefined>): {
label: string;
parts: string[];
} {
const counts = new Map<string, number>();
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<Record<string, string>>;
}>;
}> {
const rows: ProviderRow[] = [];
const details: Array<{
title: string;
columns: string[];
rows: Array<Record<string, string>>;
}> = [];
// 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,
};
}