UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

275 lines (243 loc) 8.66 kB
/** * Twitch channel plugin for OpenClaw. * * Main plugin export combining all adapters (outbound, actions, status, gateway). * This is the primary entry point for the Twitch channel integration. */ import type { OpenClawConfig } from "openclaw/plugin-sdk"; import { buildChannelConfigSchema } from "openclaw/plugin-sdk"; import { twitchMessageActions } from "./actions.js"; import { removeClientManager } from "./client-manager-registry.js"; import { TwitchConfigSchema } from "./config-schema.js"; import { DEFAULT_ACCOUNT_ID, getAccountConfig, listAccountIds } from "./config.js"; import { twitchOnboardingAdapter } from "./onboarding.js"; import { twitchOutbound } from "./outbound.js"; import { probeTwitch } from "./probe.js"; import { resolveTwitchTargets } from "./resolver.js"; import { collectTwitchStatusIssues } from "./status.js"; import { resolveTwitchToken } from "./token.js"; import type { ChannelAccountSnapshot, ChannelCapabilities, ChannelLogSink, ChannelMeta, ChannelPlugin, ChannelResolveKind, ChannelResolveResult, TwitchAccountConfig, } from "./types.js"; import { isAccountConfigured } from "./utils/twitch.js"; /** * Twitch channel plugin. * * Implements the ChannelPlugin interface to provide Twitch chat integration * for OpenClaw. Supports message sending, receiving, access control, and * status monitoring. */ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = { /** Plugin identifier */ id: "twitch", /** Plugin metadata */ meta: { id: "twitch", label: "Twitch", selectionLabel: "Twitch (Chat)", docsPath: "/channels/twitch", blurb: "Twitch chat integration", aliases: ["twitch-chat"], } satisfies ChannelMeta, /** Onboarding adapter */ onboarding: twitchOnboardingAdapter, /** Pairing configuration */ pairing: { idLabel: "twitchUserId", normalizeAllowEntry: (entry) => entry.replace(/^(twitch:)?user:?/i, ""), notifyApproval: async ({ id }) => { // Note: Twitch doesn't support DMs from bots, so pairing approval is limited // We'll log the approval instead console.warn(`Pairing approved for user ${id} (notification sent via chat if possible)`); }, }, /** Supported chat capabilities */ capabilities: { chatTypes: ["group"], } satisfies ChannelCapabilities, /** Configuration schema for Twitch channel */ configSchema: buildChannelConfigSchema(TwitchConfigSchema), /** Account configuration management */ config: { /** List all configured account IDs */ listAccountIds: (cfg: OpenClawConfig): string[] => listAccountIds(cfg), /** Resolve an account config by ID */ resolveAccount: (cfg: OpenClawConfig, accountId?: string | null): TwitchAccountConfig => { const account = getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID); if (!account) { // Return a default/empty account if not configured return { username: "", accessToken: "", clientId: "", enabled: false, } as TwitchAccountConfig; } return account; }, /** Get the default account ID */ defaultAccountId: (): string => DEFAULT_ACCOUNT_ID, /** Check if an account is configured */ isConfigured: (_account: unknown, cfg: OpenClawConfig): boolean => { const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID); const tokenResolution = resolveTwitchToken(cfg, { accountId: DEFAULT_ACCOUNT_ID }); return account ? isAccountConfigured(account, tokenResolution.token) : false; }, /** Check if an account is enabled */ isEnabled: (account: TwitchAccountConfig | undefined): boolean => account?.enabled !== false, /** Describe account status */ describeAccount: (account: TwitchAccountConfig | undefined) => { return { accountId: DEFAULT_ACCOUNT_ID, enabled: account?.enabled !== false, configured: account ? isAccountConfigured(account, account?.accessToken) : false, }; }, }, /** Outbound message adapter */ outbound: twitchOutbound, /** Message actions adapter */ actions: twitchMessageActions, /** Resolver adapter for username -> user ID resolution */ resolver: { resolveTargets: async ({ cfg, accountId, inputs, kind, runtime, }: { cfg: OpenClawConfig; accountId?: string | null; inputs: string[]; kind: ChannelResolveKind; runtime: import("../../../src/runtime.js").RuntimeEnv; }): Promise<ChannelResolveResult[]> => { const account = getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID); if (!account) { return inputs.map((input) => ({ input, resolved: false, note: "account not configured", })); } // Adapt RuntimeEnv.log to ChannelLogSink const log: ChannelLogSink = { info: (msg) => runtime.log(msg), warn: (msg) => runtime.log(msg), error: (msg) => runtime.error(msg), debug: (msg) => runtime.log(msg), }; return await resolveTwitchTargets(inputs, account, kind, log); }, }, /** Status monitoring adapter */ status: { /** Default runtime state */ defaultRuntime: { accountId: DEFAULT_ACCOUNT_ID, running: false, lastStartAt: null, lastStopAt: null, lastError: null, }, /** Build channel summary from snapshot */ buildChannelSummary: ({ snapshot }: { snapshot: ChannelAccountSnapshot }) => ({ configured: snapshot.configured ?? false, running: snapshot.running ?? false, lastStartAt: snapshot.lastStartAt ?? null, lastStopAt: snapshot.lastStopAt ?? null, lastError: snapshot.lastError ?? null, probe: snapshot.probe, lastProbeAt: snapshot.lastProbeAt ?? null, }), /** Probe account connection */ probeAccount: async ({ account, timeoutMs, }: { account: TwitchAccountConfig; timeoutMs: number; }): Promise<unknown> => { return await probeTwitch(account, timeoutMs); }, /** Build account snapshot with current status */ buildAccountSnapshot: ({ account, cfg, runtime, probe, }: { account: TwitchAccountConfig; cfg: OpenClawConfig; runtime?: ChannelAccountSnapshot; probe?: unknown; }): ChannelAccountSnapshot => { const twitch = (cfg as Record<string, unknown>).channels as | Record<string, unknown> | undefined; const twitchCfg = twitch?.twitch as Record<string, unknown> | undefined; const accountMap = (twitchCfg?.accounts as Record<string, unknown> | undefined) ?? {}; const resolvedAccountId = Object.entries(accountMap).find(([, value]) => value === account)?.[0] ?? DEFAULT_ACCOUNT_ID; const tokenResolution = resolveTwitchToken(cfg, { accountId: resolvedAccountId }); return { accountId: resolvedAccountId, enabled: account?.enabled !== false, configured: isAccountConfigured(account, tokenResolution.token), running: runtime?.running ?? false, lastStartAt: runtime?.lastStartAt ?? null, lastStopAt: runtime?.lastStopAt ?? null, lastError: runtime?.lastError ?? null, probe, }; }, /** Collect status issues for all accounts */ collectStatusIssues: collectTwitchStatusIssues, }, /** Gateway adapter for connection lifecycle */ gateway: { /** Start an account connection */ startAccount: async (ctx): Promise<void> => { const account = ctx.account; const accountId = ctx.accountId; ctx.setStatus?.({ accountId, running: true, lastStartAt: Date.now(), lastError: null, }); ctx.log?.info(`Starting Twitch connection for ${account.username}`); // Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles. const { monitorTwitchProvider } = await import("./monitor.js"); await monitorTwitchProvider({ account, accountId, config: ctx.cfg, runtime: ctx.runtime, abortSignal: ctx.abortSignal, }); }, /** Stop an account connection */ stopAccount: async (ctx): Promise<void> => { const account = ctx.account; const accountId = ctx.accountId; // Disconnect and remove client manager from registry await removeClientManager(accountId); ctx.setStatus?.({ accountId, running: false, lastStopAt: Date.now(), }); ctx.log?.info(`Stopped Twitch connection for ${account.username}`); }, }, };