From b953e0bff8a00cf4766c55f8f8cb0f6cf035d51f Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 04:33:46 +0000 Subject: [PATCH] Telegram-user: add onboarding + docs --- docs/channels/index.md | 1 + docs/channels/telegram-user.md | 68 +++++ extensions/telegram-user/src/channel.ts | 2 + extensions/telegram-user/src/onboarding.ts | 336 +++++++++++++++++++++ 4 files changed, 407 insertions(+) create mode 100644 docs/channels/telegram-user.md create mode 100644 extensions/telegram-user/src/onboarding.ts diff --git a/docs/channels/index.md b/docs/channels/index.md index ea1b1bc8a..4402e777c 100644 --- a/docs/channels/index.md +++ b/docs/channels/index.md @@ -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. diff --git a/docs/channels/telegram-user.md b/docs/channels/telegram-user.md new file mode 100644 index 000000000..1d959950b --- /dev/null +++ b/docs/channels/telegram-user.md @@ -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 +``` + +See [Pairing](/start/pairing) for details. + +## Limitations + +- DM-only (no groups or channels yet). +- Calls are not supported. diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index b2ae2af2b..dadfb7674 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -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 => { export const telegramUserPlugin: ChannelPlugin = { id: "telegram-user", meta, + onboarding: telegramUserOnboardingAdapter, pairing: { idLabel: "telegramUserId", normalizeAllowEntry: (entry) => diff --git a/extensions/telegram-user/src/onboarding.ts b/extensions/telegram-user/src/onboarding.ts new file mode 100644 index 000000000..5380188e5 --- /dev/null +++ b/extensions/telegram-user/src/onboarding.ts @@ -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 { + 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 { + 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, + }, + }, + }), +};