mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 06:11:24 +00:00
feat(channels): add resolve command + defaults
This commit is contained in:
@@ -9,11 +9,9 @@ import {
|
||||
collectDiscordAuditChannelIds,
|
||||
} from "../../discord/audit.js";
|
||||
import { probeDiscord } from "../../discord/probe.js";
|
||||
import {
|
||||
listGuildChannelsDiscord,
|
||||
sendMessageDiscord,
|
||||
sendPollDiscord,
|
||||
} from "../../discord/send.js";
|
||||
import { resolveDiscordChannelAllowlist } from "../../discord/resolve-channels.js";
|
||||
import { resolveDiscordUserAllowlist } from "../../discord/resolve-users.js";
|
||||
import { sendMessageDiscord, sendPollDiscord } from "../../discord/send.js";
|
||||
import { shouldLogVerbose } from "../../globals.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { getChatChannelMeta } from "../registry.js";
|
||||
@@ -42,6 +40,10 @@ import {
|
||||
listDiscordDirectoryGroupsFromConfig,
|
||||
listDiscordDirectoryPeersFromConfig,
|
||||
} from "./directory-config.js";
|
||||
import {
|
||||
listDiscordDirectoryGroupsLive,
|
||||
listDiscordDirectoryPeersLive,
|
||||
} from "../../discord/directory-live.js";
|
||||
|
||||
const meta = getChatChannelMeta("discord");
|
||||
|
||||
@@ -123,9 +125,10 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
normalizeEntry: (raw) => raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"),
|
||||
};
|
||||
},
|
||||
collectWarnings: ({ account }) => {
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
const warnings: string[] = [];
|
||||
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
|
||||
const guildEntries = account.config.guilds ?? {};
|
||||
const guildsConfigured = Object.keys(guildEntries).length > 0;
|
||||
const channelAllowlistConfigured = guildsConfigured;
|
||||
@@ -165,29 +168,41 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
self: async () => null,
|
||||
listPeers: async (params) => listDiscordDirectoryPeersFromConfig(params),
|
||||
listGroups: async (params) => listDiscordDirectoryGroupsFromConfig(params),
|
||||
listGroupsLive: async ({ cfg, accountId, query, limit }) => {
|
||||
listPeersLive: async (params) => listDiscordDirectoryPeersLive(params),
|
||||
listGroupsLive: async (params) => listDiscordDirectoryGroupsLive(params),
|
||||
},
|
||||
resolver: {
|
||||
resolveTargets: async ({ cfg, accountId, inputs, kind }) => {
|
||||
const account = resolveDiscordAccount({ cfg, accountId });
|
||||
const q = query?.trim().toLowerCase() || "";
|
||||
const guildIds = Object.keys(account.config.guilds ?? {}).filter((id) => /^\d+$/.test(id));
|
||||
const rows: Array<{ kind: "group"; id: string; name?: string; raw?: unknown }> = [];
|
||||
for (const guildId of guildIds) {
|
||||
const channels = await listGuildChannelsDiscord(guildId, {
|
||||
accountId: account.accountId,
|
||||
});
|
||||
for (const channel of channels) {
|
||||
const name = typeof channel.name === "string" ? channel.name : undefined;
|
||||
if (q && name && !name.toLowerCase().includes(q)) continue;
|
||||
rows.push({
|
||||
kind: "group",
|
||||
id: `channel:${channel.id}`,
|
||||
name: name ?? undefined,
|
||||
raw: channel,
|
||||
});
|
||||
}
|
||||
const token = account.token?.trim();
|
||||
if (!token) {
|
||||
return inputs.map((input) => ({
|
||||
input,
|
||||
resolved: false,
|
||||
note: "missing Discord token",
|
||||
}));
|
||||
}
|
||||
const filtered = q ? rows.filter((row) => row.name?.toLowerCase().includes(q)) : rows;
|
||||
const limited = typeof limit === "number" && limit > 0 ? filtered.slice(0, limit) : filtered;
|
||||
return limited;
|
||||
if (kind === "group") {
|
||||
const resolved = await resolveDiscordChannelAllowlist({ token, entries: inputs });
|
||||
return resolved.map((entry) => ({
|
||||
input: entry.input,
|
||||
resolved: entry.resolved,
|
||||
id: entry.channelId ?? entry.guildId,
|
||||
name:
|
||||
entry.channelName ??
|
||||
entry.guildName ??
|
||||
(entry.guildId && !entry.channelId ? entry.guildId : undefined),
|
||||
note: entry.note,
|
||||
}));
|
||||
}
|
||||
const resolved = await resolveDiscordUserAllowlist({ token, entries: inputs });
|
||||
return resolved.map((entry) => ({
|
||||
input: entry.input,
|
||||
resolved: entry.resolved,
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
note: entry.note,
|
||||
}));
|
||||
},
|
||||
},
|
||||
actions: discordMessageActions,
|
||||
|
||||
@@ -95,8 +95,9 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
approveHint: formatPairingApproveHint("imessage"),
|
||||
};
|
||||
},
|
||||
collectWarnings: ({ account }) => {
|
||||
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
||||
if (groupPolicy !== "open") return [];
|
||||
return [
|
||||
`- iMessage groups: groupPolicy="open" allows any member to trigger the bot. Set channels.imessage.groupPolicy="allowlist" + channels.imessage.groupAllowFrom to restrict senders.`,
|
||||
|
||||
93
src/channels/plugins/onboarding/channel-access.ts
Normal file
93
src/channels/plugins/onboarding/channel-access.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||
|
||||
export type ChannelAccessPolicy = "allowlist" | "open" | "disabled";
|
||||
|
||||
export function parseAllowlistEntries(raw: string): string[] {
|
||||
return String(raw ?? "")
|
||||
.split(/[,\n]/g)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function formatAllowlistEntries(entries: string[]): string {
|
||||
return entries.map((entry) => entry.trim()).filter(Boolean).join(", ");
|
||||
}
|
||||
|
||||
export async function promptChannelAccessPolicy(params: {
|
||||
prompter: WizardPrompter;
|
||||
label: string;
|
||||
currentPolicy?: ChannelAccessPolicy;
|
||||
allowOpen?: boolean;
|
||||
allowDisabled?: boolean;
|
||||
}): Promise<ChannelAccessPolicy> {
|
||||
const options: Array<{ value: ChannelAccessPolicy; label: string }> = [
|
||||
{ value: "allowlist", label: "Allowlist (recommended)" },
|
||||
];
|
||||
if (params.allowOpen !== false) {
|
||||
options.push({ value: "open", label: "Open (allow all channels)" });
|
||||
}
|
||||
if (params.allowDisabled !== false) {
|
||||
options.push({ value: "disabled", label: "Disabled (block all channels)" });
|
||||
}
|
||||
const initialValue = params.currentPolicy ?? "allowlist";
|
||||
return (await params.prompter.select({
|
||||
message: `${params.label} access`,
|
||||
options,
|
||||
initialValue,
|
||||
})) as ChannelAccessPolicy;
|
||||
}
|
||||
|
||||
export async function promptChannelAllowlist(params: {
|
||||
prompter: WizardPrompter;
|
||||
label: string;
|
||||
currentEntries?: string[];
|
||||
placeholder?: string;
|
||||
}): Promise<string[]> {
|
||||
const initialValue =
|
||||
params.currentEntries && params.currentEntries.length > 0
|
||||
? formatAllowlistEntries(params.currentEntries)
|
||||
: undefined;
|
||||
const raw = await params.prompter.text({
|
||||
message: `${params.label} allowlist (comma-separated)`,
|
||||
placeholder: params.placeholder,
|
||||
initialValue,
|
||||
});
|
||||
return parseAllowlistEntries(raw);
|
||||
}
|
||||
|
||||
export async function promptChannelAccessConfig(params: {
|
||||
prompter: WizardPrompter;
|
||||
label: string;
|
||||
currentPolicy?: ChannelAccessPolicy;
|
||||
currentEntries?: string[];
|
||||
placeholder?: string;
|
||||
allowOpen?: boolean;
|
||||
allowDisabled?: boolean;
|
||||
defaultPrompt?: boolean;
|
||||
updatePrompt?: boolean;
|
||||
}): Promise<{ policy: ChannelAccessPolicy; entries: string[] } | null> {
|
||||
const hasEntries = (params.currentEntries ?? []).length > 0;
|
||||
const shouldPrompt = params.defaultPrompt ?? !hasEntries;
|
||||
const wants = await params.prompter.confirm({
|
||||
message: params.updatePrompt
|
||||
? `Update ${params.label} access?`
|
||||
: `Configure ${params.label} access?`,
|
||||
initialValue: shouldPrompt,
|
||||
});
|
||||
if (!wants) return null;
|
||||
const policy = await promptChannelAccessPolicy({
|
||||
prompter: params.prompter,
|
||||
label: params.label,
|
||||
currentPolicy: params.currentPolicy,
|
||||
allowOpen: params.allowOpen,
|
||||
allowDisabled: params.allowDisabled,
|
||||
});
|
||||
if (policy !== "allowlist") return { policy, entries: [] };
|
||||
const entries = await promptChannelAllowlist({
|
||||
prompter: params.prompter,
|
||||
label: params.label,
|
||||
currentEntries: params.currentEntries,
|
||||
placeholder: params.placeholder,
|
||||
});
|
||||
return { policy, entries };
|
||||
}
|
||||
@@ -5,10 +5,13 @@ import {
|
||||
resolveDefaultDiscordAccountId,
|
||||
resolveDiscordAccount,
|
||||
} from "../../../discord/accounts.js";
|
||||
import { normalizeDiscordSlug } from "../../../discord/monitor/allow-list.js";
|
||||
import { resolveDiscordChannelAllowlist } from "../../../discord/resolve-channels.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
|
||||
import { promptChannelAccessConfig } from "./channel-access.js";
|
||||
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
|
||||
|
||||
const channel = "discord" as const;
|
||||
@@ -46,6 +49,103 @@ async function noteDiscordTokenHelp(prompter: WizardPrompter): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
function setDiscordGroupPolicy(
|
||||
cfg: ClawdbotConfig,
|
||||
accountId: string,
|
||||
groupPolicy: "open" | "allowlist" | "disabled",
|
||||
): ClawdbotConfig {
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
discord: {
|
||||
...cfg.channels?.discord,
|
||||
enabled: true,
|
||||
groupPolicy,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
discord: {
|
||||
...cfg.channels?.discord,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...cfg.channels?.discord?.accounts,
|
||||
[accountId]: {
|
||||
...cfg.channels?.discord?.accounts?.[accountId],
|
||||
enabled: cfg.channels?.discord?.accounts?.[accountId]?.enabled ?? true,
|
||||
groupPolicy,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setDiscordGuildChannelAllowlist(
|
||||
cfg: ClawdbotConfig,
|
||||
accountId: string,
|
||||
entries: Array<{
|
||||
guildKey: string;
|
||||
channelKey?: string;
|
||||
}>,
|
||||
): ClawdbotConfig {
|
||||
const baseGuilds =
|
||||
accountId === DEFAULT_ACCOUNT_ID
|
||||
? (cfg.channels?.discord?.guilds ?? {})
|
||||
: (cfg.channels?.discord?.accounts?.[accountId]?.guilds ?? {});
|
||||
const guilds: Record<string, { channels?: Record<string, { allow: boolean }> }> = {
|
||||
...baseGuilds,
|
||||
};
|
||||
for (const entry of entries) {
|
||||
const guildKey = entry.guildKey || "*";
|
||||
const existing = guilds[guildKey] ?? {};
|
||||
if (entry.channelKey) {
|
||||
const channels = { ...(existing.channels ?? {}) };
|
||||
channels[entry.channelKey] = { allow: true };
|
||||
guilds[guildKey] = { ...existing, channels };
|
||||
} else {
|
||||
guilds[guildKey] = existing;
|
||||
}
|
||||
}
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
discord: {
|
||||
...cfg.channels?.discord,
|
||||
enabled: true,
|
||||
guilds,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
discord: {
|
||||
...cfg.channels?.discord,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...cfg.channels?.discord?.accounts,
|
||||
[accountId]: {
|
||||
...cfg.channels?.discord?.accounts?.[accountId],
|
||||
enabled: cfg.channels?.discord?.accounts?.[accountId]?.enabled ?? true,
|
||||
guilds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "Discord",
|
||||
channel,
|
||||
@@ -174,6 +274,91 @@ export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
}
|
||||
}
|
||||
|
||||
const currentEntries = Object.entries(resolvedAccount.config.guilds ?? {}).flatMap(
|
||||
([guildKey, value]) => {
|
||||
const channels = value?.channels ?? {};
|
||||
const channelKeys = Object.keys(channels);
|
||||
if (channelKeys.length === 0) return [guildKey];
|
||||
return channelKeys.map((channelKey) => `${guildKey}/${channelKey}`);
|
||||
},
|
||||
);
|
||||
const accessConfig = await promptChannelAccessConfig({
|
||||
prompter,
|
||||
label: "Discord channels",
|
||||
currentPolicy: resolvedAccount.config.groupPolicy ?? "allowlist",
|
||||
currentEntries,
|
||||
placeholder: "My Server/#general, guildId/channelId, #support",
|
||||
updatePrompt: Boolean(resolvedAccount.config.guilds),
|
||||
});
|
||||
if (accessConfig) {
|
||||
if (accessConfig.policy !== "allowlist") {
|
||||
next = setDiscordGroupPolicy(next, discordAccountId, accessConfig.policy);
|
||||
} else {
|
||||
const accountWithTokens = resolveDiscordAccount({
|
||||
cfg: next,
|
||||
accountId: discordAccountId,
|
||||
});
|
||||
let resolved = accessConfig.entries.map((input) => ({ input, resolved: false }));
|
||||
if (accountWithTokens.token && accessConfig.entries.length > 0) {
|
||||
try {
|
||||
resolved = await resolveDiscordChannelAllowlist({
|
||||
token: accountWithTokens.token,
|
||||
entries: accessConfig.entries,
|
||||
});
|
||||
const resolvedChannels = resolved.filter(
|
||||
(entry) => entry.resolved && entry.channelId,
|
||||
);
|
||||
const resolvedGuilds = resolved.filter(
|
||||
(entry) => entry.resolved && entry.guildId && !entry.channelId,
|
||||
);
|
||||
const unresolved = resolved.filter((entry) => !entry.resolved).map((entry) => entry.input);
|
||||
if (resolvedChannels.length > 0 || resolvedGuilds.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 (resolvedGuilds.length > 0) {
|
||||
summary.push(
|
||||
`Resolved guilds: ${resolvedGuilds
|
||||
.map((entry) => entry.guildId)
|
||||
.filter(Boolean)
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (unresolved.length > 0) {
|
||||
summary.push(`Unresolved (kept as typed): ${unresolved.join(", ")}`);
|
||||
}
|
||||
await prompter.note(summary.join("\n"), "Discord channels");
|
||||
}
|
||||
} catch (err) {
|
||||
await prompter.note(
|
||||
`Channel lookup failed; keeping entries as typed. ${String(err)}`,
|
||||
"Discord channels",
|
||||
);
|
||||
}
|
||||
}
|
||||
const allowlistEntries: Array<{ guildKey: string; channelKey?: string }> = [];
|
||||
for (const entry of resolved) {
|
||||
const guildKey =
|
||||
entry.guildId ??
|
||||
(entry.guildName ? normalizeDiscordSlug(entry.guildName) : undefined) ??
|
||||
"*";
|
||||
const channelKey =
|
||||
entry.channelId ??
|
||||
(entry.channelName ? normalizeDiscordSlug(entry.channelName) : undefined);
|
||||
if (!channelKey && guildKey === "*") continue;
|
||||
allowlistEntries.push({ guildKey, ...(channelKey ? { channelKey } : {}) });
|
||||
}
|
||||
next = setDiscordGroupPolicy(next, discordAccountId, "allowlist");
|
||||
next = setDiscordGuildChannelAllowlist(next, discordAccountId, allowlistEntries);
|
||||
}
|
||||
}
|
||||
|
||||
return { cfg: next, accountId: discordAccountId };
|
||||
},
|
||||
dmPolicy,
|
||||
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
resolveDefaultSlackAccountId,
|
||||
resolveSlackAccount,
|
||||
} from "../../../slack/accounts.js";
|
||||
import { resolveSlackChannelAllowlist } from "../../../slack/resolve-channels.js";
|
||||
import { formatDocsLink } from "../../../terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
|
||||
import { promptChannelAccessConfig } from "./channel-access.js";
|
||||
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
|
||||
|
||||
const channel = "slack" as const;
|
||||
@@ -121,6 +123,85 @@ async function noteSlackTokenHelp(prompter: WizardPrompter, botName: string): Pr
|
||||
);
|
||||
}
|
||||
|
||||
function setSlackGroupPolicy(
|
||||
cfg: ClawdbotConfig,
|
||||
accountId: string,
|
||||
groupPolicy: "open" | "allowlist" | "disabled",
|
||||
): ClawdbotConfig {
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
slack: {
|
||||
...cfg.channels?.slack,
|
||||
enabled: true,
|
||||
groupPolicy,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
slack: {
|
||||
...cfg.channels?.slack,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...cfg.channels?.slack?.accounts,
|
||||
[accountId]: {
|
||||
...cfg.channels?.slack?.accounts?.[accountId],
|
||||
enabled: cfg.channels?.slack?.accounts?.[accountId]?.enabled ?? true,
|
||||
groupPolicy,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setSlackChannelAllowlist(
|
||||
cfg: ClawdbotConfig,
|
||||
accountId: string,
|
||||
channelKeys: string[],
|
||||
): ClawdbotConfig {
|
||||
const channels = Object.fromEntries(
|
||||
channelKeys.map((key) => [key, { allow: true }]),
|
||||
);
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
slack: {
|
||||
...cfg.channels?.slack,
|
||||
enabled: true,
|
||||
channels,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
slack: {
|
||||
...cfg.channels?.slack,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...cfg.channels?.slack?.accounts,
|
||||
[accountId]: {
|
||||
...cfg.channels?.slack?.accounts?.[accountId],
|
||||
enabled: cfg.channels?.slack?.accounts?.[accountId]?.enabled ?? true,
|
||||
channels,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "Slack",
|
||||
channel,
|
||||
@@ -284,6 +365,68 @@ export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
}
|
||||
}
|
||||
|
||||
const accessConfig = await promptChannelAccessConfig({
|
||||
prompter,
|
||||
label: "Slack channels",
|
||||
currentPolicy: resolvedAccount.config.groupPolicy ?? "allowlist",
|
||||
currentEntries: Object.entries(resolvedAccount.config.channels ?? {})
|
||||
.filter(([, value]) => value?.allow !== false && value?.enabled !== false)
|
||||
.map(([key]) => key),
|
||||
placeholder: "#general, #private, C123",
|
||||
updatePrompt: Boolean(resolvedAccount.config.channels),
|
||||
});
|
||||
if (accessConfig) {
|
||||
if (accessConfig.policy !== "allowlist") {
|
||||
next = setSlackGroupPolicy(next, slackAccountId, accessConfig.policy);
|
||||
} else {
|
||||
let keys = accessConfig.entries;
|
||||
const accountWithTokens = resolveSlackAccount({
|
||||
cfg: next,
|
||||
accountId: slackAccountId,
|
||||
});
|
||||
if (accountWithTokens.botToken && accessConfig.entries.length > 0) {
|
||||
try {
|
||||
const resolved = await resolveSlackChannelAllowlist({
|
||||
token: accountWithTokens.botToken,
|
||||
entries: accessConfig.entries,
|
||||
});
|
||||
const resolvedKeys = resolved
|
||||
.filter((entry) => entry.resolved && entry.id)
|
||||
.map((entry) => entry.id as string);
|
||||
const unresolved = resolved
|
||||
.filter((entry) => !entry.resolved)
|
||||
.map((entry) => entry.input);
|
||||
keys = [
|
||||
...resolvedKeys,
|
||||
...unresolved.map((entry) => entry.trim()).filter(Boolean),
|
||||
];
|
||||
if (resolvedKeys.length > 0 || unresolved.length > 0) {
|
||||
await prompter.note(
|
||||
[
|
||||
resolvedKeys.length > 0
|
||||
? `Resolved: ${resolvedKeys.join(", ")}`
|
||||
: undefined,
|
||||
unresolved.length > 0
|
||||
? `Unresolved (kept as typed): ${unresolved.join(", ")}`
|
||||
: undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
"Slack channels",
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
await prompter.note(
|
||||
`Channel lookup failed; keeping entries as typed. ${String(err)}`,
|
||||
"Slack channels",
|
||||
);
|
||||
}
|
||||
}
|
||||
next = setSlackGroupPolicy(next, slackAccountId, "allowlist");
|
||||
next = setSlackChannelAllowlist(next, slackAccountId, keys);
|
||||
}
|
||||
}
|
||||
|
||||
return { cfg: next, accountId: slackAccountId };
|
||||
},
|
||||
dmPolicy,
|
||||
|
||||
@@ -108,8 +108,9 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
|
||||
};
|
||||
},
|
||||
collectWarnings: ({ account }) => {
|
||||
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
||||
if (groupPolicy !== "open") return [];
|
||||
return [
|
||||
`- Signal groups: groupPolicy="open" allows any member to trigger the bot. Set channels.signal.groupPolicy="allowlist" + channels.signal.groupAllowFrom to restrict senders.`,
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
resolveDefaultSlackAccountId,
|
||||
resolveSlackAccount,
|
||||
} from "../../slack/accounts.js";
|
||||
import { resolveSlackChannelAllowlist } from "../../slack/resolve-channels.js";
|
||||
import { resolveSlackUserAllowlist } from "../../slack/resolve-users.js";
|
||||
import { probeSlack } from "../../slack/probe.js";
|
||||
import { sendMessageSlack } from "../../slack/send.js";
|
||||
import { getChatChannelMeta } from "../registry.js";
|
||||
@@ -32,6 +34,10 @@ import {
|
||||
listSlackDirectoryGroupsFromConfig,
|
||||
listSlackDirectoryPeersFromConfig,
|
||||
} from "./directory-config.js";
|
||||
import {
|
||||
listSlackDirectoryGroupsLive,
|
||||
listSlackDirectoryPeersLive,
|
||||
} from "../../slack/directory-live.js";
|
||||
|
||||
const meta = getChatChannelMeta("slack");
|
||||
|
||||
@@ -138,9 +144,10 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
normalizeEntry: (raw) => raw.replace(/^(slack|user):/i, ""),
|
||||
};
|
||||
},
|
||||
collectWarnings: ({ account }) => {
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
const warnings: string[] = [];
|
||||
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
|
||||
const channelAllowlistConfigured =
|
||||
Boolean(account.config.channels) && Object.keys(account.config.channels ?? {}).length > 0;
|
||||
|
||||
@@ -190,6 +197,39 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
self: async () => null,
|
||||
listPeers: async (params) => listSlackDirectoryPeersFromConfig(params),
|
||||
listGroups: async (params) => listSlackDirectoryGroupsFromConfig(params),
|
||||
listPeersLive: async (params) => listSlackDirectoryPeersLive(params),
|
||||
listGroupsLive: async (params) => listSlackDirectoryGroupsLive(params),
|
||||
},
|
||||
resolver: {
|
||||
resolveTargets: async ({ cfg, accountId, inputs, kind }) => {
|
||||
const account = resolveSlackAccount({ cfg, accountId });
|
||||
const token = account.config.userToken?.trim() || account.botToken?.trim();
|
||||
if (!token) {
|
||||
return inputs.map((input) => ({
|
||||
input,
|
||||
resolved: false,
|
||||
note: "missing Slack token",
|
||||
}));
|
||||
}
|
||||
if (kind === "group") {
|
||||
const resolved = await resolveSlackChannelAllowlist({ token, entries: inputs });
|
||||
return resolved.map((entry) => ({
|
||||
input: entry.input,
|
||||
resolved: entry.resolved,
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
note: entry.archived ? "archived" : undefined,
|
||||
}));
|
||||
}
|
||||
const resolved = await resolveSlackUserAllowlist({ token, entries: inputs });
|
||||
return resolved.map((entry) => ({
|
||||
input: entry.input,
|
||||
resolved: entry.resolved,
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
note: entry.note,
|
||||
}));
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
listActions: ({ cfg }) => {
|
||||
|
||||
@@ -141,8 +141,9 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
||||
normalizeEntry: (raw) => raw.replace(/^(telegram|tg):/i, ""),
|
||||
};
|
||||
},
|
||||
collectWarnings: ({ account }) => {
|
||||
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
||||
if (groupPolicy !== "open") return [];
|
||||
const groupAllowlistConfigured =
|
||||
account.config.groups && Object.keys(account.config.groups).length > 0;
|
||||
|
||||
@@ -263,6 +263,26 @@ export type ChannelDirectoryAdapter = {
|
||||
}) => Promise<ChannelDirectoryEntry[]>;
|
||||
};
|
||||
|
||||
export type ChannelResolveKind = "user" | "group";
|
||||
|
||||
export type ChannelResolveResult = {
|
||||
input: string;
|
||||
resolved: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
export type ChannelResolverAdapter = {
|
||||
resolveTargets: (params: {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId?: string | null;
|
||||
inputs: string[];
|
||||
kind: ChannelResolveKind;
|
||||
runtime: RuntimeEnv;
|
||||
}) => Promise<ChannelResolveResult[]>;
|
||||
};
|
||||
|
||||
export type ChannelElevatedAdapter = {
|
||||
allowFromFallback?: (params: {
|
||||
cfg: ClawdbotConfig;
|
||||
|
||||
@@ -236,6 +236,7 @@ export type ChannelDirectoryEntry = {
|
||||
name?: string;
|
||||
handle?: string;
|
||||
avatarUrl?: string;
|
||||
rank?: number;
|
||||
raw?: unknown;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
ChannelCommandAdapter,
|
||||
ChannelConfigAdapter,
|
||||
ChannelDirectoryAdapter,
|
||||
ChannelResolverAdapter,
|
||||
ChannelElevatedAdapter,
|
||||
ChannelGatewayAdapter,
|
||||
ChannelGroupAdapter,
|
||||
@@ -68,6 +69,7 @@ export type ChannelPlugin<ResolvedAccount = any> = {
|
||||
threading?: ChannelThreadingAdapter;
|
||||
messaging?: ChannelMessagingAdapter;
|
||||
directory?: ChannelDirectoryAdapter;
|
||||
resolver?: ChannelResolverAdapter;
|
||||
actions?: ChannelMessageActionAdapter;
|
||||
heartbeat?: ChannelHeartbeatAdapter;
|
||||
// Channel-owned agent tools (login flows, etc.).
|
||||
|
||||
@@ -9,6 +9,9 @@ export type {
|
||||
ChannelCommandAdapter,
|
||||
ChannelConfigAdapter,
|
||||
ChannelDirectoryAdapter,
|
||||
ChannelResolveKind,
|
||||
ChannelResolveResult,
|
||||
ChannelResolverAdapter,
|
||||
ChannelElevatedAdapter,
|
||||
ChannelGatewayAdapter,
|
||||
ChannelGatewayContext,
|
||||
|
||||
@@ -149,8 +149,9 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
||||
normalizeEntry: (raw) => normalizeE164(raw),
|
||||
};
|
||||
},
|
||||
collectWarnings: ({ account }) => {
|
||||
const groupPolicy = account.groupPolicy ?? "allowlist";
|
||||
collectWarnings: ({ account, cfg }) => {
|
||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||
const groupPolicy = account.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
||||
if (groupPolicy !== "open") return [];
|
||||
const groupAllowlistConfigured =
|
||||
Boolean(account.groups) && Object.keys(account.groups ?? {}).length > 0;
|
||||
|
||||
Reference in New Issue
Block a user