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.
|
- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
|
||||||
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
|
- [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.
|
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
|
||||||
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
|
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
|
||||||
- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook.
|
- [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";
|
} from "./send.js";
|
||||||
import { resolveTelegramUserSessionPath } from "./session.js";
|
import { resolveTelegramUserSessionPath } from "./session.js";
|
||||||
import { getTelegramUserRuntime } from "./runtime.js";
|
import { getTelegramUserRuntime } from "./runtime.js";
|
||||||
|
import { telegramUserOnboardingAdapter } from "./onboarding.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
import type { CoreConfig } from "./types.js";
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
@ -57,6 +58,7 @@ const isSessionLinked = async (accountId: string): Promise<boolean> => {
|
|||||||
export const telegramUserPlugin: ChannelPlugin<ResolvedTelegramUserAccount> = {
|
export const telegramUserPlugin: ChannelPlugin<ResolvedTelegramUserAccount> = {
|
||||||
id: "telegram-user",
|
id: "telegram-user",
|
||||||
meta,
|
meta,
|
||||||
|
onboarding: telegramUserOnboardingAdapter,
|
||||||
pairing: {
|
pairing: {
|
||||||
idLabel: "telegramUserId",
|
idLabel: "telegramUserId",
|
||||||
normalizeAllowEntry: (entry) =>
|
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