Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
524
extensions/twitch/src/setup-surface.ts
Normal file
524
extensions/twitch/src/setup-surface.ts
Normal file
@@ -0,0 +1,524 @@
|
||||
/**
|
||||
* Twitch setup wizard surface for CLI setup.
|
||||
*/
|
||||
|
||||
import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import { getChatChannelMeta, type ChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
formatDocsLink,
|
||||
type ChannelSetupAdapter,
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
type OpenClawConfig,
|
||||
type WizardPrompter,
|
||||
normalizeAccountId,
|
||||
createSetupTranslator,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
getAccountConfig,
|
||||
listAccountIds,
|
||||
resolveDefaultTwitchAccountId,
|
||||
resolveTwitchAccountContext,
|
||||
} from "./config.js";
|
||||
import type { TwitchAccountConfig, TwitchRole } from "./types.js";
|
||||
import { isAccountConfigured } from "./utils/twitch.js";
|
||||
|
||||
const channel = "twitch" as const;
|
||||
const t = createSetupTranslator();
|
||||
const INVALID_ACCOUNT_ID_MESSAGE = "Invalid Twitch account id";
|
||||
|
||||
function normalizeRequestedSetupAccountId(accountId: string): string {
|
||||
const normalized = normalizeOptionalAccountId(accountId);
|
||||
if (!normalized) {
|
||||
throw new Error(INVALID_ACCOUNT_ID_MESSAGE);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function resolveSetupAccountId(cfg: OpenClawConfig, requestedAccountId?: string): string {
|
||||
const requested = requestedAccountId?.trim();
|
||||
if (requested) {
|
||||
return normalizeRequestedSetupAccountId(requested);
|
||||
}
|
||||
|
||||
const preferred = cfg.channels?.twitch?.defaultAccount?.trim();
|
||||
return preferred ? normalizeAccountId(preferred) : resolveDefaultTwitchAccountId(cfg);
|
||||
}
|
||||
|
||||
export function setTwitchAccount(
|
||||
cfg: OpenClawConfig,
|
||||
account: Partial<TwitchAccountConfig>,
|
||||
accountId: string = resolveSetupAccountId(cfg),
|
||||
): OpenClawConfig {
|
||||
const resolvedAccountId = accountId.trim()
|
||||
? normalizeRequestedSetupAccountId(accountId)
|
||||
: resolveSetupAccountId(cfg);
|
||||
const existing = getAccountConfig(cfg, resolvedAccountId);
|
||||
const merged: TwitchAccountConfig = {
|
||||
username: account.username ?? existing?.username ?? "",
|
||||
accessToken: account.accessToken ?? existing?.accessToken ?? "",
|
||||
clientId: account.clientId ?? existing?.clientId ?? "",
|
||||
channel: account.channel ?? existing?.channel ?? "",
|
||||
enabled: account.enabled ?? existing?.enabled ?? true,
|
||||
allowFrom: account.allowFrom ?? existing?.allowFrom,
|
||||
allowedRoles: account.allowedRoles ?? existing?.allowedRoles,
|
||||
requireMention: account.requireMention ?? existing?.requireMention,
|
||||
clientSecret: account.clientSecret ?? existing?.clientSecret,
|
||||
refreshToken: account.refreshToken ?? existing?.refreshToken,
|
||||
expiresIn: account.expiresIn ?? existing?.expiresIn,
|
||||
obtainmentTimestamp: account.obtainmentTimestamp ?? existing?.obtainmentTimestamp,
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
twitch: {
|
||||
...((cfg.channels as Record<string, unknown>)?.twitch as
|
||||
| Record<string, unknown>
|
||||
| undefined),
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...((
|
||||
(cfg.channels as Record<string, unknown>)?.twitch as Record<string, unknown> | undefined
|
||||
)?.accounts as Record<string, unknown> | undefined),
|
||||
[resolvedAccountId]: merged,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function noteTwitchSetupHelp(prompter: WizardPrompter): Promise<void> {
|
||||
await prompter.note(
|
||||
[
|
||||
t("wizard.twitch.helpRequiresBot"),
|
||||
t("wizard.twitch.helpCreateApp"),
|
||||
t("wizard.twitch.helpGenerateToken"),
|
||||
t("wizard.twitch.helpTokenTools"),
|
||||
t("wizard.twitch.helpCopyToken"),
|
||||
t("wizard.twitch.helpEnvVars"),
|
||||
`Docs: ${formatDocsLink("/channels/twitch", "channels/twitch")}`,
|
||||
].join("\n"),
|
||||
t("wizard.twitch.setupTitle"),
|
||||
);
|
||||
}
|
||||
|
||||
export async function promptToken(
|
||||
prompter: WizardPrompter,
|
||||
account: TwitchAccountConfig | null,
|
||||
envToken: string | undefined,
|
||||
): Promise<string> {
|
||||
const existingToken = account?.accessToken ?? "";
|
||||
|
||||
if (existingToken && !envToken) {
|
||||
const keepToken = await prompter.confirm({
|
||||
message: t("wizard.twitch.accessTokenKeep"),
|
||||
initialValue: true,
|
||||
});
|
||||
if (keepToken) {
|
||||
return existingToken;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
await prompter.text({
|
||||
message: t("wizard.twitch.oauthTokenPrompt"),
|
||||
initialValue: envToken ?? "",
|
||||
validate: (value) => {
|
||||
const raw = value?.trim() ?? "";
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
if (!raw.startsWith("oauth:")) {
|
||||
return "Token should start with 'oauth:'";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
})
|
||||
).trim();
|
||||
}
|
||||
|
||||
export async function promptUsername(
|
||||
prompter: WizardPrompter,
|
||||
account: TwitchAccountConfig | null,
|
||||
): Promise<string> {
|
||||
return (
|
||||
await prompter.text({
|
||||
message: t("wizard.twitch.botUsernamePrompt"),
|
||||
initialValue: account?.username ?? "",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
})
|
||||
).trim();
|
||||
}
|
||||
|
||||
export async function promptClientId(
|
||||
prompter: WizardPrompter,
|
||||
account: TwitchAccountConfig | null,
|
||||
): Promise<string> {
|
||||
return (
|
||||
await prompter.text({
|
||||
message: t("wizard.twitch.clientIdPrompt"),
|
||||
initialValue: account?.clientId ?? "",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
})
|
||||
).trim();
|
||||
}
|
||||
|
||||
export async function promptChannelName(
|
||||
prompter: WizardPrompter,
|
||||
account: TwitchAccountConfig | null,
|
||||
): Promise<string> {
|
||||
return (
|
||||
await prompter.text({
|
||||
message: t("wizard.twitch.channelJoinPrompt"),
|
||||
initialValue: account?.channel ?? "",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
})
|
||||
).trim();
|
||||
}
|
||||
|
||||
export async function promptRefreshTokenSetup(
|
||||
prompter: WizardPrompter,
|
||||
account: TwitchAccountConfig | null,
|
||||
): Promise<{ clientSecret?: string; refreshToken?: string }> {
|
||||
const useRefresh = await prompter.confirm({
|
||||
message: t("wizard.twitch.refreshTokenPrompt"),
|
||||
initialValue: Boolean(account?.clientSecret && account?.refreshToken),
|
||||
});
|
||||
|
||||
if (!useRefresh) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const clientSecret =
|
||||
(
|
||||
await prompter.text({
|
||||
message: t("wizard.twitch.clientSecretPrompt"),
|
||||
initialValue: account?.clientSecret ?? "",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
})
|
||||
).trim() || undefined;
|
||||
|
||||
const refreshToken =
|
||||
(
|
||||
await prompter.text({
|
||||
message: t("wizard.twitch.refreshTokenInputPrompt"),
|
||||
initialValue: account?.refreshToken ?? "",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
})
|
||||
).trim() || undefined;
|
||||
|
||||
return { clientSecret, refreshToken };
|
||||
}
|
||||
|
||||
export async function configureWithEnvToken(
|
||||
cfg: OpenClawConfig,
|
||||
prompter: WizardPrompter,
|
||||
account: TwitchAccountConfig | null,
|
||||
envToken: string,
|
||||
forceAllowFrom: boolean,
|
||||
dmPolicy: ChannelSetupDmPolicy,
|
||||
accountId: string = resolveSetupAccountId(cfg),
|
||||
): Promise<{ cfg: OpenClawConfig } | null> {
|
||||
const resolvedAccountId = accountId.trim()
|
||||
? normalizeRequestedSetupAccountId(accountId)
|
||||
: resolveSetupAccountId(cfg);
|
||||
if (resolvedAccountId !== DEFAULT_ACCOUNT_ID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const useEnv = await prompter.confirm({
|
||||
message: t("wizard.twitch.envPrompt"),
|
||||
initialValue: true,
|
||||
});
|
||||
if (!useEnv) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const username = await promptUsername(prompter, account);
|
||||
const clientId = await promptClientId(prompter, account);
|
||||
|
||||
const cfgWithAccount = setTwitchAccount(
|
||||
cfg,
|
||||
{
|
||||
username,
|
||||
clientId,
|
||||
accessToken: envToken,
|
||||
enabled: true,
|
||||
},
|
||||
resolvedAccountId,
|
||||
);
|
||||
|
||||
if (forceAllowFrom && dmPolicy.promptAllowFrom) {
|
||||
return {
|
||||
cfg: await dmPolicy.promptAllowFrom({
|
||||
cfg: cfgWithAccount,
|
||||
prompter,
|
||||
accountId: resolvedAccountId,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return { cfg: cfgWithAccount };
|
||||
}
|
||||
|
||||
function setTwitchAccessControl(
|
||||
cfg: OpenClawConfig,
|
||||
allowedRoles: TwitchRole[],
|
||||
requireMention: boolean,
|
||||
accountId?: string,
|
||||
): OpenClawConfig {
|
||||
const resolvedAccountId = resolveSetupAccountId(cfg, accountId);
|
||||
const account = getAccountConfig(cfg, resolvedAccountId);
|
||||
if (!account) {
|
||||
return cfg;
|
||||
}
|
||||
|
||||
return setTwitchAccount(
|
||||
cfg,
|
||||
{
|
||||
...account,
|
||||
allowedRoles,
|
||||
requireMention,
|
||||
},
|
||||
resolvedAccountId,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveTwitchGroupPolicy(
|
||||
cfg: OpenClawConfig,
|
||||
accountId?: string,
|
||||
): "open" | "allowlist" | "disabled" {
|
||||
const account = getAccountConfig(cfg, resolveSetupAccountId(cfg, accountId));
|
||||
if (account?.allowedRoles?.includes("all")) {
|
||||
return "open";
|
||||
}
|
||||
if (account?.allowedRoles?.includes("moderator")) {
|
||||
return "allowlist";
|
||||
}
|
||||
return "disabled";
|
||||
}
|
||||
|
||||
function setTwitchGroupPolicy(
|
||||
cfg: OpenClawConfig,
|
||||
policy: "open" | "allowlist" | "disabled",
|
||||
accountId?: string,
|
||||
): OpenClawConfig {
|
||||
const allowedRoles: TwitchRole[] =
|
||||
policy === "open" ? ["all"] : policy === "allowlist" ? ["moderator", "vip"] : [];
|
||||
return setTwitchAccessControl(cfg, allowedRoles, true, accountId);
|
||||
}
|
||||
|
||||
const twitchDmPolicy: ChannelSetupDmPolicy = {
|
||||
label: "Twitch",
|
||||
channel,
|
||||
policyKey: "channels.twitch.accounts.default.allowedRoles",
|
||||
allowFromKey: "channels.twitch.accounts.default.allowFrom",
|
||||
resolveConfigKeys: (cfg, accountId) => {
|
||||
const resolvedAccountId = resolveSetupAccountId(cfg, accountId);
|
||||
return {
|
||||
policyKey: `channels.twitch.accounts.${resolvedAccountId}.allowedRoles`,
|
||||
allowFromKey: `channels.twitch.accounts.${resolvedAccountId}.allowFrom`,
|
||||
};
|
||||
},
|
||||
getCurrent: (cfg, accountId) => {
|
||||
const account = getAccountConfig(cfg, resolveSetupAccountId(cfg, accountId));
|
||||
if (account?.allowedRoles?.includes("all")) {
|
||||
return "open";
|
||||
}
|
||||
if (account?.allowFrom && account.allowFrom.length > 0) {
|
||||
return "allowlist";
|
||||
}
|
||||
return "disabled";
|
||||
},
|
||||
setPolicy: (cfg, policy, accountId) => {
|
||||
const allowedRoles: TwitchRole[] =
|
||||
policy === "open" ? ["all"] : policy === "allowlist" ? [] : ["moderator"];
|
||||
return setTwitchAccessControl(cfg, allowedRoles, true, accountId);
|
||||
},
|
||||
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
|
||||
const resolvedAccountId = resolveSetupAccountId(cfg, accountId);
|
||||
const account = getAccountConfig(cfg, resolvedAccountId);
|
||||
const existingAllowFrom = account?.allowFrom ?? [];
|
||||
|
||||
const entry = await prompter.text({
|
||||
message: t("wizard.twitch.allowFromPrompt"),
|
||||
placeholder: "123456789",
|
||||
initialValue: existingAllowFrom[0] || undefined,
|
||||
});
|
||||
|
||||
const allowFrom = normalizeStringEntries((entry ?? "").split(/[\n,;]+/g));
|
||||
|
||||
return setTwitchAccount(
|
||||
cfg,
|
||||
{
|
||||
...(account ?? undefined),
|
||||
allowFrom,
|
||||
},
|
||||
resolvedAccountId,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const twitchGroupAccess: NonNullable<ChannelSetupWizard["groupAccess"]> = {
|
||||
label: "Twitch chat",
|
||||
placeholder: "",
|
||||
skipAllowlistEntries: true,
|
||||
currentPolicy: ({ cfg, accountId }) => resolveTwitchGroupPolicy(cfg, accountId),
|
||||
currentEntries: ({ cfg, accountId }) => {
|
||||
const account = getAccountConfig(cfg, resolveSetupAccountId(cfg, accountId));
|
||||
return account?.allowFrom ?? [];
|
||||
},
|
||||
updatePrompt: ({ cfg, accountId }) => {
|
||||
const account = getAccountConfig(cfg, resolveSetupAccountId(cfg, accountId));
|
||||
return Boolean(account?.allowedRoles?.length || account?.allowFrom?.length);
|
||||
},
|
||||
setPolicy: ({ cfg, accountId, policy }) => setTwitchGroupPolicy(cfg, policy, accountId),
|
||||
resolveAllowlist: async () => [],
|
||||
applyAllowlist: ({ cfg }) => cfg,
|
||||
};
|
||||
|
||||
export const twitchSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ cfg }) => resolveSetupAccountId(cfg),
|
||||
applyAccountConfig: ({ cfg, accountId }) =>
|
||||
setTwitchAccount(
|
||||
cfg,
|
||||
{
|
||||
enabled: true,
|
||||
},
|
||||
accountId,
|
||||
),
|
||||
};
|
||||
|
||||
export const twitchSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
resolveAccountIdForConfigure: ({ cfg, accountOverride }) =>
|
||||
resolveSetupAccountId(cfg, accountOverride),
|
||||
resolveShouldPromptAccountIds: () => false,
|
||||
status: {
|
||||
configuredLabel: t("wizard.channels.statusConfigured"),
|
||||
unconfiguredLabel: t("wizard.channels.statusNeedsUsernameTokenClientId"),
|
||||
configuredHint: t("wizard.channels.statusConfigured"),
|
||||
unconfiguredHint: t("wizard.channels.statusNeedsSetup"),
|
||||
resolveConfigured: ({ cfg, accountId }) => {
|
||||
return resolveTwitchAccountContext(cfg, resolveSetupAccountId(cfg, accountId)).configured;
|
||||
},
|
||||
resolveStatusLines: ({ cfg, accountId }) => {
|
||||
const resolvedAccountId = resolveSetupAccountId(cfg, accountId);
|
||||
const configured = resolveTwitchAccountContext(cfg, resolvedAccountId).configured;
|
||||
return [
|
||||
`Twitch${resolvedAccountId !== DEFAULT_ACCOUNT_ID ? ` (${resolvedAccountId})` : ""}: ${
|
||||
configured
|
||||
? t("wizard.channels.statusConfigured")
|
||||
: t("wizard.channels.statusNeedsUsernameTokenClientId")
|
||||
}`,
|
||||
];
|
||||
},
|
||||
},
|
||||
credentials: [],
|
||||
finalize: async ({ cfg, accountId: requestedAccountId, prompter, forceAllowFrom }) => {
|
||||
const accountId = resolveSetupAccountId(cfg, requestedAccountId);
|
||||
const account = getAccountConfig(cfg, accountId);
|
||||
|
||||
if (!account || !isAccountConfigured(account)) {
|
||||
await noteTwitchSetupHelp(prompter);
|
||||
}
|
||||
|
||||
const envToken = process.env.OPENCLAW_TWITCH_ACCESS_TOKEN?.trim();
|
||||
|
||||
if (accountId === DEFAULT_ACCOUNT_ID && envToken && !account?.accessToken) {
|
||||
const envResult = await configureWithEnvToken(
|
||||
cfg,
|
||||
prompter,
|
||||
account,
|
||||
envToken,
|
||||
forceAllowFrom,
|
||||
twitchDmPolicy,
|
||||
accountId,
|
||||
);
|
||||
if (envResult) {
|
||||
return envResult;
|
||||
}
|
||||
}
|
||||
|
||||
const username = await promptUsername(prompter, account);
|
||||
const token = await promptToken(prompter, account, envToken);
|
||||
const clientId = await promptClientId(prompter, account);
|
||||
const channelName = await promptChannelName(prompter, account);
|
||||
const { clientSecret, refreshToken } = await promptRefreshTokenSetup(prompter, account);
|
||||
|
||||
const cfgWithAccount = setTwitchAccount(
|
||||
cfg,
|
||||
{
|
||||
username,
|
||||
accessToken: token,
|
||||
clientId,
|
||||
channel: channelName,
|
||||
clientSecret,
|
||||
refreshToken,
|
||||
enabled: true,
|
||||
},
|
||||
accountId,
|
||||
);
|
||||
|
||||
const cfgWithAllowFrom =
|
||||
forceAllowFrom && twitchDmPolicy.promptAllowFrom
|
||||
? await twitchDmPolicy.promptAllowFrom({ cfg: cfgWithAccount, prompter, accountId })
|
||||
: cfgWithAccount;
|
||||
|
||||
return { cfg: cfgWithAllowFrom };
|
||||
},
|
||||
dmPolicy: twitchDmPolicy,
|
||||
groupAccess: twitchGroupAccess,
|
||||
disable: (cfg) => {
|
||||
const twitch = (cfg.channels as Record<string, unknown>)?.twitch as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
twitch: { ...twitch, enabled: false },
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
type ResolvedTwitchAccount = TwitchAccountConfig & { accountId?: string | null };
|
||||
|
||||
export const twitchSetupPlugin: ChannelPlugin<ResolvedTwitchAccount> = {
|
||||
id: channel,
|
||||
meta: getChatChannelMeta(channel),
|
||||
capabilities: {
|
||||
chatTypes: ["group"],
|
||||
},
|
||||
config: {
|
||||
listAccountIds: (cfg) => listAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => {
|
||||
const resolvedAccountId = normalizeAccountId(accountId ?? resolveDefaultTwitchAccountId(cfg));
|
||||
const account = getAccountConfig(cfg, resolvedAccountId);
|
||||
if (!account) {
|
||||
return {
|
||||
accountId: resolvedAccountId,
|
||||
username: "",
|
||||
accessToken: "",
|
||||
clientId: "",
|
||||
channel: "",
|
||||
enabled: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
accountId: resolvedAccountId,
|
||||
...account,
|
||||
};
|
||||
},
|
||||
defaultAccountId: (cfg) => resolveDefaultTwitchAccountId(cfg),
|
||||
isConfigured: (account, cfg) => resolveTwitchAccountContext(cfg, account?.accountId).configured,
|
||||
isEnabled: (account) => account.enabled !== false,
|
||||
},
|
||||
setup: twitchSetupAdapter,
|
||||
setupWizard: twitchSetupWizard,
|
||||
};
|
||||
Reference in New Issue
Block a user