mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-29 16:54:30 +00:00
refactor: move group access into setup wizard
This commit is contained in:
@@ -251,7 +251,7 @@ export function createDiscordSetupWizardProxy(
|
||||
prompter: { note: (message: string, title?: string) => Promise<void> };
|
||||
}) => {
|
||||
const wizard = (await loadWizard()).discordSetupWizard;
|
||||
if (!wizard.groupAccess) {
|
||||
if (!wizard.groupAccess?.resolveAllowlist) {
|
||||
return entries.map((input) => ({ input, resolved: false }));
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ChannelOnboardingDmPolicy } from "../../../src/channels/plugins/onboarding-types.js";
|
||||
import { promptChannelAccessConfig } from "../../../src/channels/plugins/onboarding/channel-access.js";
|
||||
import {
|
||||
addWildcardAllowFrom,
|
||||
buildSingleChannelSecretPromptState,
|
||||
@@ -171,6 +170,78 @@ function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[]) {
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveMatrixGroupRooms(params: {
|
||||
cfg: CoreConfig;
|
||||
entries: string[];
|
||||
prompter: Pick<WizardPrompter, "note">;
|
||||
}): Promise<string[]> {
|
||||
if (params.entries.length === 0) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const resolvedIds: string[] = [];
|
||||
const unresolved: string[] = [];
|
||||
for (const entry of params.entries) {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
const cleaned = trimmed.replace(/^(room|channel):/i, "").trim();
|
||||
if (cleaned.startsWith("!") && cleaned.includes(":")) {
|
||||
resolvedIds.push(cleaned);
|
||||
continue;
|
||||
}
|
||||
const matches = await listMatrixDirectoryGroupsLive({
|
||||
cfg: params.cfg,
|
||||
query: trimmed,
|
||||
limit: 10,
|
||||
});
|
||||
const exact = matches.find(
|
||||
(match) => (match.name ?? "").toLowerCase() === trimmed.toLowerCase(),
|
||||
);
|
||||
const best = exact ?? matches[0];
|
||||
if (best?.id) {
|
||||
resolvedIds.push(best.id);
|
||||
} else {
|
||||
unresolved.push(entry);
|
||||
}
|
||||
}
|
||||
const roomKeys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)];
|
||||
const resolution = formatResolvedUnresolvedNote({
|
||||
resolved: resolvedIds,
|
||||
unresolved,
|
||||
});
|
||||
if (resolution) {
|
||||
await params.prompter.note(resolution, "Matrix rooms");
|
||||
}
|
||||
return roomKeys;
|
||||
} catch (err) {
|
||||
await params.prompter.note(
|
||||
`Room lookup failed; keeping entries as typed. ${String(err)}`,
|
||||
"Matrix rooms",
|
||||
);
|
||||
return params.entries.map((entry) => entry.trim()).filter(Boolean);
|
||||
}
|
||||
}
|
||||
|
||||
const matrixGroupAccess: NonNullable<ChannelSetupWizard["groupAccess"]> = {
|
||||
label: "Matrix rooms",
|
||||
placeholder: "!roomId:server, #alias:server, Project Room",
|
||||
currentPolicy: ({ cfg }) => cfg.channels?.matrix?.groupPolicy ?? "allowlist",
|
||||
currentEntries: ({ cfg }) =>
|
||||
Object.keys(cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms ?? {}),
|
||||
updatePrompt: ({ cfg }) => Boolean(cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms),
|
||||
setPolicy: ({ cfg, policy }) => setMatrixGroupPolicy(cfg as CoreConfig, policy),
|
||||
resolveAllowlist: async ({ cfg, entries, prompter }) =>
|
||||
await resolveMatrixGroupRooms({
|
||||
cfg: cfg as CoreConfig,
|
||||
entries,
|
||||
prompter,
|
||||
}),
|
||||
applyAllowlist: ({ cfg, resolved }) =>
|
||||
setMatrixGroupRooms(cfg as CoreConfig, resolved as string[]),
|
||||
};
|
||||
|
||||
const matrixDmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "Matrix",
|
||||
channel,
|
||||
@@ -386,72 +457,10 @@ export const matrixSetupWizard: ChannelSetupWizard = {
|
||||
next = await promptMatrixAllowFrom({ cfg: next, prompter });
|
||||
}
|
||||
|
||||
const existingGroups = next.channels?.matrix?.groups ?? next.channels?.matrix?.rooms;
|
||||
const accessConfig = await promptChannelAccessConfig({
|
||||
prompter,
|
||||
label: "Matrix rooms",
|
||||
currentPolicy: next.channels?.matrix?.groupPolicy ?? "allowlist",
|
||||
currentEntries: Object.keys(existingGroups ?? {}),
|
||||
placeholder: "!roomId:server, #alias:server, Project Room",
|
||||
updatePrompt: Boolean(existingGroups),
|
||||
});
|
||||
if (accessConfig) {
|
||||
if (accessConfig.policy !== "allowlist") {
|
||||
next = setMatrixGroupPolicy(next, accessConfig.policy);
|
||||
} else {
|
||||
let roomKeys = accessConfig.entries;
|
||||
if (accessConfig.entries.length > 0) {
|
||||
try {
|
||||
const resolvedIds: string[] = [];
|
||||
const unresolved: string[] = [];
|
||||
for (const entry of accessConfig.entries) {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
const cleaned = trimmed.replace(/^(room|channel):/i, "").trim();
|
||||
if (cleaned.startsWith("!") && cleaned.includes(":")) {
|
||||
resolvedIds.push(cleaned);
|
||||
continue;
|
||||
}
|
||||
const matches = await listMatrixDirectoryGroupsLive({
|
||||
cfg: next,
|
||||
query: trimmed,
|
||||
limit: 10,
|
||||
});
|
||||
const exact = matches.find(
|
||||
(match) => (match.name ?? "").toLowerCase() === trimmed.toLowerCase(),
|
||||
);
|
||||
const best = exact ?? matches[0];
|
||||
if (best?.id) {
|
||||
resolvedIds.push(best.id);
|
||||
} else {
|
||||
unresolved.push(entry);
|
||||
}
|
||||
}
|
||||
roomKeys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)];
|
||||
const resolution = formatResolvedUnresolvedNote({
|
||||
resolved: resolvedIds,
|
||||
unresolved,
|
||||
});
|
||||
if (resolution) {
|
||||
await prompter.note(resolution, "Matrix rooms");
|
||||
}
|
||||
} catch (err) {
|
||||
await prompter.note(
|
||||
`Room lookup failed; keeping entries as typed. ${String(err)}`,
|
||||
"Matrix rooms",
|
||||
);
|
||||
}
|
||||
}
|
||||
next = setMatrixGroupPolicy(next, "allowlist");
|
||||
next = setMatrixGroupRooms(next, roomKeys);
|
||||
}
|
||||
}
|
||||
|
||||
return { cfg: next };
|
||||
},
|
||||
dmPolicy: matrixDmPolicy,
|
||||
groupAccess: matrixGroupAccess,
|
||||
disable: (cfg) => ({
|
||||
...(cfg as CoreConfig),
|
||||
channels: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ChannelOnboardingDmPolicy } from "../../../src/channels/plugins/onboarding-types.js";
|
||||
import { promptChannelAccessConfig } from "../../../src/channels/plugins/onboarding/channel-access.js";
|
||||
import {
|
||||
mergeAllowFromEntries,
|
||||
setTopLevelChannelAllowFrom,
|
||||
@@ -191,6 +190,96 @@ function setMSTeamsTeamsAllowlist(
|
||||
};
|
||||
}
|
||||
|
||||
function listMSTeamsGroupEntries(cfg: OpenClawConfig): string[] {
|
||||
return Object.entries(cfg.channels?.msteams?.teams ?? {}).flatMap(([teamKey, value]) => {
|
||||
const channels = value?.channels ?? {};
|
||||
const channelKeys = Object.keys(channels);
|
||||
if (channelKeys.length === 0) {
|
||||
return [teamKey];
|
||||
}
|
||||
return channelKeys.map((channelKey) => `${teamKey}/${channelKey}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveMSTeamsGroupAllowlist(params: {
|
||||
cfg: OpenClawConfig;
|
||||
entries: string[];
|
||||
prompter: Pick<WizardPrompter, "note">;
|
||||
}): Promise<Array<{ teamKey: string; channelKey?: string }>> {
|
||||
let resolvedEntries = params.entries
|
||||
.map((entry) => parseMSTeamsTeamEntry(entry))
|
||||
.filter(Boolean) as Array<{ teamKey: string; channelKey?: string }>;
|
||||
if (params.entries.length === 0 || !resolveMSTeamsCredentials(params.cfg.channels?.msteams)) {
|
||||
return resolvedEntries;
|
||||
}
|
||||
try {
|
||||
const lookups = await resolveMSTeamsChannelAllowlist({
|
||||
cfg: params.cfg,
|
||||
entries: params.entries,
|
||||
});
|
||||
const resolvedChannels = lookups.filter(
|
||||
(entry) => entry.resolved && entry.teamId && entry.channelId,
|
||||
);
|
||||
const resolvedTeams = lookups.filter(
|
||||
(entry) => entry.resolved && entry.teamId && !entry.channelId,
|
||||
);
|
||||
const unresolved = lookups.filter((entry) => !entry.resolved).map((entry) => entry.input);
|
||||
resolvedEntries = [
|
||||
...resolvedChannels.map((entry) => ({
|
||||
teamKey: entry.teamId as string,
|
||||
channelKey: entry.channelId as string,
|
||||
})),
|
||||
...resolvedTeams.map((entry) => ({
|
||||
teamKey: entry.teamId as string,
|
||||
})),
|
||||
...unresolved.map((entry) => parseMSTeamsTeamEntry(entry)).filter(Boolean),
|
||||
] as Array<{ teamKey: string; channelKey?: string }>;
|
||||
const summary: string[] = [];
|
||||
if (resolvedChannels.length > 0) {
|
||||
summary.push(
|
||||
`Resolved channels: ${resolvedChannels
|
||||
.map((entry) => entry.channelId)
|
||||
.filter(Boolean)
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (resolvedTeams.length > 0) {
|
||||
summary.push(
|
||||
`Resolved teams: ${resolvedTeams
|
||||
.map((entry) => entry.teamId)
|
||||
.filter(Boolean)
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (unresolved.length > 0) {
|
||||
summary.push(`Unresolved (kept as typed): ${unresolved.join(", ")}`);
|
||||
}
|
||||
if (summary.length > 0) {
|
||||
await params.prompter.note(summary.join("\n"), "MS Teams channels");
|
||||
}
|
||||
return resolvedEntries;
|
||||
} catch (err) {
|
||||
await params.prompter.note(
|
||||
`Channel lookup failed; keeping entries as typed. ${String(err)}`,
|
||||
"MS Teams channels",
|
||||
);
|
||||
return resolvedEntries;
|
||||
}
|
||||
}
|
||||
|
||||
const msteamsGroupAccess: NonNullable<ChannelSetupWizard["groupAccess"]> = {
|
||||
label: "MS Teams channels",
|
||||
placeholder: "Team Name/Channel Name, teamId/conversationId",
|
||||
currentPolicy: ({ cfg }) => cfg.channels?.msteams?.groupPolicy ?? "allowlist",
|
||||
currentEntries: ({ cfg }) => listMSTeamsGroupEntries(cfg),
|
||||
updatePrompt: ({ cfg }) => Boolean(cfg.channels?.msteams?.teams),
|
||||
setPolicy: ({ cfg, policy }) => setMSTeamsGroupPolicy(cfg, policy),
|
||||
resolveAllowlist: async ({ cfg, entries, prompter }) =>
|
||||
await resolveMSTeamsGroupAllowlist({ cfg, entries, prompter }),
|
||||
applyAllowlist: ({ cfg, resolved }) =>
|
||||
setMSTeamsTeamsAllowlist(cfg, resolved as Array<{ teamKey: string; channelKey?: string }>),
|
||||
};
|
||||
|
||||
const msteamsDmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "MS Teams",
|
||||
channel,
|
||||
@@ -290,96 +379,10 @@ export const msteamsSetupWizard: ChannelSetupWizard = {
|
||||
};
|
||||
}
|
||||
|
||||
const currentEntries = Object.entries(next.channels?.msteams?.teams ?? {}).flatMap(
|
||||
([teamKey, value]) => {
|
||||
const channels = value?.channels ?? {};
|
||||
const channelKeys = Object.keys(channels);
|
||||
if (channelKeys.length === 0) {
|
||||
return [teamKey];
|
||||
}
|
||||
return channelKeys.map((channelKey) => `${teamKey}/${channelKey}`);
|
||||
},
|
||||
);
|
||||
const accessConfig = await promptChannelAccessConfig({
|
||||
prompter,
|
||||
label: "MS Teams channels",
|
||||
currentPolicy: next.channels?.msteams?.groupPolicy ?? "allowlist",
|
||||
currentEntries,
|
||||
placeholder: "Team Name/Channel Name, teamId/conversationId",
|
||||
updatePrompt: Boolean(next.channels?.msteams?.teams),
|
||||
});
|
||||
if (accessConfig) {
|
||||
if (accessConfig.policy !== "allowlist") {
|
||||
next = setMSTeamsGroupPolicy(next, accessConfig.policy);
|
||||
} else {
|
||||
let entries = accessConfig.entries
|
||||
.map((entry) => parseMSTeamsTeamEntry(entry))
|
||||
.filter(Boolean) as Array<{ teamKey: string; channelKey?: string }>;
|
||||
if (accessConfig.entries.length > 0 && resolveMSTeamsCredentials(next.channels?.msteams)) {
|
||||
try {
|
||||
const resolvedEntries = await resolveMSTeamsChannelAllowlist({
|
||||
cfg: next,
|
||||
entries: accessConfig.entries,
|
||||
});
|
||||
const resolvedChannels = resolvedEntries.filter(
|
||||
(entry) => entry.resolved && entry.teamId && entry.channelId,
|
||||
);
|
||||
const resolvedTeams = resolvedEntries.filter(
|
||||
(entry) => entry.resolved && entry.teamId && !entry.channelId,
|
||||
);
|
||||
const unresolved = resolvedEntries
|
||||
.filter((entry) => !entry.resolved)
|
||||
.map((entry) => entry.input);
|
||||
|
||||
entries = [
|
||||
...resolvedChannels.map((entry) => ({
|
||||
teamKey: entry.teamId as string,
|
||||
channelKey: entry.channelId as string,
|
||||
})),
|
||||
...resolvedTeams.map((entry) => ({
|
||||
teamKey: entry.teamId as string,
|
||||
})),
|
||||
...unresolved.map((entry) => parseMSTeamsTeamEntry(entry)).filter(Boolean),
|
||||
] as Array<{ teamKey: string; channelKey?: string }>;
|
||||
|
||||
if (resolvedChannels.length > 0 || resolvedTeams.length > 0 || unresolved.length > 0) {
|
||||
const summary: string[] = [];
|
||||
if (resolvedChannels.length > 0) {
|
||||
summary.push(
|
||||
`Resolved channels: ${resolvedChannels
|
||||
.map((entry) => entry.channelId)
|
||||
.filter(Boolean)
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (resolvedTeams.length > 0) {
|
||||
summary.push(
|
||||
`Resolved teams: ${resolvedTeams
|
||||
.map((entry) => entry.teamId)
|
||||
.filter(Boolean)
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (unresolved.length > 0) {
|
||||
summary.push(`Unresolved (kept as typed): ${unresolved.join(", ")}`);
|
||||
}
|
||||
await prompter.note(summary.join("\n"), "MS Teams channels");
|
||||
}
|
||||
} catch (err) {
|
||||
await prompter.note(
|
||||
`Channel lookup failed; keeping entries as typed. ${String(err)}`,
|
||||
"MS Teams channels",
|
||||
);
|
||||
}
|
||||
}
|
||||
next = setMSTeamsGroupPolicy(next, "allowlist");
|
||||
next = setMSTeamsTeamsAllowlist(next, entries);
|
||||
}
|
||||
}
|
||||
|
||||
return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
|
||||
},
|
||||
dmPolicy: msteamsDmPolicy,
|
||||
groupAccess: msteamsGroupAccess,
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
|
||||
@@ -455,7 +455,7 @@ export function createSlackSetupWizardProxy(
|
||||
}) => {
|
||||
try {
|
||||
const wizard = (await loadWizard()).slackSetupWizard;
|
||||
if (!wizard.groupAccess) {
|
||||
if (!wizard.groupAccess?.resolveAllowlist) {
|
||||
return entries;
|
||||
}
|
||||
return await wizard.groupAccess.resolveAllowlist({
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
*/
|
||||
|
||||
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";
|
||||
@@ -228,6 +227,26 @@ function setTwitchAccessControl(
|
||||
});
|
||||
}
|
||||
|
||||
function resolveTwitchGroupPolicy(cfg: OpenClawConfig): "open" | "allowlist" | "disabled" {
|
||||
const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID);
|
||||
if (account?.allowedRoles?.includes("all")) {
|
||||
return "open";
|
||||
}
|
||||
if (account?.allowedRoles?.includes("moderator")) {
|
||||
return "allowlist";
|
||||
}
|
||||
return "disabled";
|
||||
}
|
||||
|
||||
function setTwitchGroupPolicy(
|
||||
cfg: OpenClawConfig,
|
||||
policy: "open" | "allowlist" | "disabled",
|
||||
): OpenClawConfig {
|
||||
const allowedRoles: TwitchRole[] =
|
||||
policy === "open" ? ["all"] : policy === "allowlist" ? ["moderator", "vip"] : [];
|
||||
return setTwitchAccessControl(cfg, allowedRoles, true);
|
||||
}
|
||||
|
||||
const twitchDmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "Twitch",
|
||||
channel,
|
||||
@@ -270,6 +289,24 @@ const twitchDmPolicy: ChannelOnboardingDmPolicy = {
|
||||
},
|
||||
};
|
||||
|
||||
const twitchGroupAccess: NonNullable<ChannelSetupWizard["groupAccess"]> = {
|
||||
label: "Twitch chat",
|
||||
placeholder: "",
|
||||
skipAllowlistEntries: true,
|
||||
currentPolicy: ({ cfg }) => resolveTwitchGroupPolicy(cfg as OpenClawConfig),
|
||||
currentEntries: ({ cfg }) => {
|
||||
const account = getAccountConfig(cfg as OpenClawConfig, DEFAULT_ACCOUNT_ID);
|
||||
return account?.allowFrom ?? [];
|
||||
},
|
||||
updatePrompt: ({ cfg }) => {
|
||||
const account = getAccountConfig(cfg as OpenClawConfig, DEFAULT_ACCOUNT_ID);
|
||||
return Boolean(account?.allowedRoles?.length || account?.allowFrom?.length);
|
||||
},
|
||||
setPolicy: ({ cfg, policy }) => setTwitchGroupPolicy(cfg as OpenClawConfig, policy),
|
||||
resolveAllowlist: async () => [],
|
||||
applyAllowlist: ({ cfg }) => cfg as OpenClawConfig,
|
||||
};
|
||||
|
||||
export const twitchSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
applyAccountConfig: ({ cfg }) =>
|
||||
@@ -342,37 +379,10 @@ export const twitchSetupWizard: ChannelSetupWizard = {
|
||||
? await twitchDmPolicy.promptAllowFrom({ cfg: cfgWithAccount, prompter })
|
||||
: cfgWithAccount;
|
||||
|
||||
if (!account?.allowFrom || account.allowFrom.length === 0) {
|
||||
const accessConfig = await promptChannelAccessConfig({
|
||||
prompter,
|
||||
label: "Twitch chat",
|
||||
currentPolicy: account?.allowedRoles?.includes("all")
|
||||
? "open"
|
||||
: account?.allowedRoles?.includes("moderator")
|
||||
? "allowlist"
|
||||
: "disabled",
|
||||
currentEntries: [],
|
||||
placeholder: "",
|
||||
updatePrompt: false,
|
||||
});
|
||||
|
||||
if (accessConfig) {
|
||||
const allowedRoles: TwitchRole[] =
|
||||
accessConfig.policy === "open"
|
||||
? ["all"]
|
||||
: accessConfig.policy === "allowlist"
|
||||
? ["moderator", "vip"]
|
||||
: [];
|
||||
|
||||
return {
|
||||
cfg: setTwitchAccessControl(cfgWithAllowFrom, allowedRoles, true),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { cfg: cfgWithAllowFrom };
|
||||
},
|
||||
dmPolicy: twitchDmPolicy,
|
||||
groupAccess: twitchGroupAccess,
|
||||
disable: (cfg) => {
|
||||
const twitch = (cfg.channels as Record<string, unknown>)?.twitch as
|
||||
| Record<string, unknown>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { configureChannelAccessWithAllowlist } from "./channel-access-configure.js";
|
||||
import type { ChannelAccessPolicy } from "./channel-access.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { configureChannelAccessWithAllowlist } from "./setup-group-access-configure.js";
|
||||
import type { ChannelAccessPolicy } from "./setup-group-access.js";
|
||||
|
||||
function createPrompter(params: { confirm: boolean; policy?: ChannelAccessPolicy; text?: string }) {
|
||||
return {
|
||||
@@ -89,6 +89,41 @@ describe("configureChannelAccessWithAllowlist", () => {
|
||||
expect(applyAllowlist).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("supports allowlist policies without prompting for entries", async () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const prompter = createPrompter({
|
||||
confirm: true,
|
||||
policy: "allowlist",
|
||||
});
|
||||
const setPolicy = vi.fn(
|
||||
(next: OpenClawConfig, policy: ChannelAccessPolicy): OpenClawConfig => ({
|
||||
...next,
|
||||
channels: { twitch: { groupPolicy: policy } },
|
||||
}),
|
||||
);
|
||||
const resolveAllowlist = vi.fn(async () => ["ignored"]);
|
||||
const applyAllowlist = vi.fn((params: { cfg: OpenClawConfig }) => params.cfg);
|
||||
|
||||
const next = await configureChannelAccessWithAllowlist({
|
||||
cfg,
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
prompter: prompter as any,
|
||||
label: "Twitch chat",
|
||||
currentPolicy: "disabled",
|
||||
currentEntries: [],
|
||||
placeholder: "",
|
||||
updatePrompt: false,
|
||||
skipAllowlistEntries: true,
|
||||
setPolicy,
|
||||
resolveAllowlist,
|
||||
applyAllowlist,
|
||||
});
|
||||
|
||||
expect(next.channels).toEqual({ twitch: { groupPolicy: "allowlist" } });
|
||||
expect(resolveAllowlist).not.toHaveBeenCalled();
|
||||
expect(applyAllowlist).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves allowlist entries and applies them after forcing allowlist policy", async () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const prompter = createPrompter({
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||
import { promptChannelAccessConfig, type ChannelAccessPolicy } from "./channel-access.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { WizardPrompter } from "../../wizard/prompts.js";
|
||||
import { promptChannelAccessConfig, type ChannelAccessPolicy } from "./setup-group-access.js";
|
||||
|
||||
export async function configureChannelAccessWithAllowlist<TResolved>(params: {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -10,9 +10,10 @@ export async function configureChannelAccessWithAllowlist<TResolved>(params: {
|
||||
currentEntries: string[];
|
||||
placeholder: string;
|
||||
updatePrompt: boolean;
|
||||
skipAllowlistEntries?: boolean;
|
||||
setPolicy: (cfg: OpenClawConfig, policy: ChannelAccessPolicy) => OpenClawConfig;
|
||||
resolveAllowlist: (params: { cfg: OpenClawConfig; entries: string[] }) => Promise<TResolved>;
|
||||
applyAllowlist: (params: { cfg: OpenClawConfig; resolved: TResolved }) => OpenClawConfig;
|
||||
resolveAllowlist?: (params: { cfg: OpenClawConfig; entries: string[] }) => Promise<TResolved>;
|
||||
applyAllowlist?: (params: { cfg: OpenClawConfig; resolved: TResolved }) => OpenClawConfig;
|
||||
}): Promise<OpenClawConfig> {
|
||||
let next = params.cfg;
|
||||
const accessConfig = await promptChannelAccessConfig({
|
||||
@@ -22,6 +23,7 @@ export async function configureChannelAccessWithAllowlist<TResolved>(params: {
|
||||
currentEntries: params.currentEntries,
|
||||
placeholder: params.placeholder,
|
||||
updatePrompt: params.updatePrompt,
|
||||
skipAllowlistEntries: params.skipAllowlistEntries,
|
||||
});
|
||||
if (!accessConfig) {
|
||||
return next;
|
||||
@@ -29,6 +31,9 @@ export async function configureChannelAccessWithAllowlist<TResolved>(params: {
|
||||
if (accessConfig.policy !== "allowlist") {
|
||||
return params.setPolicy(next, accessConfig.policy);
|
||||
}
|
||||
if (params.skipAllowlistEntries || !params.resolveAllowlist || !params.applyAllowlist) {
|
||||
return params.setPolicy(next, "allowlist");
|
||||
}
|
||||
const resolved = await params.resolveAllowlist({
|
||||
cfg: next,
|
||||
entries: accessConfig.entries,
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
promptChannelAccessConfig,
|
||||
promptChannelAllowlist,
|
||||
promptChannelAccessPolicy,
|
||||
} from "./channel-access.js";
|
||||
} from "./setup-group-access.js";
|
||||
|
||||
function createPrompter(params?: {
|
||||
confirm?: (options: { message: string; initialValue: boolean }) => Promise<boolean>;
|
||||
@@ -83,6 +83,27 @@ describe("promptChannelAccessPolicy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("promptChannelAccessConfig", () => {
|
||||
it("skips the allowlist text prompt when entries are policy-only", async () => {
|
||||
const prompter = createPrompter({
|
||||
confirm: async () => true,
|
||||
select: async () => "allowlist",
|
||||
text: async () => {
|
||||
throw new Error("text prompt should not run");
|
||||
},
|
||||
});
|
||||
|
||||
const result = await promptChannelAccessConfig({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
prompter: prompter as any,
|
||||
label: "Twitch chat",
|
||||
skipAllowlistEntries: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ policy: "allowlist", entries: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("promptChannelAccessConfig", () => {
|
||||
it("returns null when user skips configuration", async () => {
|
||||
const prompter = createPrompter({
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||
import { splitOnboardingEntries } from "./helpers.js";
|
||||
import type { WizardPrompter } from "../../wizard/prompts.js";
|
||||
import { splitOnboardingEntries } from "./onboarding/helpers.js";
|
||||
|
||||
export type ChannelAccessPolicy = "allowlist" | "open" | "disabled";
|
||||
|
||||
@@ -64,6 +64,7 @@ export async function promptChannelAccessConfig(params: {
|
||||
placeholder?: string;
|
||||
allowOpen?: boolean;
|
||||
allowDisabled?: boolean;
|
||||
skipAllowlistEntries?: boolean;
|
||||
defaultPrompt?: boolean;
|
||||
updatePrompt?: boolean;
|
||||
}): Promise<{ policy: ChannelAccessPolicy; entries: string[] } | null> {
|
||||
@@ -88,6 +89,9 @@ export async function promptChannelAccessConfig(params: {
|
||||
if (policy !== "allowlist") {
|
||||
return { policy, entries: [] };
|
||||
}
|
||||
if (params.skipAllowlistEntries) {
|
||||
return { policy, entries: [] };
|
||||
}
|
||||
const entries = await promptChannelAllowlist({
|
||||
prompter: params.prompter,
|
||||
label: params.label,
|
||||
@@ -8,14 +8,14 @@ import type {
|
||||
ChannelOnboardingStatus,
|
||||
ChannelOnboardingStatusContext,
|
||||
} from "./onboarding-types.js";
|
||||
import { configureChannelAccessWithAllowlist } from "./onboarding/channel-access-configure.js";
|
||||
import type { ChannelAccessPolicy } from "./onboarding/channel-access.js";
|
||||
import {
|
||||
promptResolvedAllowFrom,
|
||||
resolveAccountIdForConfigure,
|
||||
runSingleChannelSecretStep,
|
||||
splitOnboardingEntries,
|
||||
} from "./onboarding/helpers.js";
|
||||
import { configureChannelAccessWithAllowlist } from "./setup-group-access-configure.js";
|
||||
import type { ChannelAccessPolicy } from "./setup-group-access.js";
|
||||
import type { ChannelSetupInput } from "./types.core.js";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
|
||||
@@ -184,6 +184,7 @@ export type ChannelSetupWizardGroupAccess = {
|
||||
placeholder: string;
|
||||
helpTitle?: string;
|
||||
helpLines?: string[];
|
||||
skipAllowlistEntries?: boolean;
|
||||
currentPolicy: (params: { cfg: OpenClawConfig; accountId: string }) => ChannelAccessPolicy;
|
||||
currentEntries: (params: { cfg: OpenClawConfig; accountId: string }) => string[];
|
||||
updatePrompt: (params: { cfg: OpenClawConfig; accountId: string }) => boolean;
|
||||
@@ -192,14 +193,14 @@ export type ChannelSetupWizardGroupAccess = {
|
||||
accountId: string;
|
||||
policy: ChannelAccessPolicy;
|
||||
}) => OpenClawConfig;
|
||||
resolveAllowlist: (params: {
|
||||
resolveAllowlist?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
credentialValues: ChannelSetupWizardCredentialValues;
|
||||
entries: string[];
|
||||
prompter: Pick<WizardPrompter, "note">;
|
||||
}) => Promise<unknown>;
|
||||
applyAllowlist: (params: {
|
||||
applyAllowlist?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
resolved: unknown;
|
||||
@@ -757,26 +758,31 @@ export function buildChannelOnboardingAdapterFromSetupWizard(params: {
|
||||
currentEntries: access.currentEntries({ cfg: next, accountId }),
|
||||
placeholder: access.placeholder,
|
||||
updatePrompt: access.updatePrompt({ cfg: next, accountId }),
|
||||
skipAllowlistEntries: access.skipAllowlistEntries,
|
||||
setPolicy: (currentCfg, policy) =>
|
||||
access.setPolicy({
|
||||
cfg: currentCfg,
|
||||
accountId,
|
||||
policy,
|
||||
}),
|
||||
resolveAllowlist: async ({ cfg: currentCfg, entries }) =>
|
||||
await access.resolveAllowlist({
|
||||
cfg: currentCfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
}),
|
||||
applyAllowlist: ({ cfg: currentCfg, resolved }) =>
|
||||
access.applyAllowlist({
|
||||
cfg: currentCfg,
|
||||
accountId,
|
||||
resolved,
|
||||
}),
|
||||
resolveAllowlist: access.resolveAllowlist
|
||||
? async ({ cfg: currentCfg, entries }) =>
|
||||
await access.resolveAllowlist!({
|
||||
cfg: currentCfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
})
|
||||
: undefined,
|
||||
applyAllowlist: access.applyAllowlist
|
||||
? ({ cfg: currentCfg, resolved }) =>
|
||||
access.applyAllowlist!({
|
||||
cfg: currentCfg,
|
||||
accountId,
|
||||
resolved,
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js
|
||||
export {
|
||||
addWildcardAllowFrom,
|
||||
mergeAllowFromEntries,
|
||||
promptAccountId,
|
||||
splitOnboardingEntries,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
} from "../channels/plugins/onboarding/helpers.js";
|
||||
|
||||
@@ -15,7 +15,6 @@ export {
|
||||
} from "../channels/plugins/helpers.js";
|
||||
export {
|
||||
addWildcardAllowFrom,
|
||||
promptAccountId,
|
||||
setTopLevelChannelAllowFrom,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
} from "../channels/plugins/onboarding/helpers.js";
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
export type { ReplyPayload } from "../auto-reply/types.js";
|
||||
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
|
||||
export { promptAccountId } from "../channels/plugins/onboarding/helpers.js";
|
||||
export {
|
||||
applyAccountNameToChannelSection,
|
||||
patchScopedAccountConfig,
|
||||
|
||||
@@ -15,7 +15,6 @@ export {
|
||||
buildSingleChannelSecretPromptState,
|
||||
addWildcardAllowFrom,
|
||||
mergeAllowFromEntries,
|
||||
promptAccountId,
|
||||
promptSingleChannelSecretInput,
|
||||
runSingleChannelSecretStep,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
|
||||
@@ -14,7 +14,6 @@ export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
|
||||
export {
|
||||
addWildcardAllowFrom,
|
||||
mergeAllowFromEntries,
|
||||
promptAccountId,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
} from "../channels/plugins/onboarding/helpers.js";
|
||||
export {
|
||||
|
||||
Reference in New Issue
Block a user