From 36a21ae9b00be1f32937f148811d39df78e62148 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 11 Jan 2026 03:57:44 +0100 Subject: [PATCH] fix: improve telegram configuration safety --- CHANGELOG.md | 7 +++ docs/providers/telegram.md | 3 +- src/auto-reply/commands-registry.ts | 7 +++ src/auto-reply/reply/commands.ts | 24 +++++++++ src/commands/onboard-providers.ts | 77 +++++++++++++++++++++++++--- ui/src/ui/app.ts | 1 + ui/src/ui/controllers/config.test.ts | 1 + ui/src/ui/controllers/config.ts | 2 + ui/src/ui/controllers/connections.ts | 35 +++++++++---- ui/src/ui/ui-types.ts | 1 + ui/src/ui/views/connections.ts | 38 +++++++++++++- 11 files changed, 177 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c781c482..db68f7bd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2026.1.11-9 + +### Fixes +- Telegram: add `/whoami` + `/id` commands to reveal sender id for allowlists. +- Telegram/Onboarding: allow `@username` and prefixed ids in `allowFrom` prompts (with stability warning). +- Control UI: stop auto-writing `telegram.groups["*"]` and warn/confirm before enabling wildcard groups. + ## 2026.1.11-6 ### Fixes diff --git a/docs/providers/telegram.md b/docs/providers/telegram.md index cfb70ad4e..cad4b0c73 100644 --- a/docs/providers/telegram.md +++ b/docs/providers/telegram.md @@ -153,7 +153,7 @@ Send in the group: Forward any message from the group to `@userinfobot` or `@getidsbot` on Telegram to see the chat ID (negative number like `-1001234567890`). -**Tip:** For your own user ID, DM `@userinfobot` with `/start`. Useful for allowlists or debugging access control. +**Tip:** For your own user ID, DM the bot and it will reply with your user ID (pairing message), or use `/whoami` once commands are enabled. **Privacy note:** `@userinfobot` is a third-party bot. If you prefer, use gateway logs (`clawdbot logs`) or Telegram developer tools to find user/chat IDs. @@ -176,6 +176,7 @@ Private topics (DM forum mode) also include `message_thread_id`. Clawdbot: - `clawdbot pairing list telegram` - `clawdbot pairing approve telegram ` - Pairing is the default token exchange used for Telegram DMs. Details: [Pairing](/start/pairing) +- `telegram.allowFrom` accepts numeric user IDs (recommended) or `@username` entries. ### Group access diff --git a/src/auto-reply/commands-registry.ts b/src/auto-reply/commands-registry.ts index 3cfd10cb7..a420e5977 100644 --- a/src/auto-reply/commands-registry.ts +++ b/src/auto-reply/commands-registry.ts @@ -136,6 +136,12 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => { description: "Show current status.", textAlias: "/status", }), + defineChatCommand({ + key: "whoami", + nativeName: "whoami", + description: "Show your sender id.", + textAlias: "/whoami", + }), defineChatCommand({ key: "config", nativeName: "config", @@ -247,6 +253,7 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => { ]; registerAlias(commands, "status", "/usage"); + registerAlias(commands, "whoami", "/id"); registerAlias(commands, "think", "/thinking", "/t"); registerAlias(commands, "verbose", "/v"); registerAlias(commands, "reasoning", "/reason"); diff --git a/src/auto-reply/reply/commands.ts b/src/auto-reply/reply/commands.ts index f0f5c6292..d80cf9614 100644 --- a/src/auto-reply/reply/commands.ts +++ b/src/auto-reply/reply/commands.ts @@ -643,6 +643,30 @@ export async function handleCommands(params: { return { shouldContinue: false, reply }; } + const whoamiRequested = command.commandBodyNormalized === "/whoami"; + if (allowTextCommands && whoamiRequested) { + const senderId = ctx.SenderId ?? ""; + const senderUsername = ctx.SenderUsername ?? ""; + const lines = ["🧭 Identity", `Provider: ${command.provider}`]; + if (senderId) lines.push(`User id: ${senderId}`); + if (senderUsername) { + const handle = senderUsername.startsWith("@") + ? senderUsername + : `@${senderUsername}`; + lines.push(`Username: ${handle}`); + } + if (ctx.ChatType === "group" && ctx.From) { + lines.push(`Chat: ${ctx.From}`); + } + if (ctx.MessageThreadId != null) { + lines.push(`Thread: ${ctx.MessageThreadId}`); + } + if (senderId) { + lines.push(`AllowFrom: ${senderId}`); + } + return { shouldContinue: false, reply: { text: lines.join("\n") } }; + } + const configCommand = allowTextCommands ? parseConfigCommand(command.commandBodyNormalized) : null; diff --git a/src/commands/onboard-providers.ts b/src/commands/onboard-providers.ts index 30258dcce..be7d93635 100644 --- a/src/commands/onboard-providers.ts +++ b/src/commands/onboard-providers.ts @@ -470,6 +470,61 @@ async function maybeConfigureDmPolicies(params: { return cfg; } +function parseTelegramAllowFromEntries(raw: string): { + entries: string[]; + hasUsernames: boolean; + error?: string; +} { + const parts = raw + .split(/[\n,;]+/g) + .map((part) => part.trim()) + .filter(Boolean); + if (parts.length === 0) { + return { entries: [], hasUsernames: false, error: "Required" }; + } + const entries: string[] = []; + let hasUsernames = false; + for (const part of parts) { + if (part === "*") { + entries.push(part); + continue; + } + const match = part.match(/^(telegram|tg):(.+)$/i); + const value = match ? match[2]?.trim() : part; + if (!value) { + return { entries: [], hasUsernames: false, error: `Invalid entry: ${part}` }; + } + if (/^\d+$/.test(value)) { + entries.push(part); + continue; + } + if (value.startsWith("@")) { + const username = value.slice(1); + if (!/^[a-z0-9_]{5,32}$/i.test(username)) { + return { + entries: [], + hasUsernames: false, + error: `Invalid username: ${part}`, + }; + } + hasUsernames = true; + entries.push(part); + continue; + } + if (/^[a-z0-9_]{5,32}$/i.test(value)) { + hasUsernames = true; + entries.push(`@${value}`); + continue; + } + return { + entries: [], + hasUsernames: false, + error: `Invalid entry: ${part}`, + }; + } + return { entries, hasUsernames }; +} + async function promptTelegramAllowFrom(params: { cfg: ClawdbotConfig; prompter: WizardPrompter; @@ -479,22 +534,30 @@ async function promptTelegramAllowFrom(params: { const resolved = resolveTelegramAccount({ cfg, accountId }); const existingAllowFrom = resolved.config.allowFrom ?? []; const entry = await prompter.text({ - message: "Telegram allowFrom (user id)", - placeholder: "123456789", + message: "Telegram allowFrom (user id or @username)", + placeholder: "123456789, @myhandle", 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 parsed = parseTelegramAllowFromEntries(raw); + return parsed.error; }, }); - const normalized = String(entry).trim(); + const parsed = parseTelegramAllowFromEntries(String(entry).trim()); + if (parsed.hasUsernames) { + await prompter.note( + [ + "Usernames can change; numeric user IDs are more stable.", + "Tip: DM the bot and it will reply with your user ID (pairing message).", + ].join("\n"), + "Telegram allowFrom", + ); + } const merged = [ ...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean), - normalized, + ...parsed.entries, ]; const unique = [...new Set(merged)]; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index d6bd06f9a..40d4b5ad8 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -257,6 +257,7 @@ export class ClawdbotApp extends LitElement { @state() telegramForm: TelegramForm = { token: "", requireMention: true, + groupsWildcardEnabled: false, allowFrom: "", proxy: "", webhookUrl: "", diff --git a/ui/src/ui/controllers/config.test.ts b/ui/src/ui/controllers/config.test.ts index e81c91f00..58de635c0 100644 --- a/ui/src/ui/controllers/config.test.ts +++ b/ui/src/ui/controllers/config.test.ts @@ -20,6 +20,7 @@ import { const baseTelegramForm: TelegramForm = { token: "", requireMention: true, + groupsWildcardEnabled: false, allowFrom: "", proxy: "", webhookUrl: "", diff --git a/ui/src/ui/controllers/config.ts b/ui/src/ui/controllers/config.ts index 249f70bf4..aed9b29d6 100644 --- a/ui/src/ui/controllers/config.ts +++ b/ui/src/ui/controllers/config.ts @@ -129,6 +129,7 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot telegramGroups["*"] && typeof telegramGroups["*"] === "object" ? (telegramGroups["*"] as Record) : {}; + const telegramHasWildcard = Boolean(telegramGroups["*"]); const allowFrom = Array.isArray(telegram.allowFrom) ? toList(telegram.allowFrom) : typeof telegram.allowFrom === "string" @@ -141,6 +142,7 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot typeof telegramDefaultGroup.requireMention === "boolean" ? telegramDefaultGroup.requireMention : true, + groupsWildcardEnabled: telegramHasWildcard, allowFrom, proxy: typeof telegram.proxy === "string" ? telegram.proxy : "", webhookUrl: typeof telegram.webhookUrl === "string" ? telegram.webhookUrl : "", diff --git a/ui/src/ui/controllers/connections.ts b/ui/src/ui/controllers/connections.ts index 76cc68065..4b2eb3169 100644 --- a/ui/src/ui/controllers/connections.ts +++ b/ui/src/ui/controllers/connections.ts @@ -181,6 +181,15 @@ export async function saveTelegramConfig(state: ConnectionsState) { state.telegramSaving = true; state.telegramConfigStatus = null; try { + if (state.telegramForm.groupsWildcardEnabled) { + const confirmed = window.confirm( + 'Telegram groups wildcard "*" allows all groups. Continue?', + ); + if (!confirmed) { + state.telegramConfigStatus = "Save cancelled."; + return; + } + } const base = state.configSnapshot?.config ?? {}; const config = { ...base } as Record; const telegram = { ...(config.telegram ?? {}) } as Record; @@ -196,16 +205,22 @@ export async function saveTelegramConfig(state: ConnectionsState) { unknown >) : {}; - const defaultGroup = - groups["*"] && typeof groups["*"] === "object" - ? ({ ...(groups["*"] as Record) } as Record< - string, - unknown - >) - : {}; - defaultGroup.requireMention = state.telegramForm.requireMention; - groups["*"] = defaultGroup; - telegram.groups = groups; + if (state.telegramForm.groupsWildcardEnabled) { + const defaultGroup = + groups["*"] && typeof groups["*"] === "object" + ? ({ ...(groups["*"] as Record) } as Record< + string, + unknown + >) + : {}; + defaultGroup.requireMention = state.telegramForm.requireMention; + groups["*"] = defaultGroup; + telegram.groups = groups; + } else if (groups["*"]) { + delete groups["*"]; + if (Object.keys(groups).length > 0) telegram.groups = groups; + else delete telegram.groups; + } delete telegram.requireMention; const allowFrom = parseList(state.telegramForm.allowFrom); if (allowFrom.length > 0) telegram.allowFrom = allowFrom; diff --git a/ui/src/ui/ui-types.ts b/ui/src/ui/ui-types.ts index 7760ea40c..1c6e181ad 100644 --- a/ui/src/ui/ui-types.ts +++ b/ui/src/ui/ui-types.ts @@ -1,6 +1,7 @@ export type TelegramForm = { token: string; requireMention: boolean; + groupsWildcardEnabled: boolean; allowFrom: string; proxy: string; webhookUrl: string; diff --git a/ui/src/ui/views/connections.ts b/ui/src/ui/views/connections.ts index 5212a1e92..fdc42ccf1 100644 --- a/ui/src/ui/views/connections.ts +++ b/ui/src/ui/views/connections.ts @@ -356,10 +356,25 @@ function renderProvider( })} /> +