feat: expand dm allowlist onboarding
This commit is contained in:
@@ -69,6 +69,11 @@ export type ChannelOnboardingDmPolicy = {
|
||||
allowFromKey: string;
|
||||
getCurrent: (cfg: ClawdbotConfig) => DmPolicy;
|
||||
setPolicy: (cfg: ClawdbotConfig, policy: DmPolicy) => ClawdbotConfig;
|
||||
promptAllowFrom?: (params: {
|
||||
cfg: ClawdbotConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}) => Promise<ClawdbotConfig>;
|
||||
};
|
||||
|
||||
export type ChannelOnboardingAdapter = {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
resolveDiscordAccount,
|
||||
} from "../../../discord/accounts.js";
|
||||
import { normalizeDiscordSlug } from "../../../discord/monitor/allow-list.js";
|
||||
import { resolveDiscordUserAllowlist } from "../../../discord/resolve-users.js";
|
||||
import {
|
||||
resolveDiscordChannelAllowlist,
|
||||
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 = {
|
||||
label: "Discord",
|
||||
channel,
|
||||
@@ -155,6 +263,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
allowFromKey: "channels.discord.dm.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.discord?.dm?.policy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setDiscordDmPolicy(cfg, policy),
|
||||
promptAllowFrom: promptDiscordAllowFrom,
|
||||
};
|
||||
|
||||
export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
resolveDefaultIMessageAccountId,
|
||||
resolveIMessageAccount,
|
||||
} from "../../../imessage/accounts.js";
|
||||
import { normalizeIMessageHandle } from "../../../imessage/targets.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../terminal/links.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 = {
|
||||
label: "iMessage",
|
||||
channel,
|
||||
@@ -36,6 +136,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
allowFromKey: "channels.imessage.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.imessage?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setIMessageDmPolicy(cfg, policy),
|
||||
promptAllowFrom: promptIMessageAllowFrom,
|
||||
};
|
||||
|
||||
export const imessageOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
resolveSignalAccount,
|
||||
} from "../../../signal/accounts.js";
|
||||
import { formatDocsLink } from "../../../terminal/links.js";
|
||||
import { normalizeE164 } from "../../../utils.js";
|
||||
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.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 = {
|
||||
label: "Signal",
|
||||
channel,
|
||||
@@ -37,6 +139,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
allowFromKey: "channels.signal.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.signal?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setSignalDmPolicy(cfg, policy),
|
||||
promptAllowFrom: promptSignalAllowFrom,
|
||||
};
|
||||
|
||||
export const signalOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
resolveSlackAccount,
|
||||
} from "../../../slack/accounts.js";
|
||||
import { resolveSlackChannelAllowlist } from "../../../slack/resolve-channels.js";
|
||||
import { resolveSlackUserAllowlist } from "../../../slack/resolve-users.js";
|
||||
import { formatDocsLink } from "../../../terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.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 = {
|
||||
label: "Slack",
|
||||
channel,
|
||||
@@ -207,6 +313,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
allowFromKey: "channels.slack.dm.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.slack?.dm?.policy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setSlackDmPolicy(cfg, policy),
|
||||
promptAllowFrom: promptSlackAllowFrom,
|
||||
};
|
||||
|
||||
export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
|
||||
@@ -65,21 +65,59 @@ async function promptTelegramAllowFrom(params: {
|
||||
const resolved = resolveTelegramAccount({ cfg, accountId });
|
||||
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
||||
await noteTelegramUserIdHelp(prompter);
|
||||
const entry = await prompter.text({
|
||||
message: "Telegram allowFrom (user id)",
|
||||
placeholder: "123456789",
|
||||
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return "Required";
|
||||
if (!/^\d+$/.test(raw)) return "Use a numeric Telegram user id";
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
const normalized = String(entry).trim();
|
||||
|
||||
const token = resolved.token;
|
||||
if (!token) {
|
||||
await prompter.note("Telegram token missing; username lookup is unavailable.", "Telegram");
|
||||
}
|
||||
|
||||
const resolveTelegramUserId = async (raw: string): Promise<string | null> => {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return null;
|
||||
const stripped = trimmed.replace(/^(telegram|tg):/i, "").trim();
|
||||
if (/^\d+$/.test(stripped)) return stripped;
|
||||
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 = [
|
||||
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
|
||||
normalized,
|
||||
...resolvedIds,
|
||||
];
|
||||
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 = {
|
||||
label: "Telegram",
|
||||
channel,
|
||||
@@ -126,6 +180,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
allowFromKey: "channels.telegram.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.telegram?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setTelegramDmPolicy(cfg, policy),
|
||||
promptAllowFrom: promptTelegramAllowFromForAccount,
|
||||
};
|
||||
|
||||
export const telegramOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
|
||||
@@ -214,8 +214,9 @@ async function maybeConfigureDmPolicies(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
selection: ChannelChoice[];
|
||||
prompter: WizardPrompter;
|
||||
accountIdsByChannel?: Map<ChannelChoice, string>;
|
||||
}): Promise<ClawdbotConfig> {
|
||||
const { selection, prompter } = params;
|
||||
const { selection, prompter, accountIdsByChannel } = params;
|
||||
const dmPolicies = selection
|
||||
.map((channel) => getChannelOnboardingAdapter(channel)?.dmPolicy)
|
||||
.filter(Boolean) as ChannelOnboardingDmPolicy[];
|
||||
@@ -233,6 +234,7 @@ async function maybeConfigureDmPolicies(params: {
|
||||
[
|
||||
"Default: pairing (unknown DMs get a pairing code).",
|
||||
`Approve: clawdbot pairing approve ${policy.channel} <code>`,
|
||||
`Allowlist DMs: ${policy.policyKey}="allowlist" + ${policy.allowFromKey} entries.`,
|
||||
`Public DMs: ${policy.policyKey}="open" + ${policy.allowFromKey} includes "*".`,
|
||||
'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.',
|
||||
`Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`,
|
||||
@@ -243,6 +245,7 @@ async function maybeConfigureDmPolicies(params: {
|
||||
message: `${policy.label} DM policy`,
|
||||
options: [
|
||||
{ value: "pairing", label: "Pairing (recommended)" },
|
||||
{ value: "allowlist", label: "Allowlist (specific users only)" },
|
||||
{ value: "open", label: "Open (public inbound DMs)" },
|
||||
{ value: "disabled", label: "Disabled (ignore DMs)" },
|
||||
],
|
||||
@@ -255,6 +258,13 @@ async function maybeConfigureDmPolicies(params: {
|
||||
if (nextPolicy !== current) {
|
||||
cfg = policy.setPolicy(cfg, nextPolicy);
|
||||
}
|
||||
if (nextPolicy === "allowlist" && policy.promptAllowFrom) {
|
||||
cfg = await policy.promptAllowFrom({
|
||||
cfg,
|
||||
prompter,
|
||||
accountId: accountIdsByChannel?.get(policy.channel),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return cfg;
|
||||
@@ -320,10 +330,12 @@ export async function setupChannels(
|
||||
options?.initialSelection?.[0] ?? resolveQuickstartDefault(statusByChannel);
|
||||
|
||||
const shouldPromptAccountIds = options?.promptAccountIds === true;
|
||||
const accountIdsByChannel = new Map<ChannelChoice, string>();
|
||||
const recordAccount = (channel: ChannelChoice, accountId: string) => {
|
||||
options?.onAccountId?.(channel, accountId);
|
||||
const adapter = getChannelOnboardingAdapter(channel);
|
||||
adapter?.onAccountRecorded?.(accountId, options);
|
||||
accountIdsByChannel.set(channel, accountId);
|
||||
};
|
||||
|
||||
const selection: ChannelChoice[] = [];
|
||||
@@ -614,7 +626,12 @@ export async function setupChannels(
|
||||
}
|
||||
|
||||
if (!options?.skipDmPolicyPrompt) {
|
||||
next = await maybeConfigureDmPolicies({ cfg: next, selection, prompter });
|
||||
next = await maybeConfigureDmPolicies({
|
||||
cfg: next,
|
||||
selection,
|
||||
prompter,
|
||||
accountIdsByChannel,
|
||||
});
|
||||
}
|
||||
|
||||
return next;
|
||||
|
||||
Reference in New Issue
Block a user