fix: improve telegram configuration safety
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# 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
|
## 2026.1.11-6
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|||||||
@@ -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`).
|
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.
|
**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 list telegram`
|
||||||
- `clawdbot pairing approve telegram <CODE>`
|
- `clawdbot pairing approve telegram <CODE>`
|
||||||
- Pairing is the default token exchange used for Telegram DMs. Details: [Pairing](/start/pairing)
|
- 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
|
### Group access
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,12 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => {
|
|||||||
description: "Show current status.",
|
description: "Show current status.",
|
||||||
textAlias: "/status",
|
textAlias: "/status",
|
||||||
}),
|
}),
|
||||||
|
defineChatCommand({
|
||||||
|
key: "whoami",
|
||||||
|
nativeName: "whoami",
|
||||||
|
description: "Show your sender id.",
|
||||||
|
textAlias: "/whoami",
|
||||||
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "config",
|
key: "config",
|
||||||
nativeName: "config",
|
nativeName: "config",
|
||||||
@@ -247,6 +253,7 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
registerAlias(commands, "status", "/usage");
|
registerAlias(commands, "status", "/usage");
|
||||||
|
registerAlias(commands, "whoami", "/id");
|
||||||
registerAlias(commands, "think", "/thinking", "/t");
|
registerAlias(commands, "think", "/thinking", "/t");
|
||||||
registerAlias(commands, "verbose", "/v");
|
registerAlias(commands, "verbose", "/v");
|
||||||
registerAlias(commands, "reasoning", "/reason");
|
registerAlias(commands, "reasoning", "/reason");
|
||||||
|
|||||||
@@ -643,6 +643,30 @@ export async function handleCommands(params: {
|
|||||||
return { shouldContinue: false, reply };
|
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
|
const configCommand = allowTextCommands
|
||||||
? parseConfigCommand(command.commandBodyNormalized)
|
? parseConfigCommand(command.commandBodyNormalized)
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -470,6 +470,61 @@ async function maybeConfigureDmPolicies(params: {
|
|||||||
return cfg;
|
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: {
|
async function promptTelegramAllowFrom(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
prompter: WizardPrompter;
|
prompter: WizardPrompter;
|
||||||
@@ -479,22 +534,30 @@ async function promptTelegramAllowFrom(params: {
|
|||||||
const resolved = resolveTelegramAccount({ cfg, accountId });
|
const resolved = resolveTelegramAccount({ cfg, accountId });
|
||||||
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
||||||
const entry = await prompter.text({
|
const entry = await prompter.text({
|
||||||
message: "Telegram allowFrom (user id)",
|
message: "Telegram allowFrom (user id or @username)",
|
||||||
placeholder: "123456789",
|
placeholder: "123456789, @myhandle",
|
||||||
initialValue: existingAllowFrom[0]
|
initialValue: existingAllowFrom[0]
|
||||||
? String(existingAllowFrom[0])
|
? String(existingAllowFrom[0])
|
||||||
: undefined,
|
: undefined,
|
||||||
validate: (value) => {
|
validate: (value) => {
|
||||||
const raw = String(value ?? "").trim();
|
const raw = String(value ?? "").trim();
|
||||||
if (!raw) return "Required";
|
const parsed = parseTelegramAllowFromEntries(raw);
|
||||||
if (!/^\d+$/.test(raw)) return "Use a numeric Telegram user id";
|
return parsed.error;
|
||||||
return undefined;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
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 = [
|
const merged = [
|
||||||
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
|
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
|
||||||
normalized,
|
...parsed.entries,
|
||||||
];
|
];
|
||||||
const unique = [...new Set(merged)];
|
const unique = [...new Set(merged)];
|
||||||
|
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ export class ClawdbotApp extends LitElement {
|
|||||||
@state() telegramForm: TelegramForm = {
|
@state() telegramForm: TelegramForm = {
|
||||||
token: "",
|
token: "",
|
||||||
requireMention: true,
|
requireMention: true,
|
||||||
|
groupsWildcardEnabled: false,
|
||||||
allowFrom: "",
|
allowFrom: "",
|
||||||
proxy: "",
|
proxy: "",
|
||||||
webhookUrl: "",
|
webhookUrl: "",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
const baseTelegramForm: TelegramForm = {
|
const baseTelegramForm: TelegramForm = {
|
||||||
token: "",
|
token: "",
|
||||||
requireMention: true,
|
requireMention: true,
|
||||||
|
groupsWildcardEnabled: false,
|
||||||
allowFrom: "",
|
allowFrom: "",
|
||||||
proxy: "",
|
proxy: "",
|
||||||
webhookUrl: "",
|
webhookUrl: "",
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
|
|||||||
telegramGroups["*"] && typeof telegramGroups["*"] === "object"
|
telegramGroups["*"] && typeof telegramGroups["*"] === "object"
|
||||||
? (telegramGroups["*"] as Record<string, unknown>)
|
? (telegramGroups["*"] as Record<string, unknown>)
|
||||||
: {};
|
: {};
|
||||||
|
const telegramHasWildcard = Boolean(telegramGroups["*"]);
|
||||||
const allowFrom = Array.isArray(telegram.allowFrom)
|
const allowFrom = Array.isArray(telegram.allowFrom)
|
||||||
? toList(telegram.allowFrom)
|
? toList(telegram.allowFrom)
|
||||||
: typeof telegram.allowFrom === "string"
|
: typeof telegram.allowFrom === "string"
|
||||||
@@ -141,6 +142,7 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
|
|||||||
typeof telegramDefaultGroup.requireMention === "boolean"
|
typeof telegramDefaultGroup.requireMention === "boolean"
|
||||||
? telegramDefaultGroup.requireMention
|
? telegramDefaultGroup.requireMention
|
||||||
: true,
|
: true,
|
||||||
|
groupsWildcardEnabled: telegramHasWildcard,
|
||||||
allowFrom,
|
allowFrom,
|
||||||
proxy: typeof telegram.proxy === "string" ? telegram.proxy : "",
|
proxy: typeof telegram.proxy === "string" ? telegram.proxy : "",
|
||||||
webhookUrl: typeof telegram.webhookUrl === "string" ? telegram.webhookUrl : "",
|
webhookUrl: typeof telegram.webhookUrl === "string" ? telegram.webhookUrl : "",
|
||||||
|
|||||||
@@ -181,6 +181,15 @@ export async function saveTelegramConfig(state: ConnectionsState) {
|
|||||||
state.telegramSaving = true;
|
state.telegramSaving = true;
|
||||||
state.telegramConfigStatus = null;
|
state.telegramConfigStatus = null;
|
||||||
try {
|
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 base = state.configSnapshot?.config ?? {};
|
||||||
const config = { ...base } as Record<string, unknown>;
|
const config = { ...base } as Record<string, unknown>;
|
||||||
const telegram = { ...(config.telegram ?? {}) } as Record<string, unknown>;
|
const telegram = { ...(config.telegram ?? {}) } as Record<string, unknown>;
|
||||||
@@ -196,16 +205,22 @@ export async function saveTelegramConfig(state: ConnectionsState) {
|
|||||||
unknown
|
unknown
|
||||||
>)
|
>)
|
||||||
: {};
|
: {};
|
||||||
const defaultGroup =
|
if (state.telegramForm.groupsWildcardEnabled) {
|
||||||
groups["*"] && typeof groups["*"] === "object"
|
const defaultGroup =
|
||||||
? ({ ...(groups["*"] as Record<string, unknown>) } as Record<
|
groups["*"] && typeof groups["*"] === "object"
|
||||||
string,
|
? ({ ...(groups["*"] as Record<string, unknown>) } as Record<
|
||||||
unknown
|
string,
|
||||||
>)
|
unknown
|
||||||
: {};
|
>)
|
||||||
defaultGroup.requireMention = state.telegramForm.requireMention;
|
: {};
|
||||||
groups["*"] = defaultGroup;
|
defaultGroup.requireMention = state.telegramForm.requireMention;
|
||||||
telegram.groups = groups;
|
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;
|
delete telegram.requireMention;
|
||||||
const allowFrom = parseList(state.telegramForm.allowFrom);
|
const allowFrom = parseList(state.telegramForm.allowFrom);
|
||||||
if (allowFrom.length > 0) telegram.allowFrom = allowFrom;
|
if (allowFrom.length > 0) telegram.allowFrom = allowFrom;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export type TelegramForm = {
|
export type TelegramForm = {
|
||||||
token: string;
|
token: string;
|
||||||
requireMention: boolean;
|
requireMention: boolean;
|
||||||
|
groupsWildcardEnabled: boolean;
|
||||||
allowFrom: string;
|
allowFrom: string;
|
||||||
proxy: string;
|
proxy: string;
|
||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
|
|||||||
@@ -356,10 +356,25 @@ function renderProvider(
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Apply default group rules</span>
|
||||||
|
<select
|
||||||
|
.value=${props.telegramForm.groupsWildcardEnabled ? "yes" : "no"}
|
||||||
|
@change=${(e: Event) =>
|
||||||
|
props.onTelegramChange({
|
||||||
|
groupsWildcardEnabled:
|
||||||
|
(e.target as HTMLSelectElement).value === "yes",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<option value="no">No</option>
|
||||||
|
<option value="yes">Yes (allow all groups)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Require mention in groups</span>
|
<span>Require mention in groups</span>
|
||||||
<select
|
<select
|
||||||
.value=${props.telegramForm.requireMention ? "yes" : "no"}
|
.value=${props.telegramForm.requireMention ? "yes" : "no"}
|
||||||
|
?disabled=${!props.telegramForm.groupsWildcardEnabled}
|
||||||
@change=${(e: Event) =>
|
@change=${(e: Event) =>
|
||||||
props.onTelegramChange({
|
props.onTelegramChange({
|
||||||
requireMention: (e.target as HTMLSelectElement).value === "yes",
|
requireMention: (e.target as HTMLSelectElement).value === "yes",
|
||||||
@@ -377,7 +392,7 @@ function renderProvider(
|
|||||||
props.onTelegramChange({
|
props.onTelegramChange({
|
||||||
allowFrom: (e.target as HTMLInputElement).value,
|
allowFrom: (e.target as HTMLInputElement).value,
|
||||||
})}
|
})}
|
||||||
placeholder="123456789, @team"
|
placeholder="123456789, @team, tg:123"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
@@ -426,12 +441,33 @@ function renderProvider(
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="callout" style="margin-top: 12px;">
|
||||||
|
Allow from supports numeric user IDs (recommended) or @usernames. DM the bot
|
||||||
|
to get your ID, or run /whoami.
|
||||||
|
</div>
|
||||||
|
|
||||||
${props.telegramTokenLocked
|
${props.telegramTokenLocked
|
||||||
? html`<div class="callout" style="margin-top: 12px;">
|
? html`<div class="callout" style="margin-top: 12px;">
|
||||||
TELEGRAM_BOT_TOKEN is set in the environment. Config edits will not override it.
|
TELEGRAM_BOT_TOKEN is set in the environment. Config edits will not override it.
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
|
||||||
|
${props.telegramForm.groupsWildcardEnabled
|
||||||
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
|
This writes telegram.groups["*"] and allows all groups. Remove it
|
||||||
|
if you only want specific groups.
|
||||||
|
<div class="row" style="margin-top: 8px;">
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
@click=${() =>
|
||||||
|
props.onTelegramChange({ groupsWildcardEnabled: false })}
|
||||||
|
>
|
||||||
|
Remove wildcard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
|
||||||
${props.telegramStatus
|
${props.telegramStatus
|
||||||
? html`<div class="callout" style="margin-top: 12px;">
|
? html`<div class="callout" style="margin-top: 12px;">
|
||||||
${props.telegramStatus}
|
${props.telegramStatus}
|
||||||
|
|||||||
Reference in New Issue
Block a user