refactor!: rename chat providers to channels
This commit is contained in:
322
src/gateway/server-methods/channels.ts
Normal file
322
src/gateway/server-methods/channels.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
|
||||
import {
|
||||
type ChannelId,
|
||||
getChannelPlugin,
|
||||
listChannelPlugins,
|
||||
normalizeChannelId,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import { buildChannelAccountSnapshot } from "../../channels/plugins/status.js";
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelPlugin,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { loadConfig, readConfigFileSnapshot } from "../../config/config.js";
|
||||
import { getChannelActivity } from "../../infra/channel-activity.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
formatValidationErrors,
|
||||
validateChannelsLogoutParams,
|
||||
validateChannelsStatusParams,
|
||||
} from "../protocol/index.js";
|
||||
import { formatForLog } from "../ws-log.js";
|
||||
import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js";
|
||||
|
||||
type ChannelLogoutPayload = {
|
||||
channel: ChannelId;
|
||||
accountId: string;
|
||||
cleared: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export async function logoutChannelAccount(params: {
|
||||
channelId: ChannelId;
|
||||
accountId?: string | null;
|
||||
cfg: ClawdbotConfig;
|
||||
context: GatewayRequestContext;
|
||||
plugin: ChannelPlugin;
|
||||
}): Promise<ChannelLogoutPayload> {
|
||||
const resolvedAccountId =
|
||||
params.accountId?.trim() ||
|
||||
params.plugin.config.defaultAccountId?.(params.cfg) ||
|
||||
params.plugin.config.listAccountIds(params.cfg)[0] ||
|
||||
DEFAULT_ACCOUNT_ID;
|
||||
const account = params.plugin.config.resolveAccount(
|
||||
params.cfg,
|
||||
resolvedAccountId,
|
||||
);
|
||||
await params.context.stopChannel(params.channelId, resolvedAccountId);
|
||||
const result = await params.plugin.gateway?.logoutAccount?.({
|
||||
cfg: params.cfg,
|
||||
accountId: resolvedAccountId,
|
||||
account,
|
||||
runtime: defaultRuntime,
|
||||
});
|
||||
if (!result) {
|
||||
throw new Error(`Channel ${params.channelId} does not support logout`);
|
||||
}
|
||||
const cleared = Boolean(result.cleared);
|
||||
const loggedOut =
|
||||
typeof result.loggedOut === "boolean" ? result.loggedOut : cleared;
|
||||
if (loggedOut) {
|
||||
params.context.markChannelLoggedOut(
|
||||
params.channelId,
|
||||
true,
|
||||
resolvedAccountId,
|
||||
);
|
||||
}
|
||||
return {
|
||||
channel: params.channelId,
|
||||
accountId: resolvedAccountId,
|
||||
...result,
|
||||
cleared,
|
||||
};
|
||||
}
|
||||
|
||||
export const channelsHandlers: GatewayRequestHandlers = {
|
||||
"channels.status": async ({ params, respond, context }) => {
|
||||
if (!validateChannelsStatusParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid channels.status params: ${formatValidationErrors(validateChannelsStatusParams.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 plugins = listChannelPlugins();
|
||||
const pluginMap = new Map<ChannelId, ChannelPlugin>(
|
||||
plugins.map((plugin) => [plugin.id, plugin]),
|
||||
);
|
||||
|
||||
const resolveRuntimeSnapshot = (
|
||||
channelId: ChannelId,
|
||||
accountId: string,
|
||||
defaultAccountId: string,
|
||||
): ChannelAccountSnapshot | undefined => {
|
||||
const accounts = runtime.channelAccounts[channelId];
|
||||
const defaultRuntime = runtime.channels[channelId];
|
||||
const raw =
|
||||
accounts?.[accountId] ??
|
||||
(accountId === defaultAccountId ? defaultRuntime : undefined);
|
||||
if (!raw) return undefined;
|
||||
return raw;
|
||||
};
|
||||
|
||||
const isAccountEnabled = (plugin: ChannelPlugin, account: unknown) =>
|
||||
plugin.config.isEnabled
|
||||
? plugin.config.isEnabled(account, cfg)
|
||||
: !account ||
|
||||
typeof account !== "object" ||
|
||||
(account as { enabled?: boolean }).enabled !== false;
|
||||
|
||||
const buildChannelAccounts = async (channelId: ChannelId) => {
|
||||
const plugin = pluginMap.get(channelId);
|
||||
if (!plugin) {
|
||||
return {
|
||||
accounts: [] as ChannelAccountSnapshot[],
|
||||
defaultAccountId: DEFAULT_ACCOUNT_ID,
|
||||
defaultAccount: undefined as ChannelAccountSnapshot | undefined,
|
||||
resolvedAccounts: {} as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
||||
plugin,
|
||||
cfg,
|
||||
accountIds,
|
||||
});
|
||||
const accounts: ChannelAccountSnapshot[] = [];
|
||||
const resolvedAccounts: Record<string, unknown> = {};
|
||||
for (const accountId of accountIds) {
|
||||
const account = plugin.config.resolveAccount(cfg, accountId);
|
||||
const enabled = isAccountEnabled(plugin, account);
|
||||
resolvedAccounts[accountId] = account;
|
||||
let probeResult: unknown;
|
||||
let lastProbeAt: number | null = null;
|
||||
if (probe && enabled && plugin.status?.probeAccount) {
|
||||
let configured = true;
|
||||
if (plugin.config.isConfigured) {
|
||||
configured = await plugin.config.isConfigured(account, cfg);
|
||||
}
|
||||
if (configured) {
|
||||
probeResult = await plugin.status.probeAccount({
|
||||
account,
|
||||
timeoutMs,
|
||||
cfg,
|
||||
});
|
||||
lastProbeAt = Date.now();
|
||||
}
|
||||
}
|
||||
let auditResult: unknown;
|
||||
if (probe && enabled && plugin.status?.auditAccount) {
|
||||
let configured = true;
|
||||
if (plugin.config.isConfigured) {
|
||||
configured = await plugin.config.isConfigured(account, cfg);
|
||||
}
|
||||
if (configured) {
|
||||
auditResult = await plugin.status.auditAccount({
|
||||
account,
|
||||
timeoutMs,
|
||||
cfg,
|
||||
probe: probeResult,
|
||||
});
|
||||
}
|
||||
}
|
||||
const runtimeSnapshot = resolveRuntimeSnapshot(
|
||||
channelId,
|
||||
accountId,
|
||||
defaultAccountId,
|
||||
);
|
||||
const snapshot = await buildChannelAccountSnapshot({
|
||||
plugin,
|
||||
cfg,
|
||||
accountId,
|
||||
runtime: runtimeSnapshot,
|
||||
probe: probeResult,
|
||||
audit: auditResult,
|
||||
});
|
||||
if (lastProbeAt) snapshot.lastProbeAt = lastProbeAt;
|
||||
const activity = getChannelActivity({
|
||||
channel: channelId as never,
|
||||
accountId,
|
||||
});
|
||||
if (snapshot.lastInboundAt == null) {
|
||||
snapshot.lastInboundAt = activity.inboundAt;
|
||||
}
|
||||
if (snapshot.lastOutboundAt == null) {
|
||||
snapshot.lastOutboundAt = activity.outboundAt;
|
||||
}
|
||||
accounts.push(snapshot);
|
||||
}
|
||||
const defaultAccount =
|
||||
accounts.find((entry) => entry.accountId === defaultAccountId) ??
|
||||
accounts[0];
|
||||
return { accounts, defaultAccountId, defaultAccount, resolvedAccounts };
|
||||
};
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
ts: Date.now(),
|
||||
channelOrder: plugins.map((plugin) => plugin.id),
|
||||
channelLabels: Object.fromEntries(
|
||||
plugins.map((plugin) => [plugin.id, plugin.meta.label]),
|
||||
),
|
||||
channels: {} as Record<string, unknown>,
|
||||
channelAccounts: {} as Record<string, unknown>,
|
||||
channelDefaultAccountId: {} as Record<string, unknown>,
|
||||
};
|
||||
const channelsMap = payload.channels as Record<string, unknown>;
|
||||
const accountsMap = payload.channelAccounts as Record<string, unknown>;
|
||||
const defaultAccountIdMap = payload.channelDefaultAccountId as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
for (const plugin of plugins) {
|
||||
const { accounts, defaultAccountId, defaultAccount, resolvedAccounts } =
|
||||
await buildChannelAccounts(plugin.id);
|
||||
const fallbackAccount =
|
||||
resolvedAccounts[defaultAccountId] ??
|
||||
plugin.config.resolveAccount(cfg, defaultAccountId);
|
||||
const summary = plugin.status?.buildChannelSummary
|
||||
? await plugin.status.buildChannelSummary({
|
||||
account: fallbackAccount,
|
||||
cfg,
|
||||
defaultAccountId,
|
||||
snapshot:
|
||||
defaultAccount ??
|
||||
({
|
||||
accountId: defaultAccountId,
|
||||
} as ChannelAccountSnapshot),
|
||||
})
|
||||
: {
|
||||
configured: defaultAccount?.configured ?? false,
|
||||
};
|
||||
channelsMap[plugin.id] = summary;
|
||||
accountsMap[plugin.id] = accounts;
|
||||
defaultAccountIdMap[plugin.id] = defaultAccountId;
|
||||
}
|
||||
|
||||
respond(true, payload, undefined);
|
||||
},
|
||||
"channels.logout": async ({ params, respond, context }) => {
|
||||
if (!validateChannelsLogoutParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid channels.logout params: ${formatValidationErrors(validateChannelsLogoutParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const rawChannel = (params as { channel?: unknown }).channel;
|
||||
const channelId =
|
||||
typeof rawChannel === "string" ? normalizeChannelId(rawChannel) : null;
|
||||
if (!channelId) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
"invalid channels.logout channel",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const plugin = getChannelPlugin(channelId);
|
||||
if (!plugin?.gateway?.logoutAccount) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`channel ${channelId} does not support logout`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const accountIdRaw = (params as { accountId?: unknown }).accountId;
|
||||
const accountId =
|
||||
typeof accountIdRaw === "string" ? accountIdRaw.trim() : undefined;
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
if (!snapshot.valid) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
"config invalid; fix it before logging out",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = await logoutChannelAccount({
|
||||
channelId,
|
||||
accountId,
|
||||
cfg: snapshot.config ?? {},
|
||||
context,
|
||||
plugin,
|
||||
});
|
||||
respond(true, payload, undefined);
|
||||
} catch (err) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user