refactor: move matrix msteams twitch to setup wizard
This commit is contained in:
parent
40be12db96
commit
0958aea112
@ -6,12 +6,10 @@ import {
|
||||
createScopedDmSecurityResolver,
|
||||
} from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
buildChannelConfigSchema,
|
||||
buildProbeChannelStatusSummary,
|
||||
collectStatusIssuesFromLastError,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk/matrix";
|
||||
@ -30,9 +28,8 @@ import {
|
||||
type ResolvedMatrixAccount,
|
||||
} from "./matrix/accounts.js";
|
||||
import { normalizeMatrixAllowList, normalizeMatrixUserId } from "./matrix/monitor/allowlist.js";
|
||||
import { matrixOnboardingAdapter } from "./onboarding.js";
|
||||
import { getMatrixRuntime } from "./runtime.js";
|
||||
import { normalizeSecretInputString } from "./secret-input.js";
|
||||
import { matrixSetupAdapter, matrixSetupWizard } from "./setup-surface.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
// Mutex for serializing account startup (workaround for concurrent dynamic import race condition)
|
||||
@ -66,38 +63,6 @@ function normalizeMatrixMessagingTarget(raw: string): string | undefined {
|
||||
return stripped || undefined;
|
||||
}
|
||||
|
||||
function buildMatrixConfigUpdate(
|
||||
cfg: CoreConfig,
|
||||
input: {
|
||||
homeserver?: string;
|
||||
userId?: string;
|
||||
accessToken?: string;
|
||||
password?: string;
|
||||
deviceName?: string;
|
||||
initialSyncLimit?: number;
|
||||
},
|
||||
): CoreConfig {
|
||||
const existing = cfg.channels?.matrix ?? {};
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
matrix: {
|
||||
...existing,
|
||||
enabled: true,
|
||||
...(input.homeserver ? { homeserver: input.homeserver } : {}),
|
||||
...(input.userId ? { userId: input.userId } : {}),
|
||||
...(input.accessToken ? { accessToken: input.accessToken } : {}),
|
||||
...(input.password ? { password: input.password } : {}),
|
||||
...(input.deviceName ? { deviceName: input.deviceName } : {}),
|
||||
...(typeof input.initialSyncLimit === "number"
|
||||
? { initialSyncLimit: input.initialSyncLimit }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const matrixConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }) =>
|
||||
resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId }),
|
||||
@ -132,7 +97,7 @@ const resolveMatrixDmPolicy = createScopedDmSecurityResolver<ResolvedMatrixAccou
|
||||
export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
||||
id: "matrix",
|
||||
meta,
|
||||
onboarding: matrixOnboardingAdapter,
|
||||
setupWizard: matrixSetupWizard,
|
||||
pairing: {
|
||||
idLabel: "matrixUserId",
|
||||
normalizeAllowEntry: (entry) => entry.replace(/^matrix:/i, ""),
|
||||
@ -316,67 +281,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
||||
(await loadMatrixChannelRuntime()).resolveMatrixTargets({ cfg, inputs, kind, runtime }),
|
||||
},
|
||||
actions: matrixMessageActions,
|
||||
setup: {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg: cfg as CoreConfig,
|
||||
channelKey: "matrix",
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
validateInput: ({ input }) => {
|
||||
if (input.useEnv) {
|
||||
return null;
|
||||
}
|
||||
if (!input.homeserver?.trim()) {
|
||||
return "Matrix requires --homeserver";
|
||||
}
|
||||
const accessToken = input.accessToken?.trim();
|
||||
const password = normalizeSecretInputString(input.password);
|
||||
const userId = input.userId?.trim();
|
||||
if (!accessToken && !password) {
|
||||
return "Matrix requires --access-token or --password";
|
||||
}
|
||||
if (!accessToken) {
|
||||
if (!userId) {
|
||||
return "Matrix requires --user-id when using --password";
|
||||
}
|
||||
if (!password) {
|
||||
return "Matrix requires --password when using --user-id";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, input }) => {
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg: cfg as CoreConfig,
|
||||
channelKey: "matrix",
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
name: input.name,
|
||||
});
|
||||
if (input.useEnv) {
|
||||
return {
|
||||
...namedConfig,
|
||||
channels: {
|
||||
...namedConfig.channels,
|
||||
matrix: {
|
||||
...namedConfig.channels?.matrix,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
}
|
||||
return buildMatrixConfigUpdate(namedConfig as CoreConfig, {
|
||||
homeserver: input.homeserver?.trim(),
|
||||
userId: input.userId?.trim(),
|
||||
accessToken: input.accessToken?.trim(),
|
||||
password: normalizeSecretInputString(input.password),
|
||||
deviceName: input.deviceName?.trim(),
|
||||
initialSyncLimit: input.initialSyncLimit,
|
||||
});
|
||||
},
|
||||
},
|
||||
setup: matrixSetupAdapter,
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, limit) => getMatrixRuntime().channel.text.chunkMarkdownText!(text, limit),
|
||||
|
||||
@ -1,19 +1,29 @@
|
||||
import type { DmPolicy } from "openclaw/plugin-sdk/matrix";
|
||||
import type { ChannelOnboardingDmPolicy } from "../../../src/channels/plugins/onboarding-types.js";
|
||||
import { promptChannelAccessConfig } from "../../../src/channels/plugins/onboarding/channel-access.js";
|
||||
import {
|
||||
addWildcardAllowFrom,
|
||||
buildSingleChannelSecretPromptState,
|
||||
formatResolvedUnresolvedNote,
|
||||
formatDocsLink,
|
||||
hasConfiguredSecretInput,
|
||||
mergeAllowFromEntries,
|
||||
promptSingleChannelSecretInput,
|
||||
promptChannelAccessConfig,
|
||||
setTopLevelChannelGroupPolicy,
|
||||
type SecretInput,
|
||||
type ChannelOnboardingAdapter,
|
||||
type ChannelOnboardingDmPolicy,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/matrix";
|
||||
} from "../../../src/channels/plugins/onboarding/helpers.js";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type { DmPolicy } from "../../../src/config/types.js";
|
||||
import type { SecretInput } from "../../../src/config/types.secrets.js";
|
||||
import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
} from "../../../src/config/types.secrets.js";
|
||||
import { formatResolvedUnresolvedNote } from "../../../src/plugin-sdk/resolution-notes.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
|
||||
import { resolveMatrixAccount } from "./matrix/accounts.js";
|
||||
import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js";
|
||||
@ -22,6 +32,38 @@ import type { CoreConfig } from "./types.js";
|
||||
|
||||
const channel = "matrix" as const;
|
||||
|
||||
function buildMatrixConfigUpdate(
|
||||
cfg: CoreConfig,
|
||||
input: {
|
||||
homeserver?: string;
|
||||
userId?: string;
|
||||
accessToken?: string;
|
||||
password?: string;
|
||||
deviceName?: string;
|
||||
initialSyncLimit?: number;
|
||||
},
|
||||
): CoreConfig {
|
||||
const existing = cfg.channels?.matrix ?? {};
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
matrix: {
|
||||
...existing,
|
||||
enabled: true,
|
||||
...(input.homeserver ? { homeserver: input.homeserver } : {}),
|
||||
...(input.userId ? { userId: input.userId } : {}),
|
||||
...(input.accessToken ? { accessToken: input.accessToken } : {}),
|
||||
...(input.password ? { password: input.password } : {}),
|
||||
...(input.deviceName ? { deviceName: input.deviceName } : {}),
|
||||
...(typeof input.initialSyncLimit === "number"
|
||||
? { initialSyncLimit: input.initialSyncLimit }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setMatrixDmPolicy(cfg: CoreConfig, policy: DmPolicy) {
|
||||
const allowFrom =
|
||||
policy === "open" ? addWildcardAllowFrom(cfg.channels?.matrix?.dm?.allowFrom) : undefined;
|
||||
@ -168,7 +210,7 @@ function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[]) {
|
||||
};
|
||||
}
|
||||
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
const matrixDmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "Matrix",
|
||||
channel,
|
||||
policyKey: "channels.matrix.dm.policy",
|
||||
@ -178,26 +220,100 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
promptAllowFrom: promptMatrixAllowFrom,
|
||||
};
|
||||
|
||||
export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const account = resolveMatrixAccount({ cfg: cfg as CoreConfig });
|
||||
const configured = account.configured;
|
||||
const sdkReady = isMatrixSdkAvailable();
|
||||
return {
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [
|
||||
`Matrix: ${configured ? "configured" : "needs homeserver + access token or password"}`,
|
||||
],
|
||||
selectionHint: !sdkReady
|
||||
? "install @vector-im/matrix-bot-sdk"
|
||||
: configured
|
||||
? "configured"
|
||||
: "needs auth",
|
||||
};
|
||||
export const matrixSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg: cfg as CoreConfig,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
validateInput: ({ input }) => {
|
||||
if (input.useEnv) {
|
||||
return null;
|
||||
}
|
||||
if (!input.homeserver?.trim()) {
|
||||
return "Matrix requires --homeserver";
|
||||
}
|
||||
const accessToken = input.accessToken?.trim();
|
||||
const password = normalizeSecretInputString(input.password);
|
||||
const userId = input.userId?.trim();
|
||||
if (!accessToken && !password) {
|
||||
return "Matrix requires --access-token or --password";
|
||||
}
|
||||
if (!accessToken) {
|
||||
if (!userId) {
|
||||
return "Matrix requires --user-id when using --password";
|
||||
}
|
||||
if (!password) {
|
||||
return "Matrix requires --password when using --user-id";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
configure: async ({ cfg, runtime, prompter, forceAllowFrom }) => {
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg: cfg as CoreConfig,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name: input.name,
|
||||
});
|
||||
const next =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
channelKey: channel,
|
||||
})
|
||||
: namedConfig;
|
||||
if (input.useEnv) {
|
||||
return {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
matrix: {
|
||||
...next.channels?.matrix,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
}
|
||||
return buildMatrixConfigUpdate(next as CoreConfig, {
|
||||
homeserver: input.homeserver?.trim(),
|
||||
userId: input.userId?.trim(),
|
||||
accessToken: input.accessToken?.trim(),
|
||||
password: normalizeSecretInputString(input.password),
|
||||
deviceName: input.deviceName?.trim(),
|
||||
initialSyncLimit: input.initialSyncLimit,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const matrixSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
resolveAccountIdForConfigure: () => DEFAULT_ACCOUNT_ID,
|
||||
resolveShouldPromptAccountIds: () => false,
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs homeserver + access token or password",
|
||||
configuredHint: "configured",
|
||||
unconfiguredHint: "needs auth",
|
||||
resolveConfigured: ({ cfg }) => resolveMatrixAccount({ cfg: cfg as CoreConfig }).configured,
|
||||
resolveStatusLines: ({ cfg }) => {
|
||||
const configured = resolveMatrixAccount({ cfg: cfg as CoreConfig }).configured;
|
||||
return [
|
||||
`Matrix: ${configured ? "configured" : "needs homeserver + access token or password"}`,
|
||||
];
|
||||
},
|
||||
resolveSelectionHint: ({ cfg, configured }) => {
|
||||
if (!isMatrixSdkAvailable()) {
|
||||
return "install @vector-im/matrix-bot-sdk";
|
||||
}
|
||||
return configured ? "configured" : "needs auth";
|
||||
},
|
||||
},
|
||||
credentials: [],
|
||||
finalize: async ({ cfg, runtime, prompter, forceAllowFrom }) => {
|
||||
let next = cfg as CoreConfig;
|
||||
await ensureMatrixSdkInstalled({
|
||||
runtime,
|
||||
@ -231,16 +347,11 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
initialValue: true,
|
||||
});
|
||||
if (useEnv) {
|
||||
next = {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
matrix: {
|
||||
...next.channels?.matrix,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
next = matrixSetupAdapter.applyAccountConfig({
|
||||
cfg: next,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
input: { useEnv: true },
|
||||
}) as CoreConfig;
|
||||
if (forceAllowFrom) {
|
||||
next = await promptMatrixAllowFrom({ cfg: next, prompter });
|
||||
}
|
||||
@ -284,7 +395,6 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
}
|
||||
|
||||
if (!accessToken && !passwordConfigured()) {
|
||||
// Ask auth method FIRST before asking for user ID
|
||||
const authMode = await prompter.select({
|
||||
message: "Matrix auth method",
|
||||
options: [
|
||||
@ -300,11 +410,8 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
// With access token, we can fetch the userId automatically - don't prompt for it
|
||||
// The client.ts will use whoami() to get it
|
||||
userId = "";
|
||||
} else {
|
||||
// Password auth requires user ID upfront
|
||||
userId = String(
|
||||
await prompter.text({
|
||||
message: "Matrix user ID",
|
||||
@ -333,7 +440,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
const passwordResult = await promptSingleChannelSecretInput({
|
||||
cfg: next,
|
||||
prompter,
|
||||
providerHint: "matrix",
|
||||
providerHint: channel,
|
||||
credentialLabel: "password",
|
||||
accountConfigured: passwordPromptState.accountConfigured,
|
||||
canUseEnv: passwordPromptState.canUseEnv,
|
||||
@ -359,7 +466,6 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
}),
|
||||
).trim();
|
||||
|
||||
// Ask about E2EE encryption
|
||||
const enableEncryption = await prompter.confirm({
|
||||
message: "Enable end-to-end encryption (E2EE)?",
|
||||
initialValue: existing.encryption ?? false,
|
||||
@ -375,7 +481,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
homeserver,
|
||||
userId: userId || undefined,
|
||||
accessToken: accessToken || undefined,
|
||||
password: password,
|
||||
password,
|
||||
deviceName: deviceName || undefined,
|
||||
encryption: enableEncryption || undefined,
|
||||
},
|
||||
@ -451,7 +557,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
|
||||
return { cfg: next };
|
||||
},
|
||||
dmPolicy,
|
||||
dmPolicy: matrixDmPolicy,
|
||||
disable: (cfg) => ({
|
||||
...(cfg as CoreConfig),
|
||||
channels: {
|
||||
@ -16,7 +16,6 @@ import {
|
||||
MSTeamsConfigSchema,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
} from "openclaw/plugin-sdk/msteams";
|
||||
import { msteamsOnboardingAdapter } from "./onboarding.js";
|
||||
import { resolveMSTeamsGroupToolPolicy } from "./policy.js";
|
||||
import {
|
||||
normalizeMSTeamsMessagingTarget,
|
||||
@ -27,6 +26,7 @@ import {
|
||||
resolveMSTeamsUserAllowlist,
|
||||
} from "./resolve-allowlist.js";
|
||||
import { getMSTeamsRuntime } from "./runtime.js";
|
||||
import { msteamsSetupAdapter, msteamsSetupWizard } from "./setup-surface.js";
|
||||
import { resolveMSTeamsCredentials } from "./token.js";
|
||||
|
||||
type ResolvedMSTeamsAccount = {
|
||||
@ -56,7 +56,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
...meta,
|
||||
aliases: [...meta.aliases],
|
||||
},
|
||||
onboarding: msteamsOnboardingAdapter,
|
||||
setupWizard: msteamsSetupWizard,
|
||||
pairing: {
|
||||
idLabel: "msteamsUserId",
|
||||
normalizeAllowEntry: (entry) => entry.replace(/^(msteams|user):/i, ""),
|
||||
@ -145,19 +145,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
});
|
||||
},
|
||||
},
|
||||
setup: {
|
||||
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
applyAccountConfig: ({ cfg }) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
msteams: {
|
||||
...cfg.channels?.msteams,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
setup: msteamsSetupAdapter,
|
||||
messaging: {
|
||||
normalizeTarget: normalizeMSTeamsMessagingTarget,
|
||||
targetResolver: {
|
||||
|
||||
@ -1,21 +1,19 @@
|
||||
import type {
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
OpenClawConfig,
|
||||
DmPolicy,
|
||||
WizardPrompter,
|
||||
MSTeamsTeamConfig,
|
||||
} from "openclaw/plugin-sdk/msteams";
|
||||
import type { ChannelOnboardingDmPolicy } from "../../../src/channels/plugins/onboarding-types.js";
|
||||
import { promptChannelAccessConfig } from "../../../src/channels/plugins/onboarding/channel-access.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
mergeAllowFromEntries,
|
||||
promptChannelAccessConfig,
|
||||
setTopLevelChannelAllowFrom,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
setTopLevelChannelGroupPolicy,
|
||||
splitOnboardingEntries,
|
||||
} from "openclaw/plugin-sdk/msteams";
|
||||
} from "../../../src/channels/plugins/onboarding/helpers.js";
|
||||
import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type { DmPolicy, MSTeamsTeamConfig } from "../../../src/config/types.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
import {
|
||||
parseMSTeamsTeamEntry,
|
||||
resolveMSTeamsChannelAllowlist,
|
||||
@ -29,7 +27,7 @@ const channel = "msteams" as const;
|
||||
function setMSTeamsDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy) {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel: "msteams",
|
||||
channel,
|
||||
dmPolicy,
|
||||
});
|
||||
}
|
||||
@ -37,7 +35,7 @@ function setMSTeamsDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy) {
|
||||
function setMSTeamsAllowFrom(cfg: OpenClawConfig, allowFrom: string[]): OpenClawConfig {
|
||||
return setTopLevelChannelAllowFrom({
|
||||
cfg,
|
||||
channel: "msteams",
|
||||
channel,
|
||||
allowFrom,
|
||||
});
|
||||
}
|
||||
@ -138,7 +136,7 @@ async function promptMSTeamsAllowFrom(params: {
|
||||
async function noteMSTeamsCredentialHelp(prompter: WizardPrompter): Promise<void> {
|
||||
await prompter.note(
|
||||
[
|
||||
"1) Azure Bot registration → get App ID + Tenant ID",
|
||||
"1) Azure Bot registration -> get App ID + Tenant ID",
|
||||
"2) Add a client secret (App Password)",
|
||||
"3) Set webhook URL + messaging endpoint",
|
||||
"Tip: you can also set MSTEAMS_APP_ID / MSTEAMS_APP_PASSWORD / MSTEAMS_TENANT_ID.",
|
||||
@ -154,7 +152,7 @@ function setMSTeamsGroupPolicy(
|
||||
): OpenClawConfig {
|
||||
return setTopLevelChannelGroupPolicy({
|
||||
cfg,
|
||||
channel: "msteams",
|
||||
channel,
|
||||
groupPolicy,
|
||||
enabled: true,
|
||||
});
|
||||
@ -193,7 +191,7 @@ function setMSTeamsTeamsAllowlist(
|
||||
};
|
||||
}
|
||||
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
const msteamsDmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "MS Teams",
|
||||
channel,
|
||||
policyKey: "channels.msteams.dmPolicy",
|
||||
@ -203,21 +201,46 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
promptAllowFrom: promptMSTeamsAllowFrom,
|
||||
};
|
||||
|
||||
export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
export const msteamsSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
applyAccountConfig: ({ cfg }) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
msteams: {
|
||||
...cfg.channels?.msteams,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const msteamsSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const configured =
|
||||
Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)) ||
|
||||
hasConfiguredMSTeamsCredentials(cfg.channels?.msteams);
|
||||
return {
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [`MS Teams: ${configured ? "configured" : "needs app credentials"}`],
|
||||
selectionHint: configured ? "configured" : "needs app creds",
|
||||
quickstartScore: configured ? 2 : 0,
|
||||
};
|
||||
resolveAccountIdForConfigure: () => DEFAULT_ACCOUNT_ID,
|
||||
resolveShouldPromptAccountIds: () => false,
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs app credentials",
|
||||
configuredHint: "configured",
|
||||
unconfiguredHint: "needs app creds",
|
||||
configuredScore: 2,
|
||||
unconfiguredScore: 0,
|
||||
resolveConfigured: ({ cfg }) => {
|
||||
return (
|
||||
Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)) ||
|
||||
hasConfiguredMSTeamsCredentials(cfg.channels?.msteams)
|
||||
);
|
||||
},
|
||||
resolveStatusLines: ({ cfg }) => {
|
||||
const configured =
|
||||
Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)) ||
|
||||
hasConfiguredMSTeamsCredentials(cfg.channels?.msteams);
|
||||
return [`MS Teams: ${configured ? "configured" : "needs app credentials"}`];
|
||||
},
|
||||
},
|
||||
configure: async ({ cfg, prompter }) => {
|
||||
credentials: [],
|
||||
finalize: async ({ cfg, prompter }) => {
|
||||
const resolved = resolveMSTeamsCredentials(cfg.channels?.msteams);
|
||||
const hasConfigCreds = hasConfiguredMSTeamsCredentials(cfg.channels?.msteams);
|
||||
const canUseEnv = Boolean(
|
||||
@ -243,13 +266,11 @@ export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
initialValue: true,
|
||||
});
|
||||
if (keepEnv) {
|
||||
next = {
|
||||
...next,
|
||||
channels: {
|
||||
...next.channels,
|
||||
msteams: { ...next.channels?.msteams, enabled: true },
|
||||
},
|
||||
};
|
||||
next = msteamsSetupAdapter.applyAccountConfig({
|
||||
cfg: next,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
input: {},
|
||||
});
|
||||
} else {
|
||||
({ appId, appPassword, tenantId } = await promptMSTeamsCredentials(prompter));
|
||||
}
|
||||
@ -308,17 +329,17 @@ export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
.filter(Boolean) as Array<{ teamKey: string; channelKey?: string }>;
|
||||
if (accessConfig.entries.length > 0 && resolveMSTeamsCredentials(next.channels?.msteams)) {
|
||||
try {
|
||||
const resolved = await resolveMSTeamsChannelAllowlist({
|
||||
const resolvedEntries = await resolveMSTeamsChannelAllowlist({
|
||||
cfg: next,
|
||||
entries: accessConfig.entries,
|
||||
});
|
||||
const resolvedChannels = resolved.filter(
|
||||
const resolvedChannels = resolvedEntries.filter(
|
||||
(entry) => entry.resolved && entry.teamId && entry.channelId,
|
||||
);
|
||||
const resolvedTeams = resolved.filter(
|
||||
const resolvedTeams = resolvedEntries.filter(
|
||||
(entry) => entry.resolved && entry.teamId && !entry.channelId,
|
||||
);
|
||||
const unresolved = resolved
|
||||
const unresolved = resolvedEntries
|
||||
.filter((entry) => !entry.resolved)
|
||||
.map((entry) => entry.input);
|
||||
|
||||
@ -370,7 +391,7 @@ export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
|
||||
return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
|
||||
},
|
||||
dmPolicy,
|
||||
dmPolicy: msteamsDmPolicy,
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Tests for onboarding.ts helpers
|
||||
* Tests for setup-surface.ts helpers
|
||||
*
|
||||
* Tests cover:
|
||||
* - promptToken helper
|
||||
@ -15,11 +15,6 @@ import type { WizardPrompter } from "openclaw/plugin-sdk/twitch";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { TwitchAccountConfig } from "./types.js";
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/twitch", () => ({
|
||||
formatDocsLink: (url: string, fallback: string) => fallback || url,
|
||||
promptChannelAccessConfig: vi.fn(async () => null),
|
||||
}));
|
||||
|
||||
// Mock the helpers we're testing
|
||||
const mockPromptText = vi.fn();
|
||||
const mockPromptConfirm = vi.fn();
|
||||
@ -35,7 +30,7 @@ const mockAccount: TwitchAccountConfig = {
|
||||
channel: "#testchannel",
|
||||
};
|
||||
|
||||
describe("onboarding helpers", () => {
|
||||
describe("setup surface helpers", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
@ -46,7 +41,7 @@ describe("onboarding helpers", () => {
|
||||
|
||||
describe("promptToken", () => {
|
||||
it("should return existing token when user confirms to keep it", async () => {
|
||||
const { promptToken } = await import("./onboarding.js");
|
||||
const { promptToken } = await import("./setup-surface.js");
|
||||
|
||||
mockPromptConfirm.mockResolvedValue(true);
|
||||
|
||||
@ -61,7 +56,7 @@ describe("onboarding helpers", () => {
|
||||
});
|
||||
|
||||
it("should prompt for new token when user doesn't keep existing", async () => {
|
||||
const { promptToken } = await import("./onboarding.js");
|
||||
const { promptToken } = await import("./setup-surface.js");
|
||||
|
||||
mockPromptConfirm.mockResolvedValue(false);
|
||||
mockPromptText.mockResolvedValue("oauth:newtoken123");
|
||||
@ -77,7 +72,7 @@ describe("onboarding helpers", () => {
|
||||
});
|
||||
|
||||
it("should use env token as initial value when provided", async () => {
|
||||
const { promptToken } = await import("./onboarding.js");
|
||||
const { promptToken } = await import("./setup-surface.js");
|
||||
|
||||
mockPromptConfirm.mockResolvedValue(false);
|
||||
mockPromptText.mockResolvedValue("oauth:fromenv");
|
||||
@ -92,7 +87,7 @@ describe("onboarding helpers", () => {
|
||||
});
|
||||
|
||||
it("should validate token format", async () => {
|
||||
const { promptToken } = await import("./onboarding.js");
|
||||
const { promptToken } = await import("./setup-surface.js");
|
||||
|
||||
// Set up mocks - user doesn't want to keep existing token
|
||||
mockPromptConfirm.mockResolvedValueOnce(false);
|
||||
@ -124,7 +119,7 @@ describe("onboarding helpers", () => {
|
||||
});
|
||||
|
||||
it("should return early when no existing token and no env token", async () => {
|
||||
const { promptToken } = await import("./onboarding.js");
|
||||
const { promptToken } = await import("./setup-surface.js");
|
||||
|
||||
mockPromptText.mockResolvedValue("oauth:newtoken");
|
||||
|
||||
@ -137,7 +132,7 @@ describe("onboarding helpers", () => {
|
||||
|
||||
describe("promptUsername", () => {
|
||||
it("should prompt for username with validation", async () => {
|
||||
const { promptUsername } = await import("./onboarding.js");
|
||||
const { promptUsername } = await import("./setup-surface.js");
|
||||
|
||||
mockPromptText.mockResolvedValue("mybot");
|
||||
|
||||
@ -152,7 +147,7 @@ describe("onboarding helpers", () => {
|
||||
});
|
||||
|
||||
it("should use existing username as initial value", async () => {
|
||||
const { promptUsername } = await import("./onboarding.js");
|
||||
const { promptUsername } = await import("./setup-surface.js");
|
||||
|
||||
mockPromptText.mockResolvedValue("testbot");
|
||||
|
||||
@ -168,7 +163,7 @@ describe("onboarding helpers", () => {
|
||||
|
||||
describe("promptClientId", () => {
|
||||
it("should prompt for client ID with validation", async () => {
|
||||
const { promptClientId } = await import("./onboarding.js");
|
||||
const { promptClientId } = await import("./setup-surface.js");
|
||||
|
||||
mockPromptText.mockResolvedValue("abc123xyz");
|
||||
|
||||
@ -185,7 +180,7 @@ describe("onboarding helpers", () => {
|
||||
|
||||
describe("promptChannelName", () => {
|
||||
it("should return channel name when provided", async () => {
|
||||
const { promptChannelName } = await import("./onboarding.js");
|
||||
const { promptChannelName } = await import("./setup-surface.js");
|
||||
|
||||
mockPromptText.mockResolvedValue("#mychannel");
|
||||
|
||||
@ -195,7 +190,7 @@ describe("onboarding helpers", () => {
|
||||
});
|
||||
|
||||
it("should require a non-empty channel name", async () => {
|
||||
const { promptChannelName } = await import("./onboarding.js");
|
||||
const { promptChannelName } = await import("./setup-surface.js");
|
||||
|
||||
mockPromptText.mockResolvedValue("");
|
||||
|
||||
@ -210,7 +205,7 @@ describe("onboarding helpers", () => {
|
||||
|
||||
describe("promptRefreshTokenSetup", () => {
|
||||
it("should return empty object when user declines", async () => {
|
||||
const { promptRefreshTokenSetup } = await import("./onboarding.js");
|
||||
const { promptRefreshTokenSetup } = await import("./setup-surface.js");
|
||||
|
||||
mockPromptConfirm.mockResolvedValue(false);
|
||||
|
||||
@ -224,7 +219,7 @@ describe("onboarding helpers", () => {
|
||||
});
|
||||
|
||||
it("should prompt for credentials when user accepts", async () => {
|
||||
const { promptRefreshTokenSetup } = await import("./onboarding.js");
|
||||
const { promptRefreshTokenSetup } = await import("./setup-surface.js");
|
||||
|
||||
mockPromptConfirm
|
||||
.mockResolvedValueOnce(true) // First call: useRefresh
|
||||
@ -242,7 +237,7 @@ describe("onboarding helpers", () => {
|
||||
});
|
||||
|
||||
it("should use existing values as initial prompts", async () => {
|
||||
const { promptRefreshTokenSetup } = await import("./onboarding.js");
|
||||
const { promptRefreshTokenSetup } = await import("./setup-surface.js");
|
||||
|
||||
const accountWithRefresh = {
|
||||
...mockAccount,
|
||||
@ -267,7 +262,7 @@ describe("onboarding helpers", () => {
|
||||
|
||||
describe("configureWithEnvToken", () => {
|
||||
it("should return null when user declines env token", async () => {
|
||||
const { configureWithEnvToken } = await import("./onboarding.js");
|
||||
const { configureWithEnvToken } = await import("./setup-surface.js");
|
||||
|
||||
// Reset and set up mock - user declines env token
|
||||
mockPromptConfirm.mockReset().mockResolvedValue(false as never);
|
||||
@ -287,7 +282,7 @@ describe("onboarding helpers", () => {
|
||||
});
|
||||
|
||||
it("should prompt for username and clientId when using env token", async () => {
|
||||
const { configureWithEnvToken } = await import("./onboarding.js");
|
||||
const { configureWithEnvToken } = await import("./setup-surface.js");
|
||||
|
||||
// Reset and set up mocks - user accepts env token
|
||||
mockPromptConfirm.mockReset().mockResolvedValue(true as never);
|
||||
|
||||
@ -12,10 +12,10 @@ 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 { twitchSetupAdapter, twitchSetupWizard } from "./setup-surface.js";
|
||||
import { collectTwitchStatusIssues } from "./status.js";
|
||||
import { resolveTwitchToken } from "./token.js";
|
||||
import type {
|
||||
@ -51,8 +51,9 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
||||
aliases: ["twitch-chat"],
|
||||
} satisfies ChannelMeta,
|
||||
|
||||
/** Onboarding adapter */
|
||||
onboarding: twitchOnboardingAdapter,
|
||||
/** Setup wizard surface */
|
||||
setup: twitchSetupAdapter,
|
||||
setupWizard: twitchSetupWizard,
|
||||
|
||||
/** Pairing configuration */
|
||||
pairing: {
|
||||
|
||||
@ -1,25 +1,21 @@
|
||||
/**
|
||||
* Twitch onboarding adapter for CLI setup wizard.
|
||||
* Twitch setup wizard surface for CLI setup.
|
||||
*/
|
||||
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/twitch";
|
||||
import {
|
||||
formatDocsLink,
|
||||
promptChannelAccessConfig,
|
||||
type ChannelOnboardingAdapter,
|
||||
type ChannelOnboardingDmPolicy,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/twitch";
|
||||
import type { ChannelOnboardingDmPolicy } from "../../../src/channels/plugins/onboarding-types.js";
|
||||
import { promptChannelAccessConfig } from "../../../src/channels/plugins/onboarding/channel-access.js";
|
||||
import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js";
|
||||
import type { TwitchAccountConfig, TwitchRole } from "./types.js";
|
||||
import { isAccountConfigured } from "./utils/twitch.js";
|
||||
|
||||
const channel = "twitch" as const;
|
||||
|
||||
/**
|
||||
* Set Twitch account configuration
|
||||
*/
|
||||
function setTwitchAccount(
|
||||
export function setTwitchAccount(
|
||||
cfg: OpenClawConfig,
|
||||
account: Partial<TwitchAccountConfig>,
|
||||
): OpenClawConfig {
|
||||
@ -59,9 +55,6 @@ function setTwitchAccount(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Note about Twitch setup
|
||||
*/
|
||||
async function noteTwitchSetupHelp(prompter: WizardPrompter): Promise<void> {
|
||||
await prompter.note(
|
||||
[
|
||||
@ -77,17 +70,13 @@ async function noteTwitchSetupHelp(prompter: WizardPrompter): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for Twitch OAuth token with early returns.
|
||||
*/
|
||||
async function promptToken(
|
||||
export async function promptToken(
|
||||
prompter: WizardPrompter,
|
||||
account: TwitchAccountConfig | null,
|
||||
envToken: string | undefined,
|
||||
): Promise<string> {
|
||||
const existingToken = account?.accessToken ?? "";
|
||||
|
||||
// If we have an existing token and no env var, ask if we should keep it
|
||||
if (existingToken && !envToken) {
|
||||
const keepToken = await prompter.confirm({
|
||||
message: "Access token already configured. Keep it?",
|
||||
@ -98,7 +87,6 @@ async function promptToken(
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt for new token
|
||||
return String(
|
||||
await prompter.text({
|
||||
message: "Twitch OAuth token (oauth:...)",
|
||||
@ -117,10 +105,7 @@ async function promptToken(
|
||||
).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for Twitch username.
|
||||
*/
|
||||
async function promptUsername(
|
||||
export async function promptUsername(
|
||||
prompter: WizardPrompter,
|
||||
account: TwitchAccountConfig | null,
|
||||
): Promise<string> {
|
||||
@ -133,10 +118,7 @@ async function promptUsername(
|
||||
).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for Twitch Client ID.
|
||||
*/
|
||||
async function promptClientId(
|
||||
export async function promptClientId(
|
||||
prompter: WizardPrompter,
|
||||
account: TwitchAccountConfig | null,
|
||||
): Promise<string> {
|
||||
@ -149,27 +131,20 @@ async function promptClientId(
|
||||
).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for optional channel name.
|
||||
*/
|
||||
async function promptChannelName(
|
||||
export async function promptChannelName(
|
||||
prompter: WizardPrompter,
|
||||
account: TwitchAccountConfig | null,
|
||||
): Promise<string> {
|
||||
const channelName = String(
|
||||
return String(
|
||||
await prompter.text({
|
||||
message: "Channel to join",
|
||||
initialValue: account?.channel ?? "",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
return channelName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for token refresh credentials (client secret and refresh token).
|
||||
*/
|
||||
async function promptRefreshTokenSetup(
|
||||
export async function promptRefreshTokenSetup(
|
||||
prompter: WizardPrompter,
|
||||
account: TwitchAccountConfig | null,
|
||||
): Promise<{ clientSecret?: string; refreshToken?: string }> {
|
||||
@ -203,10 +178,7 @@ async function promptRefreshTokenSetup(
|
||||
return { clientSecret, refreshToken };
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure with env token path (returns early if user chooses env token).
|
||||
*/
|
||||
async function configureWithEnvToken(
|
||||
export async function configureWithEnvToken(
|
||||
cfg: OpenClawConfig,
|
||||
prompter: WizardPrompter,
|
||||
account: TwitchAccountConfig | null,
|
||||
@ -228,7 +200,7 @@ async function configureWithEnvToken(
|
||||
const cfgWithAccount = setTwitchAccount(cfg, {
|
||||
username,
|
||||
clientId,
|
||||
accessToken: "", // Will use env var
|
||||
accessToken: "",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
@ -239,9 +211,6 @@ async function configureWithEnvToken(
|
||||
return { cfg: cfgWithAccount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Twitch access control (role-based)
|
||||
*/
|
||||
function setTwitchAccessControl(
|
||||
cfg: OpenClawConfig,
|
||||
allowedRoles: TwitchRole[],
|
||||
@ -259,14 +228,13 @@ function setTwitchAccessControl(
|
||||
});
|
||||
}
|
||||
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
const twitchDmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "Twitch",
|
||||
channel,
|
||||
policyKey: "channels.twitch.allowedRoles", // Twitch uses roles instead of DM policy
|
||||
policyKey: "channels.twitch.allowedRoles",
|
||||
allowFromKey: "channels.twitch.accounts.default.allowFrom",
|
||||
getCurrent: (cfg) => {
|
||||
const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID);
|
||||
// Map allowedRoles to policy equivalent
|
||||
const account = getAccountConfig(cfg as OpenClawConfig, DEFAULT_ACCOUNT_ID);
|
||||
if (account?.allowedRoles?.includes("all")) {
|
||||
return "open";
|
||||
}
|
||||
@ -278,10 +246,10 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
setPolicy: (cfg, policy) => {
|
||||
const allowedRoles: TwitchRole[] =
|
||||
policy === "open" ? ["all"] : policy === "allowlist" ? [] : ["moderator"];
|
||||
return setTwitchAccessControl(cfg, allowedRoles, true);
|
||||
return setTwitchAccessControl(cfg as OpenClawConfig, allowedRoles, true);
|
||||
},
|
||||
promptAllowFrom: async ({ cfg, prompter }) => {
|
||||
const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID);
|
||||
const account = getAccountConfig(cfg as OpenClawConfig, DEFAULT_ACCOUNT_ID);
|
||||
const existingAllowFrom = account?.allowFrom ?? [];
|
||||
|
||||
const entry = await prompter.text({
|
||||
@ -295,28 +263,43 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return setTwitchAccount(cfg, {
|
||||
return setTwitchAccount(cfg as OpenClawConfig, {
|
||||
...(account ?? undefined),
|
||||
allowFrom,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const twitchOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID);
|
||||
const configured = account ? isAccountConfigured(account) : false;
|
||||
export const twitchSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
applyAccountConfig: ({ cfg }) =>
|
||||
setTwitchAccount(cfg, {
|
||||
enabled: true,
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [`Twitch: ${configured ? "configured" : "needs username, token, and clientId"}`],
|
||||
selectionHint: configured ? "configured" : "needs setup",
|
||||
};
|
||||
export const twitchSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
resolveAccountIdForConfigure: () => DEFAULT_ACCOUNT_ID,
|
||||
resolveShouldPromptAccountIds: () => false,
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs username, token, and clientId",
|
||||
configuredHint: "configured",
|
||||
unconfiguredHint: "needs setup",
|
||||
resolveConfigured: ({ cfg }) => {
|
||||
const account = getAccountConfig(cfg as OpenClawConfig, DEFAULT_ACCOUNT_ID);
|
||||
return account ? isAccountConfigured(account) : false;
|
||||
},
|
||||
resolveStatusLines: ({ cfg }) => {
|
||||
const account = getAccountConfig(cfg as OpenClawConfig, DEFAULT_ACCOUNT_ID);
|
||||
const configured = account ? isAccountConfigured(account) : false;
|
||||
return [`Twitch: ${configured ? "configured" : "needs username, token, and clientId"}`];
|
||||
},
|
||||
},
|
||||
configure: async ({ cfg, prompter, forceAllowFrom }) => {
|
||||
const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID);
|
||||
credentials: [],
|
||||
finalize: async ({ cfg, prompter, forceAllowFrom }) => {
|
||||
const account = getAccountConfig(cfg as OpenClawConfig, DEFAULT_ACCOUNT_ID);
|
||||
|
||||
if (!account || !isAccountConfigured(account)) {
|
||||
await noteTwitchSetupHelp(prompter);
|
||||
@ -324,29 +307,27 @@ export const twitchOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
|
||||
const envToken = process.env.OPENCLAW_TWITCH_ACCESS_TOKEN?.trim();
|
||||
|
||||
// Check if env var is set and config is empty
|
||||
if (envToken && !account?.accessToken) {
|
||||
const envResult = await configureWithEnvToken(
|
||||
cfg,
|
||||
cfg as OpenClawConfig,
|
||||
prompter,
|
||||
account,
|
||||
envToken,
|
||||
forceAllowFrom,
|
||||
dmPolicy,
|
||||
twitchDmPolicy,
|
||||
);
|
||||
if (envResult) {
|
||||
return envResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt for credentials
|
||||
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, {
|
||||
const cfgWithAccount = setTwitchAccount(cfg as OpenClawConfig, {
|
||||
username,
|
||||
accessToken: token,
|
||||
clientId,
|
||||
@ -357,11 +338,10 @@ export const twitchOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
});
|
||||
|
||||
const cfgWithAllowFrom =
|
||||
forceAllowFrom && dmPolicy.promptAllowFrom
|
||||
? await dmPolicy.promptAllowFrom({ cfg: cfgWithAccount, prompter })
|
||||
forceAllowFrom && twitchDmPolicy.promptAllowFrom
|
||||
? await twitchDmPolicy.promptAllowFrom({ cfg: cfgWithAccount, prompter })
|
||||
: cfgWithAccount;
|
||||
|
||||
// Prompt for access control if allowFrom not set
|
||||
if (!account?.allowFrom || account.allowFrom.length === 0) {
|
||||
const accessConfig = await promptChannelAccessConfig({
|
||||
prompter,
|
||||
@ -384,14 +364,15 @@ export const twitchOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
? ["moderator", "vip"]
|
||||
: [];
|
||||
|
||||
const cfgWithAccessControl = setTwitchAccessControl(cfgWithAllowFrom, allowedRoles, true);
|
||||
return { cfg: cfgWithAccessControl };
|
||||
return {
|
||||
cfg: setTwitchAccessControl(cfgWithAllowFrom, allowedRoles, true),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { cfg: cfgWithAllowFrom };
|
||||
},
|
||||
dmPolicy,
|
||||
dmPolicy: twitchDmPolicy,
|
||||
disable: (cfg) => {
|
||||
const twitch = (cfg.channels as Record<string, unknown>)?.twitch as
|
||||
| Record<string, unknown>
|
||||
@ -405,13 +386,3 @@ export const twitchOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Export helper functions for testing
|
||||
export {
|
||||
promptToken,
|
||||
promptUsername,
|
||||
promptClientId,
|
||||
promptChannelName,
|
||||
promptRefreshTokenSetup,
|
||||
configureWithEnvToken,
|
||||
};
|
||||
@ -32,11 +32,6 @@ export {
|
||||
} from "../channels/plugins/config-helpers.js";
|
||||
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
|
||||
export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
|
||||
export type {
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
} from "../channels/plugins/onboarding-types.js";
|
||||
export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js";
|
||||
export {
|
||||
buildSingleChannelSecretPromptState,
|
||||
addWildcardAllowFrom,
|
||||
@ -113,3 +108,7 @@ export {
|
||||
buildProbeChannelStatusSummary,
|
||||
collectStatusIssuesFromLastError,
|
||||
} from "./status-helpers.js";
|
||||
export {
|
||||
matrixSetupAdapter,
|
||||
matrixSetupWizard,
|
||||
} from "../../extensions/matrix/src/setup-surface.js";
|
||||
|
||||
@ -32,11 +32,6 @@ export {
|
||||
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
|
||||
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
|
||||
export { buildMediaPayload } from "../channels/plugins/media-payload.js";
|
||||
export type {
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
} from "../channels/plugins/onboarding-types.js";
|
||||
export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js";
|
||||
export {
|
||||
addWildcardAllowFrom,
|
||||
mergeAllowFromEntries,
|
||||
@ -122,3 +117,7 @@ export {
|
||||
createDefaultChannelRuntimeState,
|
||||
} from "./status-helpers.js";
|
||||
export { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||
export {
|
||||
msteamsSetupAdapter,
|
||||
msteamsSetupWizard,
|
||||
} from "../../extensions/msteams/src/setup-surface.js";
|
||||
|
||||
@ -22,11 +22,6 @@ export type {
|
||||
ChannelStatusIssue,
|
||||
} from "../channels/plugins/types.js";
|
||||
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
||||
export type {
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
} from "../channels/plugins/onboarding-types.js";
|
||||
export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js";
|
||||
export { createReplyPrefixOptions } from "../channels/reply-prefix.js";
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
export { MarkdownConfigSchema } from "../config/zod-schema.core.js";
|
||||
@ -38,3 +33,7 @@ export type { OpenClawPluginApi } from "../plugins/types.js";
|
||||
export type { RuntimeEnv } from "../runtime.js";
|
||||
export { formatDocsLink } from "../terminal/links.js";
|
||||
export type { WizardPrompter } from "../wizard/prompts.js";
|
||||
export {
|
||||
twitchSetupAdapter,
|
||||
twitchSetupWizard,
|
||||
} from "../../extensions/twitch/src/setup-surface.js";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user