feat: expand dm allowlist onboarding
This commit is contained in:
@@ -69,6 +69,11 @@ export type ChannelOnboardingDmPolicy = {
|
|||||||
allowFromKey: string;
|
allowFromKey: string;
|
||||||
getCurrent: (cfg: ClawdbotConfig) => DmPolicy;
|
getCurrent: (cfg: ClawdbotConfig) => DmPolicy;
|
||||||
setPolicy: (cfg: ClawdbotConfig, policy: DmPolicy) => ClawdbotConfig;
|
setPolicy: (cfg: ClawdbotConfig, policy: DmPolicy) => ClawdbotConfig;
|
||||||
|
promptAllowFrom?: (params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
accountId?: string;
|
||||||
|
}) => Promise<ClawdbotConfig>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChannelOnboardingAdapter = {
|
export type ChannelOnboardingAdapter = {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
resolveDiscordAccount,
|
resolveDiscordAccount,
|
||||||
} from "../../../discord/accounts.js";
|
} from "../../../discord/accounts.js";
|
||||||
import { normalizeDiscordSlug } from "../../../discord/monitor/allow-list.js";
|
import { normalizeDiscordSlug } from "../../../discord/monitor/allow-list.js";
|
||||||
|
import { resolveDiscordUserAllowlist } from "../../../discord/resolve-users.js";
|
||||||
import {
|
import {
|
||||||
resolveDiscordChannelAllowlist,
|
resolveDiscordChannelAllowlist,
|
||||||
type DiscordChannelResolution,
|
type DiscordChannelResolution,
|
||||||
@@ -148,6 +149,113 @@ function setDiscordGuildChannelAllowlist(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setDiscordAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig {
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
discord: {
|
||||||
|
...cfg.channels?.discord,
|
||||||
|
dm: {
|
||||||
|
...cfg.channels?.discord?.dm,
|
||||||
|
enabled: cfg.channels?.discord?.dm?.enabled ?? true,
|
||||||
|
allowFrom,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDiscordAllowFromInput(raw: string): string[] {
|
||||||
|
return raw
|
||||||
|
.split(/[\n,;]+/g)
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptDiscordAllowFrom(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
accountId?: string;
|
||||||
|
}): Promise<ClawdbotConfig> {
|
||||||
|
const accountId =
|
||||||
|
params.accountId && normalizeAccountId(params.accountId)
|
||||||
|
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID
|
||||||
|
: resolveDefaultDiscordAccountId(params.cfg);
|
||||||
|
const resolved = resolveDiscordAccount({ cfg: params.cfg, accountId });
|
||||||
|
const token = resolved.token;
|
||||||
|
const existing = params.cfg.channels?.discord?.dm?.allowFrom ?? [];
|
||||||
|
await params.prompter.note(
|
||||||
|
[
|
||||||
|
"Allowlist Discord DMs by username (we resolve to user ids).",
|
||||||
|
"Examples:",
|
||||||
|
"- 123456789012345678",
|
||||||
|
"- @alice",
|
||||||
|
"- alice#1234",
|
||||||
|
"Multiple entries: comma-separated.",
|
||||||
|
`Docs: ${formatDocsLink("/discord", "discord")}`,
|
||||||
|
].join("\n"),
|
||||||
|
"Discord allowlist",
|
||||||
|
);
|
||||||
|
|
||||||
|
const parseInputs = (value: string) => parseDiscordAllowFromInput(value);
|
||||||
|
const parseId = (value: string) => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
const mention = trimmed.match(/^<@!?(\d+)>$/);
|
||||||
|
if (mention) return mention[1];
|
||||||
|
const prefixed = trimmed.replace(/^(user:|discord:)/i, "");
|
||||||
|
if (/^\d+$/.test(prefixed)) return prefixed;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const entry = await params.prompter.text({
|
||||||
|
message: "Discord allowFrom (usernames or ids)",
|
||||||
|
placeholder: "@alice, 123456789012345678",
|
||||||
|
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||||
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||||
|
});
|
||||||
|
const parts = parseInputs(String(entry));
|
||||||
|
if (!token) {
|
||||||
|
const ids = parts.map(parseId).filter(Boolean) as string[];
|
||||||
|
if (ids.length !== parts.length) {
|
||||||
|
await params.prompter.note(
|
||||||
|
"Bot token missing; use numeric user ids (or mention form) only.",
|
||||||
|
"Discord allowlist",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const unique = [...new Set([...existing.map((v) => String(v).trim()), ...ids])].filter(
|
||||||
|
Boolean,
|
||||||
|
);
|
||||||
|
return setDiscordAllowFrom(params.cfg, unique);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await resolveDiscordUserAllowlist({
|
||||||
|
token,
|
||||||
|
entries: parts,
|
||||||
|
}).catch(() => null);
|
||||||
|
if (!results) {
|
||||||
|
await params.prompter.note("Failed to resolve usernames. Try again.", "Discord allowlist");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const unresolved = results.filter((res) => !res.resolved || !res.id);
|
||||||
|
if (unresolved.length > 0) {
|
||||||
|
await params.prompter.note(
|
||||||
|
`Could not resolve: ${unresolved.map((res) => res.input).join(", ")}`,
|
||||||
|
"Discord allowlist",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ids = results.map((res) => res.id as string);
|
||||||
|
const unique = [
|
||||||
|
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids]),
|
||||||
|
];
|
||||||
|
return setDiscordAllowFrom(params.cfg, unique);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||||
label: "Discord",
|
label: "Discord",
|
||||||
channel,
|
channel,
|
||||||
@@ -155,6 +263,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|||||||
allowFromKey: "channels.discord.dm.allowFrom",
|
allowFromKey: "channels.discord.dm.allowFrom",
|
||||||
getCurrent: (cfg) => cfg.channels?.discord?.dm?.policy ?? "pairing",
|
getCurrent: (cfg) => cfg.channels?.discord?.dm?.policy ?? "pairing",
|
||||||
setPolicy: (cfg, policy) => setDiscordDmPolicy(cfg, policy),
|
setPolicy: (cfg, policy) => setDiscordDmPolicy(cfg, policy),
|
||||||
|
promptAllowFrom: promptDiscordAllowFrom,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
|
export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
resolveDefaultIMessageAccountId,
|
resolveDefaultIMessageAccountId,
|
||||||
resolveIMessageAccount,
|
resolveIMessageAccount,
|
||||||
} from "../../../imessage/accounts.js";
|
} from "../../../imessage/accounts.js";
|
||||||
|
import { normalizeIMessageHandle } from "../../../imessage/targets.js";
|
||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
|
||||||
import { formatDocsLink } from "../../../terminal/links.js";
|
import { formatDocsLink } from "../../../terminal/links.js";
|
||||||
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
|
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
|
||||||
@@ -29,6 +30,105 @@ function setIMessageDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setIMessageAllowFrom(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
accountId: string,
|
||||||
|
allowFrom: string[],
|
||||||
|
): ClawdbotConfig {
|
||||||
|
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
imessage: {
|
||||||
|
...cfg.channels?.imessage,
|
||||||
|
allowFrom,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
imessage: {
|
||||||
|
...cfg.channels?.imessage,
|
||||||
|
accounts: {
|
||||||
|
...cfg.channels?.imessage?.accounts,
|
||||||
|
[accountId]: {
|
||||||
|
...cfg.channels?.imessage?.accounts?.[accountId],
|
||||||
|
allowFrom,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIMessageAllowFromInput(raw: string): string[] {
|
||||||
|
return raw
|
||||||
|
.split(/[\n,;]+/g)
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptIMessageAllowFrom(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
accountId?: string;
|
||||||
|
}): Promise<ClawdbotConfig> {
|
||||||
|
const accountId =
|
||||||
|
params.accountId && normalizeAccountId(params.accountId)
|
||||||
|
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID
|
||||||
|
: resolveDefaultIMessageAccountId(params.cfg);
|
||||||
|
const resolved = resolveIMessageAccount({ cfg: params.cfg, accountId });
|
||||||
|
const existing = resolved.config.allowFrom ?? [];
|
||||||
|
await params.prompter.note(
|
||||||
|
[
|
||||||
|
"Allowlist iMessage DMs by handle or chat target.",
|
||||||
|
"Examples:",
|
||||||
|
"- +15555550123",
|
||||||
|
"- user@example.com",
|
||||||
|
"- chat_id:123",
|
||||||
|
"- chat_guid:... or chat_identifier:...",
|
||||||
|
"Multiple entries: comma-separated.",
|
||||||
|
`Docs: ${formatDocsLink("/imessage", "imessage")}`,
|
||||||
|
].join("\n"),
|
||||||
|
"iMessage allowlist",
|
||||||
|
);
|
||||||
|
const entry = await params.prompter.text({
|
||||||
|
message: "iMessage allowFrom (handle or chat_id)",
|
||||||
|
placeholder: "+15555550123, user@example.com, chat_id:123",
|
||||||
|
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||||
|
validate: (value) => {
|
||||||
|
const raw = String(value ?? "").trim();
|
||||||
|
if (!raw) return "Required";
|
||||||
|
const parts = parseIMessageAllowFromInput(raw);
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part === "*") continue;
|
||||||
|
if (part.toLowerCase().startsWith("chat_id:")) {
|
||||||
|
const id = part.slice("chat_id:".length).trim();
|
||||||
|
if (!/^\d+$/.test(id)) return `Invalid chat_id: ${part}`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (part.toLowerCase().startsWith("chat_guid:")) {
|
||||||
|
if (!part.slice("chat_guid:".length).trim()) return "Invalid chat_guid entry";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (part.toLowerCase().startsWith("chat_identifier:")) {
|
||||||
|
if (!part.slice("chat_identifier:".length).trim()) return "Invalid chat_identifier entry";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!normalizeIMessageHandle(part)) return `Invalid handle: ${part}`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const parts = parseIMessageAllowFromInput(String(entry));
|
||||||
|
const unique = [...new Set(parts)];
|
||||||
|
return setIMessageAllowFrom(params.cfg, accountId, unique);
|
||||||
|
}
|
||||||
|
|
||||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||||
label: "iMessage",
|
label: "iMessage",
|
||||||
channel,
|
channel,
|
||||||
@@ -36,6 +136,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|||||||
allowFromKey: "channels.imessage.allowFrom",
|
allowFromKey: "channels.imessage.allowFrom",
|
||||||
getCurrent: (cfg) => cfg.channels?.imessage?.dmPolicy ?? "pairing",
|
getCurrent: (cfg) => cfg.channels?.imessage?.dmPolicy ?? "pairing",
|
||||||
setPolicy: (cfg, policy) => setIMessageDmPolicy(cfg, policy),
|
setPolicy: (cfg, policy) => setIMessageDmPolicy(cfg, policy),
|
||||||
|
promptAllowFrom: promptIMessageAllowFrom,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const imessageOnboardingAdapter: ChannelOnboardingAdapter = {
|
export const imessageOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
resolveSignalAccount,
|
resolveSignalAccount,
|
||||||
} from "../../../signal/accounts.js";
|
} from "../../../signal/accounts.js";
|
||||||
import { formatDocsLink } from "../../../terminal/links.js";
|
import { formatDocsLink } from "../../../terminal/links.js";
|
||||||
|
import { normalizeE164 } from "../../../utils.js";
|
||||||
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
|
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
|
||||||
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
|
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
|
||||||
|
|
||||||
@@ -30,6 +31,107 @@ function setSignalDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSignalAllowFrom(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
accountId: string,
|
||||||
|
allowFrom: string[],
|
||||||
|
): ClawdbotConfig {
|
||||||
|
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
signal: {
|
||||||
|
...cfg.channels?.signal,
|
||||||
|
allowFrom,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
signal: {
|
||||||
|
...cfg.channels?.signal,
|
||||||
|
accounts: {
|
||||||
|
...cfg.channels?.signal?.accounts,
|
||||||
|
[accountId]: {
|
||||||
|
...cfg.channels?.signal?.accounts?.[accountId],
|
||||||
|
allowFrom,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSignalAllowFromInput(raw: string): string[] {
|
||||||
|
return raw
|
||||||
|
.split(/[\n,;]+/g)
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUuidLike(value: string): boolean {
|
||||||
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptSignalAllowFrom(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
accountId?: string;
|
||||||
|
}): Promise<ClawdbotConfig> {
|
||||||
|
const accountId =
|
||||||
|
params.accountId && normalizeAccountId(params.accountId)
|
||||||
|
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID
|
||||||
|
: resolveDefaultSignalAccountId(params.cfg);
|
||||||
|
const resolved = resolveSignalAccount({ cfg: params.cfg, accountId });
|
||||||
|
const existing = resolved.config.allowFrom ?? [];
|
||||||
|
await params.prompter.note(
|
||||||
|
[
|
||||||
|
"Allowlist Signal DMs by sender id.",
|
||||||
|
"Examples:",
|
||||||
|
"- +15555550123",
|
||||||
|
"- uuid:123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
"Multiple entries: comma-separated.",
|
||||||
|
`Docs: ${formatDocsLink("/signal", "signal")}`,
|
||||||
|
].join("\n"),
|
||||||
|
"Signal allowlist",
|
||||||
|
);
|
||||||
|
const entry = await params.prompter.text({
|
||||||
|
message: "Signal allowFrom (E.164 or uuid)",
|
||||||
|
placeholder: "+15555550123, uuid:123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||||
|
validate: (value) => {
|
||||||
|
const raw = String(value ?? "").trim();
|
||||||
|
if (!raw) return "Required";
|
||||||
|
const parts = parseSignalAllowFromInput(raw);
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part === "*") continue;
|
||||||
|
if (part.toLowerCase().startsWith("uuid:")) {
|
||||||
|
if (!part.slice("uuid:".length).trim()) return "Invalid uuid entry";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isUuidLike(part)) continue;
|
||||||
|
if (!normalizeE164(part)) return `Invalid entry: ${part}`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const parts = parseSignalAllowFromInput(String(entry));
|
||||||
|
const normalized = parts
|
||||||
|
.map((part) => {
|
||||||
|
if (part === "*") return "*";
|
||||||
|
if (part.toLowerCase().startsWith("uuid:")) return `uuid:${part.slice(5).trim()}`;
|
||||||
|
if (isUuidLike(part)) return `uuid:${part}`;
|
||||||
|
return normalizeE164(part);
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
const unique = [...new Set(normalized)];
|
||||||
|
return setSignalAllowFrom(params.cfg, accountId, unique);
|
||||||
|
}
|
||||||
|
|
||||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||||
label: "Signal",
|
label: "Signal",
|
||||||
channel,
|
channel,
|
||||||
@@ -37,6 +139,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|||||||
allowFromKey: "channels.signal.allowFrom",
|
allowFromKey: "channels.signal.allowFrom",
|
||||||
getCurrent: (cfg) => cfg.channels?.signal?.dmPolicy ?? "pairing",
|
getCurrent: (cfg) => cfg.channels?.signal?.dmPolicy ?? "pairing",
|
||||||
setPolicy: (cfg, policy) => setSignalDmPolicy(cfg, policy),
|
setPolicy: (cfg, policy) => setSignalDmPolicy(cfg, policy),
|
||||||
|
promptAllowFrom: promptSignalAllowFrom,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const signalOnboardingAdapter: ChannelOnboardingAdapter = {
|
export const signalOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
resolveSlackAccount,
|
resolveSlackAccount,
|
||||||
} from "../../../slack/accounts.js";
|
} from "../../../slack/accounts.js";
|
||||||
import { resolveSlackChannelAllowlist } from "../../../slack/resolve-channels.js";
|
import { resolveSlackChannelAllowlist } from "../../../slack/resolve-channels.js";
|
||||||
|
import { resolveSlackUserAllowlist } from "../../../slack/resolve-users.js";
|
||||||
import { formatDocsLink } from "../../../terminal/links.js";
|
import { formatDocsLink } from "../../../terminal/links.js";
|
||||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||||
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
|
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
|
||||||
@@ -200,6 +201,111 @@ function setSlackChannelAllowlist(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSlackAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig {
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
slack: {
|
||||||
|
...cfg.channels?.slack,
|
||||||
|
dm: {
|
||||||
|
...cfg.channels?.slack?.dm,
|
||||||
|
enabled: cfg.channels?.slack?.dm?.enabled ?? true,
|
||||||
|
allowFrom,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSlackAllowFromInput(raw: string): string[] {
|
||||||
|
return raw
|
||||||
|
.split(/[\n,;]+/g)
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptSlackAllowFrom(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
accountId?: string;
|
||||||
|
}): Promise<ClawdbotConfig> {
|
||||||
|
const accountId =
|
||||||
|
params.accountId && normalizeAccountId(params.accountId)
|
||||||
|
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID
|
||||||
|
: resolveDefaultSlackAccountId(params.cfg);
|
||||||
|
const resolved = resolveSlackAccount({ cfg: params.cfg, accountId });
|
||||||
|
const token = resolved.config.userToken ?? resolved.config.botToken ?? "";
|
||||||
|
const existing = params.cfg.channels?.slack?.dm?.allowFrom ?? [];
|
||||||
|
await params.prompter.note(
|
||||||
|
[
|
||||||
|
"Allowlist Slack DMs by username (we resolve to user ids).",
|
||||||
|
"Examples:",
|
||||||
|
"- U12345678",
|
||||||
|
"- @alice",
|
||||||
|
"Multiple entries: comma-separated.",
|
||||||
|
`Docs: ${formatDocsLink("/slack", "slack")}`,
|
||||||
|
].join("\n"),
|
||||||
|
"Slack allowlist",
|
||||||
|
);
|
||||||
|
const parseInputs = (value: string) => parseSlackAllowFromInput(value);
|
||||||
|
const parseId = (value: string) => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
const mention = trimmed.match(/^<@([A-Z0-9]+)>$/i);
|
||||||
|
if (mention) return mention[1]?.toUpperCase();
|
||||||
|
const prefixed = trimmed.replace(/^(slack:|user:)/i, "");
|
||||||
|
if (/^[A-Z][A-Z0-9]+$/i.test(prefixed)) return prefixed.toUpperCase();
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const entry = await params.prompter.text({
|
||||||
|
message: "Slack allowFrom (usernames or ids)",
|
||||||
|
placeholder: "@alice, U12345678",
|
||||||
|
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||||
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||||
|
});
|
||||||
|
const parts = parseInputs(String(entry));
|
||||||
|
if (!token) {
|
||||||
|
const ids = parts.map(parseId).filter(Boolean) as string[];
|
||||||
|
if (ids.length !== parts.length) {
|
||||||
|
await params.prompter.note(
|
||||||
|
"Slack token missing; use user ids (or mention form) only.",
|
||||||
|
"Slack allowlist",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const unique = [...new Set([...existing.map((v) => String(v).trim()), ...ids])].filter(
|
||||||
|
Boolean,
|
||||||
|
);
|
||||||
|
return setSlackAllowFrom(params.cfg, unique);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await resolveSlackUserAllowlist({
|
||||||
|
token,
|
||||||
|
entries: parts,
|
||||||
|
}).catch(() => null);
|
||||||
|
if (!results) {
|
||||||
|
await params.prompter.note("Failed to resolve usernames. Try again.", "Slack allowlist");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const unresolved = results.filter((res) => !res.resolved || !res.id);
|
||||||
|
if (unresolved.length > 0) {
|
||||||
|
await params.prompter.note(
|
||||||
|
`Could not resolve: ${unresolved.map((res) => res.input).join(", ")}`,
|
||||||
|
"Slack allowlist",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ids = results.map((res) => res.id as string);
|
||||||
|
const unique = [
|
||||||
|
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids]),
|
||||||
|
];
|
||||||
|
return setSlackAllowFrom(params.cfg, unique);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||||
label: "Slack",
|
label: "Slack",
|
||||||
channel,
|
channel,
|
||||||
@@ -207,6 +313,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|||||||
allowFromKey: "channels.slack.dm.allowFrom",
|
allowFromKey: "channels.slack.dm.allowFrom",
|
||||||
getCurrent: (cfg) => cfg.channels?.slack?.dm?.policy ?? "pairing",
|
getCurrent: (cfg) => cfg.channels?.slack?.dm?.policy ?? "pairing",
|
||||||
setPolicy: (cfg, policy) => setSlackDmPolicy(cfg, policy),
|
setPolicy: (cfg, policy) => setSlackDmPolicy(cfg, policy),
|
||||||
|
promptAllowFrom: promptSlackAllowFrom,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
|
export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||||
|
|||||||
@@ -65,21 +65,59 @@ async function promptTelegramAllowFrom(params: {
|
|||||||
const resolved = resolveTelegramAccount({ cfg, accountId });
|
const resolved = resolveTelegramAccount({ cfg, accountId });
|
||||||
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
||||||
await noteTelegramUserIdHelp(prompter);
|
await noteTelegramUserIdHelp(prompter);
|
||||||
const entry = await prompter.text({
|
|
||||||
message: "Telegram allowFrom (user id)",
|
const token = resolved.token;
|
||||||
placeholder: "123456789",
|
if (!token) {
|
||||||
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
await prompter.note("Telegram token missing; username lookup is unavailable.", "Telegram");
|
||||||
validate: (value) => {
|
}
|
||||||
const raw = String(value ?? "").trim();
|
|
||||||
if (!raw) return "Required";
|
const resolveTelegramUserId = async (raw: string): Promise<string | null> => {
|
||||||
if (!/^\d+$/.test(raw)) return "Use a numeric Telegram user id";
|
const trimmed = raw.trim();
|
||||||
return undefined;
|
if (!trimmed) return null;
|
||||||
},
|
const stripped = trimmed.replace(/^(telegram|tg):/i, "").trim();
|
||||||
});
|
if (/^\d+$/.test(stripped)) return stripped;
|
||||||
const normalized = String(entry).trim();
|
if (!token) return null;
|
||||||
|
const username = stripped.startsWith("@") ? stripped : `@${stripped}`;
|
||||||
|
const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`;
|
||||||
|
const res = await fetch(url);
|
||||||
|
const data = (await res.json().catch(() => null)) as
|
||||||
|
| { ok?: boolean; result?: { id?: number | string } }
|
||||||
|
| null;
|
||||||
|
const id = data?.ok ? data?.result?.id : undefined;
|
||||||
|
if (typeof id === "number" || typeof id === "string") return String(id);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseInput = (value: string) =>
|
||||||
|
value
|
||||||
|
.split(/[\n,;]+/g)
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
let resolvedIds: string[] = [];
|
||||||
|
while (resolvedIds.length === 0) {
|
||||||
|
const entry = await prompter.text({
|
||||||
|
message: "Telegram allowFrom (username or user id)",
|
||||||
|
placeholder: "@username",
|
||||||
|
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
||||||
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||||
|
});
|
||||||
|
const parts = parseInput(String(entry));
|
||||||
|
const results = await Promise.all(parts.map((part) => resolveTelegramUserId(part)));
|
||||||
|
const unresolved = parts.filter((_, idx) => !results[idx]);
|
||||||
|
if (unresolved.length > 0) {
|
||||||
|
await prompter.note(
|
||||||
|
`Could not resolve: ${unresolved.join(", ")}. Use @username or numeric id.`,
|
||||||
|
"Telegram allowlist",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
resolvedIds = results.filter(Boolean) as string[];
|
||||||
|
}
|
||||||
|
|
||||||
const merged = [
|
const merged = [
|
||||||
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
|
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
|
||||||
normalized,
|
...resolvedIds,
|
||||||
];
|
];
|
||||||
const unique = [...new Set(merged)];
|
const unique = [...new Set(merged)];
|
||||||
|
|
||||||
@@ -119,6 +157,22 @@ async function promptTelegramAllowFrom(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function promptTelegramAllowFromForAccount(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
accountId?: string;
|
||||||
|
}): Promise<ClawdbotConfig> {
|
||||||
|
const accountId =
|
||||||
|
params.accountId && normalizeAccountId(params.accountId)
|
||||||
|
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID
|
||||||
|
: resolveDefaultTelegramAccountId(params.cfg);
|
||||||
|
return promptTelegramAllowFrom({
|
||||||
|
cfg: params.cfg,
|
||||||
|
prompter: params.prompter,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||||
label: "Telegram",
|
label: "Telegram",
|
||||||
channel,
|
channel,
|
||||||
@@ -126,6 +180,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|||||||
allowFromKey: "channels.telegram.allowFrom",
|
allowFromKey: "channels.telegram.allowFrom",
|
||||||
getCurrent: (cfg) => cfg.channels?.telegram?.dmPolicy ?? "pairing",
|
getCurrent: (cfg) => cfg.channels?.telegram?.dmPolicy ?? "pairing",
|
||||||
setPolicy: (cfg, policy) => setTelegramDmPolicy(cfg, policy),
|
setPolicy: (cfg, policy) => setTelegramDmPolicy(cfg, policy),
|
||||||
|
promptAllowFrom: promptTelegramAllowFromForAccount,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const telegramOnboardingAdapter: ChannelOnboardingAdapter = {
|
export const telegramOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||||
|
|||||||
@@ -214,8 +214,9 @@ async function maybeConfigureDmPolicies(params: {
|
|||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
selection: ChannelChoice[];
|
selection: ChannelChoice[];
|
||||||
prompter: WizardPrompter;
|
prompter: WizardPrompter;
|
||||||
|
accountIdsByChannel?: Map<ChannelChoice, string>;
|
||||||
}): Promise<ClawdbotConfig> {
|
}): Promise<ClawdbotConfig> {
|
||||||
const { selection, prompter } = params;
|
const { selection, prompter, accountIdsByChannel } = params;
|
||||||
const dmPolicies = selection
|
const dmPolicies = selection
|
||||||
.map((channel) => getChannelOnboardingAdapter(channel)?.dmPolicy)
|
.map((channel) => getChannelOnboardingAdapter(channel)?.dmPolicy)
|
||||||
.filter(Boolean) as ChannelOnboardingDmPolicy[];
|
.filter(Boolean) as ChannelOnboardingDmPolicy[];
|
||||||
@@ -233,6 +234,7 @@ async function maybeConfigureDmPolicies(params: {
|
|||||||
[
|
[
|
||||||
"Default: pairing (unknown DMs get a pairing code).",
|
"Default: pairing (unknown DMs get a pairing code).",
|
||||||
`Approve: clawdbot pairing approve ${policy.channel} <code>`,
|
`Approve: clawdbot pairing approve ${policy.channel} <code>`,
|
||||||
|
`Allowlist DMs: ${policy.policyKey}="allowlist" + ${policy.allowFromKey} entries.`,
|
||||||
`Public DMs: ${policy.policyKey}="open" + ${policy.allowFromKey} includes "*".`,
|
`Public DMs: ${policy.policyKey}="open" + ${policy.allowFromKey} includes "*".`,
|
||||||
'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.',
|
'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.',
|
||||||
`Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`,
|
`Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`,
|
||||||
@@ -243,6 +245,7 @@ async function maybeConfigureDmPolicies(params: {
|
|||||||
message: `${policy.label} DM policy`,
|
message: `${policy.label} DM policy`,
|
||||||
options: [
|
options: [
|
||||||
{ value: "pairing", label: "Pairing (recommended)" },
|
{ value: "pairing", label: "Pairing (recommended)" },
|
||||||
|
{ value: "allowlist", label: "Allowlist (specific users only)" },
|
||||||
{ value: "open", label: "Open (public inbound DMs)" },
|
{ value: "open", label: "Open (public inbound DMs)" },
|
||||||
{ value: "disabled", label: "Disabled (ignore DMs)" },
|
{ value: "disabled", label: "Disabled (ignore DMs)" },
|
||||||
],
|
],
|
||||||
@@ -255,6 +258,13 @@ async function maybeConfigureDmPolicies(params: {
|
|||||||
if (nextPolicy !== current) {
|
if (nextPolicy !== current) {
|
||||||
cfg = policy.setPolicy(cfg, nextPolicy);
|
cfg = policy.setPolicy(cfg, nextPolicy);
|
||||||
}
|
}
|
||||||
|
if (nextPolicy === "allowlist" && policy.promptAllowFrom) {
|
||||||
|
cfg = await policy.promptAllowFrom({
|
||||||
|
cfg,
|
||||||
|
prompter,
|
||||||
|
accountId: accountIdsByChannel?.get(policy.channel),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
@@ -320,10 +330,12 @@ export async function setupChannels(
|
|||||||
options?.initialSelection?.[0] ?? resolveQuickstartDefault(statusByChannel);
|
options?.initialSelection?.[0] ?? resolveQuickstartDefault(statusByChannel);
|
||||||
|
|
||||||
const shouldPromptAccountIds = options?.promptAccountIds === true;
|
const shouldPromptAccountIds = options?.promptAccountIds === true;
|
||||||
|
const accountIdsByChannel = new Map<ChannelChoice, string>();
|
||||||
const recordAccount = (channel: ChannelChoice, accountId: string) => {
|
const recordAccount = (channel: ChannelChoice, accountId: string) => {
|
||||||
options?.onAccountId?.(channel, accountId);
|
options?.onAccountId?.(channel, accountId);
|
||||||
const adapter = getChannelOnboardingAdapter(channel);
|
const adapter = getChannelOnboardingAdapter(channel);
|
||||||
adapter?.onAccountRecorded?.(accountId, options);
|
adapter?.onAccountRecorded?.(accountId, options);
|
||||||
|
accountIdsByChannel.set(channel, accountId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selection: ChannelChoice[] = [];
|
const selection: ChannelChoice[] = [];
|
||||||
@@ -614,7 +626,12 @@ export async function setupChannels(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!options?.skipDmPolicyPrompt) {
|
if (!options?.skipDmPolicyPrompt) {
|
||||||
next = await maybeConfigureDmPolicies({ cfg: next, selection, prompter });
|
next = await maybeConfigureDmPolicies({
|
||||||
|
cfg: next,
|
||||||
|
selection,
|
||||||
|
prompter,
|
||||||
|
accountIdsByChannel,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
|
|||||||
Reference in New Issue
Block a user