Telegram-user: add onboarding + docs
This commit is contained in:
parent
a4bea14162
commit
b953e0bff8
@ -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.
|
||||
|
||||
68
docs/channels/telegram-user.md
Normal file
68
docs/channels/telegram-user.md
Normal 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.
|
||||
@ -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) =>
|
||||
|
||||
336
extensions/telegram-user/src/onboarding.ts
Normal file
336
extensions/telegram-user/src/onboarding.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user