Telegram-user: add onboarding + docs

This commit is contained in:
Muhammed Mukhthar CM 2026-01-23 04:33:46 +00:00
parent a4bea14162
commit b953e0bff8
4 changed files with 407 additions and 0 deletions

View File

@ -13,6 +13,7 @@ Text is supported everywhere; media and reactions vary by channel.
- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
- [Telegram User](/channels/telegram-user) — MTProto user account; DM-only for now (plugin, installed separately).
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook.

View File

@ -0,0 +1,68 @@
---
summary: "Connect a Telegram user account via MTProto (DM-only)"
---
# Telegram User
Telegram User connects Clawdbot to a **personal Telegram account** using MTProto.
Use this when you need user-level DMs or want to message from your own account.
## Requirements
- Telegram API ID + API hash from [my.telegram.org](https://my.telegram.org).
- The `telegram-user` plugin installed.
## Install the plugin
If the plugin is not bundled, install it:
```bash
clawdbot plugins install @clawdbot/telegram-user
```
## Configure
You can store credentials in config or use env vars.
Option A: env vars (default account only)
```bash
export TELEGRAM_USER_API_ID="123456"
export TELEGRAM_USER_API_HASH="your_api_hash"
clawdbot channels add --channel telegram-user --use-env
```
Option B: config
```bash
clawdbot channels add --channel telegram-user --api-id 123456 --api-hash your_api_hash
```
## Login (QR or phone code)
QR login (default):
```bash
clawdbot channels login --channel telegram-user
```
Phone login:
```bash
export TELEGRAM_USER_PHONE="+15551234567"
clawdbot channels login --channel telegram-user
```
Optional env helpers:
- `TELEGRAM_USER_CODE` (one-time code)
- `TELEGRAM_USER_PASSWORD` (2FA password)
## Security (DM policy)
By default, DMs are protected with pairing. Approve requests with:
```bash
clawdbot pairing approve telegram-user <code>
```
See [Pairing](/start/pairing) for details.
## Limitations
- DM-only (no groups or channels yet).
- Calls are not supported.

View File

@ -30,6 +30,7 @@ import {
} from "./send.js";
import { resolveTelegramUserSessionPath } from "./session.js";
import { getTelegramUserRuntime } from "./runtime.js";
import { telegramUserOnboardingAdapter } from "./onboarding.js";
import type { CoreConfig } from "./types.js";
const meta = {
@ -57,6 +58,7 @@ const isSessionLinked = async (accountId: string): Promise<boolean> => {
export const telegramUserPlugin: ChannelPlugin<ResolvedTelegramUserAccount> = {
id: "telegram-user",
meta,
onboarding: telegramUserOnboardingAdapter,
pairing: {
idLabel: "telegramUserId",
normalizeAllowEntry: (entry) =>

View File

@ -0,0 +1,336 @@
import {
addWildcardAllowFrom,
formatDocsLink,
promptAccountId,
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
type ChannelOnboardingAdapter,
type ChannelOnboardingDmPolicy,
type ClawdbotConfig,
type DmPolicy,
type WizardPrompter,
} from "clawdbot/plugin-sdk";
import {
listTelegramUserAccountIds,
resolveDefaultTelegramUserAccountId,
resolveTelegramUserAccount,
} from "./accounts.js";
import type { CoreConfig } from "./types.js";
const channel = "telegram-user" as const;
function setTelegramUserDmPolicy(
cfg: ClawdbotConfig,
policy: DmPolicy,
accountId?: string,
): ClawdbotConfig {
const resolvedAccountId = normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID;
const allowFrom =
policy === "open"
? addWildcardAllowFrom(
(cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"])?.allowFrom,
)
: undefined;
if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
return {
...cfg,
channels: {
...cfg.channels,
"telegram-user": {
...(cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"]),
dmPolicy: policy,
...(allowFrom ? { allowFrom } : {}),
},
},
};
}
return {
...cfg,
channels: {
...cfg.channels,
"telegram-user": {
...(cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"]),
accounts: {
...((cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"])
?.accounts ?? {}),
[resolvedAccountId]: {
...((cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"])
?.accounts?.[resolvedAccountId] ?? {}),
dmPolicy: policy,
...(allowFrom ? { allowFrom } : {}),
},
},
},
},
};
}
async function noteTelegramUserAuthHelp(prompter: WizardPrompter): Promise<void> {
await prompter.note(
[
"Telegram User (MTProto) needs an API ID + API hash from my.telegram.org.",
"You can store them in config or set TELEGRAM_USER_API_ID/TELEGRAM_USER_API_HASH.",
"Login happens via `clawdbot channels login --channel telegram-user`.",
`Docs: ${formatDocsLink("/channels/telegram-user", "channels/telegram-user")}`,
].join("\n"),
"Telegram user setup",
);
}
function parseAllowFromInput(raw: string): string[] {
return raw
.split(/[\n,;]+/g)
.map((entry) =>
entry
.trim()
.replace(/^(telegram-user|telegram|tg):/i, "")
.replace(/^user:/i, "")
.trim(),
)
.filter(Boolean);
}
async function promptTelegramUserAllowFrom(params: {
cfg: ClawdbotConfig;
prompter: WizardPrompter;
accountId?: string;
}): Promise<ClawdbotConfig> {
const accountId = normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID;
const resolved = resolveTelegramUserAccount({
cfg: params.cfg as CoreConfig,
accountId,
});
const existingAllowFrom = resolved.config.allowFrom ?? [];
const entry = await params.prompter.text({
message: "Telegram user allowFrom (user id or @username)",
placeholder: "@username",
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
});
const parsed = parseAllowFromInput(String(entry));
const merged = [
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
...parsed,
];
const unique = [...new Set(merged)];
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...params.cfg,
channels: {
...params.cfg.channels,
"telegram-user": {
...(params.cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"]),
enabled: true,
dmPolicy: "allowlist",
allowFrom: unique,
},
},
};
}
return {
...params.cfg,
channels: {
...params.cfg.channels,
"telegram-user": {
...(params.cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"]),
enabled: true,
accounts: {
...((params.cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"])
?.accounts ?? {}),
[accountId]: {
...((params.cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"])
?.accounts?.[accountId] ?? {}),
enabled: true,
dmPolicy: "allowlist",
allowFrom: unique,
},
},
},
},
};
}
const dmPolicy: ChannelOnboardingDmPolicy = {
label: "Telegram User",
channel,
policyKey: "channels.telegram-user.dmPolicy",
allowFromKey: "channels.telegram-user.allowFrom",
getCurrent: (cfg) =>
(cfg as CoreConfig).channels?.["telegram-user"]?.dmPolicy ?? "pairing",
setPolicy: (cfg, policy) => setTelegramUserDmPolicy(cfg, policy),
promptAllowFrom: async ({ cfg, prompter, accountId }) =>
await promptTelegramUserAllowFrom({ cfg, prompter, accountId }),
};
export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = {
channel,
getStatus: async ({ cfg }) => {
const configured = listTelegramUserAccountIds(cfg as CoreConfig).some((accountId) => {
const resolved = resolveTelegramUserAccount({ cfg: cfg as CoreConfig, accountId });
return Boolean(resolved.credentials.apiId && resolved.credentials.apiHash);
});
return {
channel,
configured,
statusLines: [
`Telegram User: ${configured ? "configured" : "needs API ID + API hash"}`,
],
selectionHint: configured ? "configured" : "needs credentials",
};
},
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds, forceAllowFrom }) => {
const override = accountOverrides["telegram-user"]?.trim();
const defaultAccountId = resolveDefaultTelegramUserAccountId(cfg as CoreConfig);
let accountId = override ? normalizeAccountId(override) : defaultAccountId;
if (shouldPromptAccountIds && !override) {
accountId = await promptAccountId({
cfg: cfg as ClawdbotConfig,
prompter,
label: "Telegram User",
currentId: accountId ?? defaultAccountId,
listAccountIds: (next) => listTelegramUserAccountIds(next as CoreConfig),
defaultAccountId,
});
}
const resolvedAccountId = normalizeAccountId(accountId) ?? defaultAccountId;
let next = cfg as CoreConfig;
const resolved = resolveTelegramUserAccount({
cfg: next,
accountId: resolvedAccountId,
});
const configured = Boolean(resolved.credentials.apiId && resolved.credentials.apiHash);
if (!configured) {
await noteTelegramUserAuthHelp(prompter);
}
const envApiId = process.env.TELEGRAM_USER_API_ID?.trim();
const envApiHash = process.env.TELEGRAM_USER_API_HASH?.trim();
const canUseEnv =
resolvedAccountId === DEFAULT_ACCOUNT_ID && Boolean(envApiId && envApiHash);
const hasConfig = Boolean(resolved.config.apiId && resolved.config.apiHash);
let useEnv = false;
if (canUseEnv && !hasConfig) {
useEnv = await prompter.confirm({
message: "Telegram user env vars detected. Use env values?",
initialValue: true,
});
}
let apiId = resolved.config.apiId;
let apiHash = resolved.config.apiHash;
if (!useEnv && (!apiId || !apiHash)) {
if (configured) {
const keep = await prompter.confirm({
message: "Telegram user credentials already configured. Keep them?",
initialValue: true,
});
if (!keep) {
apiId = undefined;
apiHash = undefined;
}
}
if (!apiId || !apiHash) {
const apiIdRaw = String(
await prompter.text({
message: "Telegram API ID",
initialValue: apiId ? String(apiId) : envApiId,
validate: (value) =>
Number.isFinite(Number.parseInt(String(value ?? ""), 10))
? undefined
: "Enter a numeric API ID",
}),
);
apiId = Number.parseInt(apiIdRaw, 10);
apiHash = String(
await prompter.text({
message: "Telegram API hash",
initialValue: apiHash ?? envApiHash,
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
}),
).trim();
}
}
if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
next = {
...next,
channels: {
...next.channels,
"telegram-user": {
...next.channels?.["telegram-user"],
enabled: true,
...(useEnv
? {}
: {
apiId,
apiHash,
}),
},
},
};
} else {
next = {
...next,
channels: {
...next.channels,
"telegram-user": {
...next.channels?.["telegram-user"],
enabled: true,
accounts: {
...next.channels?.["telegram-user"]?.accounts,
[resolvedAccountId]: {
...next.channels?.["telegram-user"]?.accounts?.[resolvedAccountId],
enabled: true,
...(useEnv
? {}
: {
apiId,
apiHash,
}),
},
},
},
},
};
}
if (forceAllowFrom) {
next = await promptTelegramUserAllowFrom({
cfg: next,
prompter,
accountId: resolvedAccountId,
});
}
await prompter.note(
[
"Next: link the account via QR or phone code.",
"Run: clawdbot channels login --channel telegram-user",
].join("\n"),
"Telegram user login",
);
return { cfg: next, accountId: resolvedAccountId };
},
dmPolicy,
disable: (cfg) => ({
...(cfg as CoreConfig),
channels: {
...(cfg as CoreConfig).channels,
"telegram-user": {
...(cfg as CoreConfig).channels?.["telegram-user"],
enabled: false,
},
},
}),
};