From 52e730e090f8a95af6c521a60d36267e2a2556db Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 04:17:52 +0000 Subject: [PATCH 01/46] Channels: add telegram-user plugin --- extensions/telegram-user/.gitignore | 1 + extensions/telegram-user/clawdbot.plugin.json | 11 + extensions/telegram-user/index.ts | 18 + extensions/telegram-user/package.json | 34 ++ extensions/telegram-user/src/accounts.ts | 95 ++++++ extensions/telegram-user/src/active-client.ts | 11 + extensions/telegram-user/src/channel.ts | 315 ++++++++++++++++++ extensions/telegram-user/src/client.ts | 13 + extensions/telegram-user/src/config-schema.ts | 20 ++ extensions/telegram-user/src/login.ts | 41 +++ .../telegram-user/src/monitor/handler.ts | 299 +++++++++++++++++ extensions/telegram-user/src/monitor/index.ts | 93 ++++++ extensions/telegram-user/src/runtime.ts | 14 + extensions/telegram-user/src/send.ts | 128 +++++++ extensions/telegram-user/src/session.ts | 20 ++ extensions/telegram-user/src/types.ts | 31 ++ src/channels/plugins/types.core.ts | 2 + src/cli/channels-cli.ts | 4 + src/commands/channels/add-mutators.ts | 4 + src/commands/channels/add.ts | 13 +- 20 files changed, 1166 insertions(+), 1 deletion(-) create mode 100644 extensions/telegram-user/.gitignore create mode 100644 extensions/telegram-user/clawdbot.plugin.json create mode 100644 extensions/telegram-user/index.ts create mode 100644 extensions/telegram-user/package.json create mode 100644 extensions/telegram-user/src/accounts.ts create mode 100644 extensions/telegram-user/src/active-client.ts create mode 100644 extensions/telegram-user/src/channel.ts create mode 100644 extensions/telegram-user/src/client.ts create mode 100644 extensions/telegram-user/src/config-schema.ts create mode 100644 extensions/telegram-user/src/login.ts create mode 100644 extensions/telegram-user/src/monitor/handler.ts create mode 100644 extensions/telegram-user/src/monitor/index.ts create mode 100644 extensions/telegram-user/src/runtime.ts create mode 100644 extensions/telegram-user/src/send.ts create mode 100644 extensions/telegram-user/src/session.ts create mode 100644 extensions/telegram-user/src/types.ts diff --git a/extensions/telegram-user/.gitignore b/extensions/telegram-user/.gitignore new file mode 100644 index 000000000..c2658d7d1 --- /dev/null +++ b/extensions/telegram-user/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/extensions/telegram-user/clawdbot.plugin.json b/extensions/telegram-user/clawdbot.plugin.json new file mode 100644 index 000000000..359ee5d90 --- /dev/null +++ b/extensions/telegram-user/clawdbot.plugin.json @@ -0,0 +1,11 @@ +{ + "id": "telegram-user", + "channels": [ + "telegram-user" + ], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } +} diff --git a/extensions/telegram-user/index.ts b/extensions/telegram-user/index.ts new file mode 100644 index 000000000..db524a6ed --- /dev/null +++ b/extensions/telegram-user/index.ts @@ -0,0 +1,18 @@ +import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk"; +import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk"; + +import { telegramUserPlugin } from "./src/channel.js"; +import { setTelegramUserRuntime } from "./src/runtime.js"; + +const plugin = { + id: "telegram-user", + name: "Telegram User", + description: "Telegram MTProto user channel plugin", + configSchema: emptyPluginConfigSchema(), + register(api: ClawdbotPluginApi) { + setTelegramUserRuntime(api.runtime); + api.registerChannel({ plugin: telegramUserPlugin }); + }, +}; + +export default plugin; diff --git a/extensions/telegram-user/package.json b/extensions/telegram-user/package.json new file mode 100644 index 000000000..54f2c7eaa --- /dev/null +++ b/extensions/telegram-user/package.json @@ -0,0 +1,34 @@ +{ + "name": "@clawdbot/telegram-user", + "version": "2026.1.22", + "type": "module", + "description": "Clawdbot Telegram user (MTProto) channel plugin", + "clawdbot": { + "extensions": [ + "./index.ts" + ], + "channel": { + "id": "telegram-user", + "label": "Telegram User", + "selectionLabel": "Telegram User (MTProto)", + "detailLabel": "Telegram User", + "docsPath": "/channels/telegram-user", + "docsLabel": "telegram-user", + "blurb": "login as a Telegram user via QR; DM-only for now.", + "order": 12, + "quickstartAllowFrom": true + }, + "install": { + "npmSpec": "@clawdbot/telegram-user", + "localPath": "extensions/telegram-user", + "defaultChoice": "npm" + } + }, + "dependencies": { + "@mtcute/core": "^0.27.6", + "@mtcute/dispatcher": "^0.27.6", + "@mtcute/node": "^0.27.6", + "qrcode-terminal": "^0.12.0", + "clawdbot": "workspace:*" + } +} diff --git a/extensions/telegram-user/src/accounts.ts b/extensions/telegram-user/src/accounts.ts new file mode 100644 index 000000000..6754a1052 --- /dev/null +++ b/extensions/telegram-user/src/accounts.ts @@ -0,0 +1,95 @@ +import type { CoreConfig, TelegramUserAccountConfig } from "./types.js"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk"; + +export type TelegramUserCredentials = { + apiId?: number; + apiHash?: string; + apiIdSource: "env" | "config" | "none"; + apiHashSource: "env" | "config" | "none"; +}; + +export type ResolvedTelegramUserAccount = { + accountId: string; + enabled: boolean; + name?: string; + credentials: TelegramUserCredentials; + config: TelegramUserAccountConfig; +}; + +function resolveAccountConfig( + cfg: CoreConfig, + accountId: string, +): TelegramUserAccountConfig | undefined { + const accounts = cfg.channels?.["telegram-user"]?.accounts; + if (!accounts || typeof accounts !== "object") return undefined; + const direct = accounts[accountId] as TelegramUserAccountConfig | undefined; + if (direct) return direct; + const normalized = normalizeAccountId(accountId); + const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized); + return matchKey ? (accounts[matchKey] as TelegramUserAccountConfig | undefined) : undefined; +} + +function mergeTelegramUserAccountConfig( + cfg: CoreConfig, + accountId: string, +): TelegramUserAccountConfig { + const { accounts: _ignored, ...base } = (cfg.channels?.["telegram-user"] ?? + {}) as TelegramUserAccountConfig & { accounts?: unknown }; + const account = resolveAccountConfig(cfg, accountId) ?? {}; + return { ...base, ...account }; +} + +function resolveCredentials(cfg: CoreConfig, accountId: string): TelegramUserCredentials { + const merged = mergeTelegramUserAccountConfig(cfg, accountId); + const envApiId = + accountId === DEFAULT_ACCOUNT_ID + ? Number.parseInt(process.env.TELEGRAM_USER_API_ID ?? "", 10) + : Number.NaN; + const envApiHash = + accountId === DEFAULT_ACCOUNT_ID ? process.env.TELEGRAM_USER_API_HASH?.trim() : undefined; + const apiId = + Number.isFinite(envApiId) && envApiId > 0 ? envApiId : merged.apiId ?? undefined; + const apiHash = envApiHash || merged.apiHash?.trim(); + return { + apiId, + apiHash, + apiIdSource: + Number.isFinite(envApiId) && envApiId > 0 + ? "env" + : merged.apiId + ? "config" + : "none", + apiHashSource: envApiHash ? "env" : merged.apiHash ? "config" : "none", + }; +} + +export function listTelegramUserAccountIds(cfg: CoreConfig): string[] { + const accounts = cfg.channels?.["telegram-user"]?.accounts; + const ids = accounts ? Object.keys(accounts).filter(Boolean) : []; + if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; + if (!ids.includes(DEFAULT_ACCOUNT_ID)) ids.push(DEFAULT_ACCOUNT_ID); + return ids.sort((a, b) => a.localeCompare(b)); +} + +export function resolveDefaultTelegramUserAccountId(cfg: CoreConfig): string { + const ids = listTelegramUserAccountIds(cfg); + if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + return ids[0] ?? DEFAULT_ACCOUNT_ID; +} + +export function resolveTelegramUserAccount(params: { + cfg: CoreConfig; + accountId?: string | null; +}): ResolvedTelegramUserAccount { + const normalized = normalizeAccountId(params.accountId); + const merged = mergeTelegramUserAccountConfig(params.cfg, normalized); + const baseEnabled = params.cfg.channels?.["telegram-user"]?.enabled !== false; + const enabled = baseEnabled && merged.enabled !== false; + return { + accountId: normalized, + enabled, + name: merged.name?.trim() || undefined, + credentials: resolveCredentials(params.cfg, normalized), + config: merged, + }; +} diff --git a/extensions/telegram-user/src/active-client.ts b/extensions/telegram-user/src/active-client.ts new file mode 100644 index 000000000..4eb60171f --- /dev/null +++ b/extensions/telegram-user/src/active-client.ts @@ -0,0 +1,11 @@ +import type { TelegramClient } from "@mtcute/node"; + +let activeClient: TelegramClient | null = null; + +export function setActiveTelegramUserClient(next: TelegramClient | null) { + activeClient = next; +} + +export function getActiveTelegramUserClient(): TelegramClient | null { + return activeClient; +} diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts new file mode 100644 index 000000000..b2ae2af2b --- /dev/null +++ b/extensions/telegram-user/src/channel.ts @@ -0,0 +1,315 @@ +import fs from "node:fs"; + +import { + applyAccountNameToChannelSection, + buildChannelConfigSchema, + DEFAULT_ACCOUNT_ID, + deleteAccountFromConfigSection, + formatPairingApproveHint, + normalizeAccountId, + setAccountEnabledInConfigSection, + type ChannelPlugin, + type ChannelSetupInput, + type ClawdbotConfig, +} from "clawdbot/plugin-sdk"; + +import { + listTelegramUserAccountIds, + resolveDefaultTelegramUserAccountId, + resolveTelegramUserAccount, + type ResolvedTelegramUserAccount, +} from "./accounts.js"; +import { TelegramUserConfigSchema } from "./config-schema.js"; +import { loginTelegramUser } from "./login.js"; +import { monitorTelegramUserProvider } from "./monitor/index.js"; +import { + looksLikeTelegramUserTargetId, + normalizeTelegramUserMessagingTarget, + sendMediaTelegramUser, + sendMessageTelegramUser, +} from "./send.js"; +import { resolveTelegramUserSessionPath } from "./session.js"; +import { getTelegramUserRuntime } from "./runtime.js"; +import type { CoreConfig } from "./types.js"; + +const meta = { + id: "telegram-user", + label: "Telegram User", + selectionLabel: "Telegram User (MTProto)", + detailLabel: "Telegram User", + docsPath: "/channels/telegram-user", + docsLabel: "telegram-user", + blurb: "login as a Telegram user via QR; DM-only for now.", + order: 12, + quickstartAllowFrom: true, +}; + +type TelegramUserSetupInput = ChannelSetupInput & { + apiId?: number; + apiHash?: string; +}; + +const isSessionLinked = async (accountId: string): Promise => { + const sessionPath = resolveTelegramUserSessionPath(accountId); + return fs.existsSync(sessionPath); +}; + +export const telegramUserPlugin: ChannelPlugin = { + id: "telegram-user", + meta, + pairing: { + idLabel: "telegramUserId", + normalizeAllowEntry: (entry) => + entry.replace(/^(telegram-user|telegram|tg):/i, "").toLowerCase(), + notifyApproval: async ({ id }) => { + await sendMessageTelegramUser(String(id), "Clawdbot: access approved.", {}); + }, + }, + capabilities: { + chatTypes: ["direct"], + reactions: false, + threads: false, + media: true, + nativeCommands: false, + blockStreaming: true, + }, + messaging: { + normalizeTarget: normalizeTelegramUserMessagingTarget, + targetResolver: { + looksLikeId: looksLikeTelegramUserTargetId, + hint: "", + }, + }, + reload: { configPrefixes: ["channels.telegram-user"] }, + configSchema: buildChannelConfigSchema(TelegramUserConfigSchema), + config: { + listAccountIds: (cfg) => listTelegramUserAccountIds(cfg as CoreConfig), + resolveAccount: (cfg, accountId) => + resolveTelegramUserAccount({ cfg: cfg as CoreConfig, accountId }), + defaultAccountId: (cfg) => resolveDefaultTelegramUserAccountId(cfg as CoreConfig), + setAccountEnabled: ({ cfg, accountId, enabled }) => + setAccountEnabledInConfigSection({ + cfg, + sectionKey: "telegram-user", + accountId, + enabled, + allowTopLevel: true, + }), + deleteAccount: ({ cfg, accountId }) => + deleteAccountFromConfigSection({ + cfg, + sectionKey: "telegram-user", + accountId, + clearBaseFields: ["apiId", "apiHash", "name"], + }), + isConfigured: (account) => + Boolean(account.credentials.apiId && account.credentials.apiHash), + describeAccount: (account) => ({ + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured: Boolean(account.credentials.apiId && account.credentials.apiHash), + }), + resolveAllowFrom: ({ cfg, accountId }) => + (resolveTelegramUserAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? []).map( + (entry) => String(entry), + ), + formatAllowFrom: ({ allowFrom }) => + allowFrom + .map((entry) => String(entry).trim()) + .filter(Boolean) + .map((entry) => entry.replace(/^(telegram-user|telegram|tg):/i, "")) + .map((entry) => entry.toLowerCase()), + }, + security: { + resolveDmPolicy: ({ cfg, accountId, account }) => { + const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; + const useAccountPath = Boolean(cfg.channels?.["telegram-user"]?.accounts?.[resolvedAccountId]); + const basePath = useAccountPath + ? `channels.telegram-user.accounts.${resolvedAccountId}.` + : "channels.telegram-user."; + return { + policy: account.config.dmPolicy ?? "pairing", + allowFrom: account.config.allowFrom ?? [], + policyPath: `${basePath}dmPolicy`, + allowFromPath: basePath, + approveHint: formatPairingApproveHint("telegram-user"), + normalizeEntry: (raw) => + raw.replace(/^(telegram-user|telegram|tg):/i, "").toLowerCase(), + }; + }, + }, + outbound: { + deliveryMode: "direct", + chunker: (text, limit) => + getTelegramUserRuntime().channel.text.chunkMarkdownText(text, limit), + textChunkLimit: 4000, + sendText: async ({ to, text, accountId }) => { + const result = await sendMessageTelegramUser(to, text, { accountId: accountId ?? undefined }); + return { channel: "telegram-user", ...result }; + }, + sendMedia: async ({ to, text, mediaUrl, accountId }) => { + const result = await sendMediaTelegramUser(to, text, { + accountId: accountId ?? undefined, + mediaUrl, + }); + return { channel: "telegram-user", ...result }; + }, + }, + auth: { + login: async ({ cfg, accountId, runtime }) => { + const account = resolveTelegramUserAccount({ + cfg: cfg as CoreConfig, + accountId, + }); + const apiId = account.credentials.apiId; + const apiHash = account.credentials.apiHash; + if (!apiId || !apiHash) { + throw new Error("Telegram user apiId/apiHash required. Set in config or env."); + } + const storagePath = resolveTelegramUserSessionPath(account.accountId); + await loginTelegramUser({ + apiId, + apiHash, + storagePath, + runtime, + }); + }, + }, + status: { + defaultRuntime: { + accountId: DEFAULT_ACCOUNT_ID, + running: false, + lastStartAt: null, + lastStopAt: null, + lastError: null, + lastInboundAt: null, + lastOutboundAt: null, + }, + buildAccountSnapshot: async ({ account, runtime }) => ({ + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured: Boolean(account.credentials.apiId && account.credentials.apiHash), + linked: await isSessionLinked(account.accountId), + running: runtime?.running ?? false, + lastStartAt: runtime?.lastStartAt ?? null, + lastStopAt: runtime?.lastStopAt ?? null, + lastError: runtime?.lastError ?? null, + lastInboundAt: runtime?.lastInboundAt ?? null, + lastOutboundAt: runtime?.lastOutboundAt ?? null, + dmPolicy: account.config.dmPolicy ?? "pairing", + allowFrom: (account.config.allowFrom ?? []).map((entry) => String(entry)), + }), + resolveAccountState: ({ configured }) => (configured ? "configured" : "not configured"), + }, + setup: { + resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), + applyAccountName: ({ cfg, accountId, name }) => + applyAccountNameToChannelSection({ + cfg: cfg as ClawdbotConfig, + channelKey: "telegram-user", + accountId, + name, + }), + validateInput: ({ accountId, input }) => { + const setupInput = input as TelegramUserSetupInput; + if (setupInput.useEnv && accountId !== DEFAULT_ACCOUNT_ID) { + return "TELEGRAM_USER_API_ID/TELEGRAM_USER_API_HASH can only be used for the default account."; + } + if (!setupInput.useEnv && (!setupInput.apiId || !setupInput.apiHash)) { + return "Telegram user requires apiId/apiHash (or --use-env)."; + } + return null; + }, + applyAccountConfig: ({ cfg, accountId, input }) => { + const setupInput = input as TelegramUserSetupInput; + const namedConfig = applyAccountNameToChannelSection({ + cfg: cfg as ClawdbotConfig, + channelKey: "telegram-user", + accountId, + name: setupInput.name, + }); + if (accountId === DEFAULT_ACCOUNT_ID) { + return { + ...namedConfig, + channels: { + ...namedConfig.channels, + "telegram-user": { + ...namedConfig.channels?.["telegram-user"], + enabled: true, + ...(setupInput.useEnv + ? {} + : { + apiId: setupInput.apiId, + apiHash: setupInput.apiHash, + }), + }, + }, + }; + } + return { + ...namedConfig, + channels: { + ...namedConfig.channels, + "telegram-user": { + ...namedConfig.channels?.["telegram-user"], + enabled: true, + accounts: { + ...namedConfig.channels?.["telegram-user"]?.accounts, + [accountId]: { + ...namedConfig.channels?.["telegram-user"]?.accounts?.[accountId], + enabled: true, + ...(setupInput.useEnv + ? {} + : { + apiId: setupInput.apiId, + apiHash: setupInput.apiHash, + }), + }, + }, + }, + }, + }; + }, + }, + gateway: { + startAccount: async (ctx) => { + ctx.setStatus({ + accountId: ctx.accountId, + running: true, + lastStartAt: Date.now(), + lastError: null, + }); + try { + await monitorTelegramUserProvider({ + runtime: ctx.runtime, + abortSignal: ctx.abortSignal, + accountId: ctx.accountId, + }); + ctx.setStatus({ + accountId: ctx.accountId, + running: false, + lastStopAt: Date.now(), + }); + } catch (err) { + ctx.setStatus({ + accountId: ctx.accountId, + running: false, + lastStopAt: Date.now(), + lastError: String(err), + }); + throw err; + } + }, + stopAccount: async () => { + const { getActiveTelegramUserClient, setActiveTelegramUserClient } = + await import("./active-client.js"); + const active = getActiveTelegramUserClient(); + if (active) { + await active.destroy().catch(() => undefined); + setActiveTelegramUserClient(null); + } + }, + }, +}; diff --git a/extensions/telegram-user/src/client.ts b/extensions/telegram-user/src/client.ts new file mode 100644 index 000000000..5a3b09dea --- /dev/null +++ b/extensions/telegram-user/src/client.ts @@ -0,0 +1,13 @@ +import { TelegramClient } from "@mtcute/node"; + +export function createTelegramUserClient(params: { + apiId: number; + apiHash: string; + storagePath: string; +}) { + return new TelegramClient({ + apiId: params.apiId, + apiHash: params.apiHash, + storage: params.storagePath, + }); +} diff --git a/extensions/telegram-user/src/config-schema.ts b/extensions/telegram-user/src/config-schema.ts new file mode 100644 index 000000000..d40d029ca --- /dev/null +++ b/extensions/telegram-user/src/config-schema.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +const allowFromEntry = z.union([z.string(), z.number()]); + +const TelegramUserAccountSchema = z + .object({ + name: z.string().optional(), + enabled: z.boolean().optional(), + apiId: z.number().int().positive().optional(), + apiHash: z.string().optional(), + dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(), + allowFrom: z.array(allowFromEntry).optional(), + textChunkLimit: z.number().int().positive().optional(), + mediaMaxMb: z.number().positive().optional(), + }) + .strict(); + +export const TelegramUserConfigSchema = TelegramUserAccountSchema.extend({ + accounts: z.record(z.string(), TelegramUserAccountSchema.optional()).optional(), +}); diff --git a/extensions/telegram-user/src/login.ts b/extensions/telegram-user/src/login.ts new file mode 100644 index 000000000..b26fd911f --- /dev/null +++ b/extensions/telegram-user/src/login.ts @@ -0,0 +1,41 @@ +import qrcode from "qrcode-terminal"; +import type { RuntimeEnv } from "clawdbot/plugin-sdk"; + +import { createTelegramUserClient } from "./client.js"; +import { ensureTelegramUserSessionDir } from "./session.js"; + +export async function loginTelegramUser(params: { + apiId: number; + apiHash: string; + storagePath: string; + runtime: RuntimeEnv; +}) { + const { apiId, apiHash, storagePath, runtime } = params; + ensureTelegramUserSessionDir({ sessionPath: storagePath }); + const client = createTelegramUserClient({ apiId, apiHash, storagePath }); + let lastUrl = ""; + + const password = process.env.TELEGRAM_USER_PASSWORD?.trim() || undefined; + + try { + const user = await client.start({ + qrCodeHandler: (url, expires) => { + if (url === lastUrl) return; + lastUrl = url; + runtime.log(`Scan this QR in Telegram (expires ${expires.toLocaleTimeString()}):`); + qrcode.generate(url, { small: true }); + }, + ...(password ? { password } : {}), + invalidCodeCallback: async (type) => { + if (type === "password") { + runtime.error?.( + "Telegram 2FA password rejected. Set TELEGRAM_USER_PASSWORD and rerun.", + ); + } + }, + }); + runtime.log(`Telegram user logged in as ${user.displayName}.`); + } finally { + await client.destroy(); + } +} diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts new file mode 100644 index 000000000..e8d2a0046 --- /dev/null +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -0,0 +1,299 @@ +import type { TelegramClient } from "@mtcute/node"; +import type { MessageContext } from "@mtcute/dispatcher"; +import type { RuntimeEnv } from "clawdbot/plugin-sdk"; + +import { getTelegramUserRuntime } from "../runtime.js"; +import type { CoreConfig, TelegramUserAccountConfig } from "../types.js"; +import { sendMediaTelegramUser, sendMessageTelegramUser } from "../send.js"; + +const DEFAULT_TEXT_LIMIT = 4000; +const DEFAULT_MEDIA_MAX_MB = 5; + +type TelegramUserHandlerParams = { + client: TelegramClient; + cfg: CoreConfig; + runtime: RuntimeEnv; + accountId: string; + accountConfig: TelegramUserAccountConfig; +}; + +function normalizeAllowEntry(raw: string): string { + const trimmed = raw.trim().toLowerCase(); + return trimmed + .replace(/^(telegram-user|telegram|tg):/i, "") + .replace(/^user:/i, "") + .trim(); +} + +function parseAllowlist(entries: Array | undefined) { + const normalized = (entries ?? []) + .map((entry) => normalizeAllowEntry(String(entry))) + .filter(Boolean); + const hasWildcard = normalized.includes("*"); + const usernames = new Set(); + const ids = new Set(); + for (const entry of normalized) { + if (entry === "*") continue; + if (/^-?\d+$/.test(entry)) { + ids.add(entry); + continue; + } + const username = entry.startsWith("@") ? entry.slice(1) : entry; + if (username) usernames.add(username); + } + return { hasWildcard, usernames, ids }; +} + +function isSenderAllowed(params: { + allowFrom: Array | undefined; + senderId: string; + senderUsername?: string | null; +}): boolean { + const parsed = parseAllowlist(params.allowFrom); + if (parsed.hasWildcard) return true; + if (parsed.ids.has(params.senderId)) return true; + const username = params.senderUsername?.trim().toLowerCase(); + if (!username) return false; + return parsed.usernames.has(username.replace(/^@/, "")); +} + +async function resolveMediaAttachment(params: { + client: TelegramClient; + mediaMaxMb: number; + media: MessageContext["media"]; +}) { + if (!params.media) return null; + const core = getTelegramUserRuntime(); + const maxBytes = Math.max(1, params.mediaMaxMb) * 1024 * 1024; + if ("fileSize" in params.media && typeof params.media.fileSize === "number") { + if (params.media.fileSize > maxBytes) { + throw new Error(`Media exceeds ${(maxBytes / (1024 * 1024)).toFixed(0)}MB limit`); + } + } + const buffer = Buffer.from(await params.client.downloadAsBuffer(params.media)); + const fileName = + params.media && "fileName" in params.media && typeof params.media.fileName === "string" + ? params.media.fileName + : undefined; + const contentType = + params.media && "mimeType" in params.media && typeof params.media.mimeType === "string" + ? params.media.mimeType + : await core.media.detectMime({ buffer, filePath: fileName }); + const saved = await core.channel.media.saveMediaBuffer( + buffer, + contentType, + "telegram-user", + maxBytes, + fileName, + ); + return { + path: saved.path, + contentType: saved.contentType ?? contentType, + }; +} + +export function createTelegramUserMessageHandler(params: TelegramUserHandlerParams) { + const { client, cfg, runtime, accountId, accountConfig } = params; + const core = getTelegramUserRuntime(); + const textLimit = accountConfig.textChunkLimit ?? DEFAULT_TEXT_LIMIT; + const mediaMaxMb = accountConfig.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB; + const dmPolicy = accountConfig.dmPolicy ?? "pairing"; + const allowFrom = accountConfig.allowFrom ?? []; + + return async (msg: MessageContext) => { + try { + if (msg.isOutgoing || msg.isService) return; + if (msg.chat.type !== "user") return; + + const sender = await msg.getCompleteSender().catch(() => msg.sender); + if (sender.type !== "user") return; + if ("isSelf" in sender && sender.isSelf) return; + + const senderId = String(sender.id); + const senderUsername = "username" in sender ? sender.username : null; + const senderName = "displayName" in sender ? sender.displayName : senderId; + const storeAllowFrom = await core.channel.pairing + .readAllowFromStore("telegram-user") + .catch(() => []); + const combinedAllowFrom = [...allowFrom, ...storeAllowFrom]; + + if (dmPolicy === "disabled") return; + if ( + dmPolicy !== "open" && + !isSenderAllowed({ allowFrom: combinedAllowFrom, senderId, senderUsername }) + ) { + if (dmPolicy === "pairing") { + const pairing = await core.channel.pairing.upsertPairingRequest({ + channel: "telegram-user", + id: senderId, + meta: { + username: senderUsername ?? undefined, + name: senderName, + }, + }); + const reply = core.channel.pairing.buildPairingReply({ + channel: "telegram-user", + idLine: `Telegram user id: ${senderId}`, + code: pairing.code, + }); + await sendMessageTelegramUser(`telegram-user:${senderId}`, reply, { + client, + accountId, + }); + } + return; + } + + const text = msg.text?.trim() ?? ""; + const media = await resolveMediaAttachment({ + client, + mediaMaxMb, + media: msg.media, + }).catch((err) => { + runtime.error?.(`telegram-user media download failed: ${String(err)}`); + return null; + }); + if (!text && !media) return; + + core.channel.activity.record({ + channel: "telegram-user", + accountId, + direction: "inbound", + }); + + const route = core.channel.routing.resolveAgentRoute({ + cfg, + channel: "telegram-user", + accountId, + peer: { + kind: "dm", + id: senderId, + }, + }); + const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { + agentId: route.agentId, + }); + const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg); + const previousTimestamp = core.channel.session.readSessionUpdatedAt({ + storePath, + sessionKey: route.sessionKey, + }); + const body = core.channel.reply.formatAgentEnvelope({ + channel: "Telegram User", + from: senderName, + timestamp: msg.date, + previousTimestamp, + envelope: envelopeOptions, + body: text || "(media)", + }); + + const ctxPayload = core.channel.reply.finalizeInboundContext({ + Body: body, + RawBody: text, + CommandBody: text, + From: `telegram-user:${senderId}`, + To: `telegram-user:${senderId}`, + SessionKey: route.sessionKey, + AccountId: route.accountId, + ChatType: "direct", + ConversationLabel: senderName, + SenderName: senderName, + SenderId: senderId, + SenderUsername: senderUsername ?? undefined, + Provider: "telegram-user" as const, + Surface: "telegram-user" as const, + MessageSid: String(msg.id), + ReplyToId: String(msg.id), + Timestamp: msg.date, + MediaPath: media?.path, + MediaType: media?.contentType, + MediaUrl: media?.path, + CommandAuthorized: true, + CommandSource: "text" as const, + OriginatingChannel: "telegram-user" as const, + OriginatingTo: `telegram-user:${senderId}`, + }); + + void core.channel.session + .recordSessionMetaFromInbound({ + storePath, + sessionKey: ctxPayload.SessionKey ?? route.sessionKey, + ctx: ctxPayload, + }) + .catch((err) => { + runtime.error?.(`telegram-user failed to update session meta: ${String(err)}`); + }); + + await core.channel.session.updateLastRoute({ + storePath, + sessionKey: route.mainSessionKey, + channel: "telegram-user", + to: `telegram-user:${senderId}`, + accountId: route.accountId, + ctx: ctxPayload, + }); + + let hasReplied = false; + const { dispatcher, replyOptions, markDispatchIdle } = + core.channel.reply.createReplyDispatcherWithTyping({ + responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId) + .responsePrefix, + humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId), + deliver: async (payload) => { + const replyToId = hasReplied ? undefined : msg.id; + const replyText = payload.text ?? ""; + const mediaUrl = payload.mediaUrl; + if (mediaUrl) { + await sendMediaTelegramUser(`telegram-user:${senderId}`, replyText, { + client, + accountId, + replyToId, + mediaUrl, + maxBytes: mediaMaxMb * 1024 * 1024, + }); + hasReplied = true; + core.channel.activity.record({ + channel: "telegram-user", + accountId, + direction: "outbound", + }); + return; + } + if (replyText) { + for (const chunk of core.channel.text.chunkMarkdownText(replyText, textLimit)) { + const trimmed = chunk.trim(); + if (!trimmed) continue; + await sendMessageTelegramUser(`telegram-user:${senderId}`, trimmed, { + client, + accountId, + replyToId, + }); + hasReplied = true; + core.channel.activity.record({ + channel: "telegram-user", + accountId, + direction: "outbound", + }); + } + } + }, + onReplyStart: async () => { + await client.sendTyping(senderId).catch(() => undefined); + }, + onError: (err) => { + runtime.error?.(`telegram-user reply failed: ${String(err)}`); + }, + }); + + await core.channel.reply.dispatchReplyFromConfig({ + ctx: ctxPayload, + cfg, + dispatcher, + replyOptions, + }); + markDispatchIdle(); + } catch (err) { + runtime.error?.(`telegram-user handler failed: ${String(err)}`); + } + }; +} diff --git a/extensions/telegram-user/src/monitor/index.ts b/extensions/telegram-user/src/monitor/index.ts new file mode 100644 index 000000000..55d3d4203 --- /dev/null +++ b/extensions/telegram-user/src/monitor/index.ts @@ -0,0 +1,93 @@ +import fs from "node:fs"; +import { Dispatcher, filters } from "@mtcute/dispatcher"; +import type { RuntimeEnv } from "clawdbot/plugin-sdk"; + +import { createTelegramUserClient } from "../client.js"; +import { resolveTelegramUserAccount } from "../accounts.js"; +import { resolveTelegramUserSessionPath } from "../session.js"; +import { getTelegramUserRuntime } from "../runtime.js"; +import { setActiveTelegramUserClient } from "../active-client.js"; +import { createTelegramUserMessageHandler } from "./handler.js"; +import type { CoreConfig } from "../types.js"; + +export type MonitorTelegramUserOpts = { + runtime?: RuntimeEnv; + abortSignal?: AbortSignal; + accountId?: string | null; +}; + +export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts = {}) { + const core = getTelegramUserRuntime(); + const cfg = core.config.loadConfig() as CoreConfig; + const account = resolveTelegramUserAccount({ + cfg, + accountId: opts.accountId, + }); + if (!account.enabled) return; + + const apiId = account.credentials.apiId; + const apiHash = account.credentials.apiHash; + if (!apiId || !apiHash) { + throw new Error("Telegram user credentials missing (apiId/apiHash required)."); + } + + const runtime: RuntimeEnv = + opts.runtime ?? + ({ + log: (message: string) => core.logging.getChildLogger({ module: "telegram-user" }).info(message), + error: (message: string) => + core.logging.getChildLogger({ module: "telegram-user" }).error(message), + exit: (code: number): never => { + throw new Error(`exit ${code}`); + }, + } satisfies RuntimeEnv); + + const storagePath = resolveTelegramUserSessionPath(account.accountId); + if (!fs.existsSync(storagePath)) { + throw new Error( + "Telegram user session missing. Run `clawdbot channels login --channel telegram-user` first.", + ); + } + const client = createTelegramUserClient({ apiId, apiHash, storagePath }); + setActiveTelegramUserClient(client); + + const stop = async () => { + setActiveTelegramUserClient(null); + await client.destroy().catch(() => undefined); + }; + + opts.abortSignal?.addEventListener( + "abort", + () => { + void stop(); + }, + { once: true }, + ); + + await client.start(); + + const dispatcher = Dispatcher.for(client); + const handleMessage = createTelegramUserMessageHandler({ + client, + cfg, + runtime, + accountId: account.accountId, + accountConfig: account.config, + }); + + dispatcher.onNewMessage(filters.chat("user"), handleMessage); + + await new Promise((resolve, reject) => { + client.onError.add((err) => { + runtime.error?.(`telegram-user client error: ${String(err)}`); + reject(err); + }); + if (opts.abortSignal?.aborted) { + resolve(); + return; + } + opts.abortSignal?.addEventListener("abort", () => resolve(), { once: true }); + }); + + await stop(); +} diff --git a/extensions/telegram-user/src/runtime.ts b/extensions/telegram-user/src/runtime.ts new file mode 100644 index 000000000..464387f19 --- /dev/null +++ b/extensions/telegram-user/src/runtime.ts @@ -0,0 +1,14 @@ +import type { PluginRuntime } from "clawdbot/plugin-sdk"; + +let runtime: PluginRuntime | null = null; + +export function setTelegramUserRuntime(next: PluginRuntime) { + runtime = next; +} + +export function getTelegramUserRuntime(): PluginRuntime { + if (!runtime) { + throw new Error("Telegram user runtime not initialized"); + } + return runtime; +} diff --git a/extensions/telegram-user/src/send.ts b/extensions/telegram-user/src/send.ts new file mode 100644 index 000000000..e52412d7f --- /dev/null +++ b/extensions/telegram-user/src/send.ts @@ -0,0 +1,128 @@ +import fs from "node:fs"; +import type { TelegramClient } from "@mtcute/node"; +import { InputMedia } from "@mtcute/core"; + +import { getTelegramUserRuntime } from "./runtime.js"; +import { resolveTelegramUserAccount } from "./accounts.js"; +import { createTelegramUserClient } from "./client.js"; +import { resolveTelegramUserSessionPath } from "./session.js"; +import type { CoreConfig } from "./types.js"; + +export type TelegramUserSendResult = { + messageId: string; + chatId: string; +}; + +export type TelegramUserSendOpts = { + client?: TelegramClient; + accountId?: string; + replyToId?: number; + mediaUrl?: string; +}; + +const normalizeTarget = (raw: string): string => { + const trimmed = raw.trim(); + if (!trimmed) throw new Error("Recipient is required for Telegram User sends"); + return trimmed + .replace(/^(telegram-user|telegram|tg):/i, "") + .replace(/^user:/i, "") + .trim(); +}; + +export function normalizeTelegramUserMessagingTarget(raw: string): string { + return normalizeTarget(raw); +} + +export function looksLikeTelegramUserTargetId(value: string): boolean { + const trimmed = value.trim(); + if (!trimmed) return false; + return /^-?\d+$/.test(trimmed) || /^@?[a-z0-9_]{5,}$/i.test(trimmed); +} + +function resolveTelegramUserPeer(target: string): number | string { + if (/^-?\d+$/.test(target)) { + const parsed = Number.parseInt(target, 10); + if (Number.isFinite(parsed)) return parsed; + } + return target; +} + +async function resolveClient(params: { + client?: TelegramClient; + cfg: CoreConfig; + accountId?: string; +}): Promise<{ client: TelegramClient; stopOnDone: boolean }> { + if (params.client) return { client: params.client, stopOnDone: false }; + const account = resolveTelegramUserAccount({ + cfg: params.cfg, + accountId: params.accountId, + }); + const apiId = account.credentials.apiId; + const apiHash = account.credentials.apiHash; + if (!apiId || !apiHash) { + throw new Error("Telegram user credentials missing (apiId/apiHash required)."); + } + const storagePath = resolveTelegramUserSessionPath(account.accountId); + if (!fs.existsSync(storagePath)) { + throw new Error( + "Telegram user session missing. Run `clawdbot channels login --channel telegram-user` first.", + ); + } + const client = createTelegramUserClient({ apiId, apiHash, storagePath }); + await client.start(); + return { client, stopOnDone: true }; +} + +export async function sendMessageTelegramUser( + to: string, + text: string, + opts: TelegramUserSendOpts = {}, +): Promise { + const cfg = getTelegramUserRuntime().config.loadConfig() as CoreConfig; + const { client, stopOnDone } = await resolveClient({ + client: opts.client, + cfg, + accountId: opts.accountId, + }); + try { + const target = resolveTelegramUserPeer(normalizeTarget(to)); + const message = await client.sendText(target, text, { + ...(opts.replyToId ? { replyTo: opts.replyToId } : {}), + }); + return { messageId: String(message.id), chatId: String(target) }; + } finally { + if (stopOnDone) { + await client.destroy(); + } + } +} + +export async function sendMediaTelegramUser( + to: string, + text: string, + opts: TelegramUserSendOpts & { mediaUrl: string; maxBytes?: number }, +): Promise { + const cfg = getTelegramUserRuntime().config.loadConfig() as CoreConfig; + const { client, stopOnDone } = await resolveClient({ + client: opts.client, + cfg, + accountId: opts.accountId, + }); + try { + const target = resolveTelegramUserPeer(normalizeTarget(to)); + const media = await getTelegramUserRuntime().media.loadWebMedia(opts.mediaUrl, opts.maxBytes); + const input = InputMedia.auto(media.buffer, { + fileName: media.fileName ?? undefined, + fileMime: media.contentType, + caption: text, + }); + const message = await client.sendMedia(target, input, { + ...(opts.replyToId ? { replyTo: opts.replyToId } : {}), + }); + return { messageId: String(message.id), chatId: String(target) }; + } finally { + if (stopOnDone) { + await client.destroy(); + } + } +} diff --git a/extensions/telegram-user/src/session.ts b/extensions/telegram-user/src/session.ts new file mode 100644 index 000000000..23874658c --- /dev/null +++ b/extensions/telegram-user/src/session.ts @@ -0,0 +1,20 @@ +import fs from "node:fs"; +import path from "node:path"; + +import { normalizeAccountId } from "clawdbot/plugin-sdk"; +import { getTelegramUserRuntime } from "./runtime.js"; + +export function resolveTelegramUserSessionPath(accountId?: string | null): string { + const normalized = normalizeAccountId(accountId); + const stateDir = getTelegramUserRuntime().state.resolveStateDir(); + return path.join(stateDir, "telegram-user", `session-${normalized}.sqlite`); +} + +export function ensureTelegramUserSessionDir(params?: { + accountId?: string | null; + sessionPath?: string; +}): void { + const sessionPath = + params?.sessionPath ?? resolveTelegramUserSessionPath(params?.accountId); + fs.mkdirSync(path.dirname(sessionPath), { recursive: true }); +} diff --git a/extensions/telegram-user/src/types.ts b/extensions/telegram-user/src/types.ts new file mode 100644 index 000000000..2c7439af7 --- /dev/null +++ b/extensions/telegram-user/src/types.ts @@ -0,0 +1,31 @@ +export type DmPolicy = "pairing" | "allowlist" | "open" | "disabled"; + +export type TelegramUserAccountConfig = { + /** Optional display name for this account (used in CLI/UI lists). */ + name?: string; + /** If false, do not start this Telegram user account. Default: true. */ + enabled?: boolean; + /** Telegram API ID from my.telegram.org. */ + apiId?: number; + /** Telegram API hash from my.telegram.org. */ + apiHash?: string; + /** Direct message access policy (default: pairing). */ + dmPolicy?: DmPolicy; + /** Allowlist for DM senders (user ids or usernames, or "*"). */ + allowFrom?: Array; + /** Outbound text chunk size (chars). Default: 4000. */ + textChunkLimit?: number; + /** Max outbound media size in MB. */ + mediaMaxMb?: number; +}; + +export type TelegramUserConfig = TelegramUserAccountConfig & { + accounts?: Record; +}; + +export type CoreConfig = { + channels?: { + "telegram-user"?: TelegramUserConfig; + }; + [key: string]: unknown; +}; diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index d640b389f..44391a9f2 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -36,6 +36,8 @@ export type ChannelSetupInput = { audienceType?: string; audience?: string; useEnv?: boolean; + apiId?: number; + apiHash?: string; homeserver?: string; userId?: string; accessToken?: string; diff --git a/src/cli/channels-cli.ts b/src/cli/channels-cli.ts index dd60016d4..54d064883 100644 --- a/src/cli/channels-cli.ts +++ b/src/cli/channels-cli.ts @@ -39,6 +39,8 @@ const optionNamesAdd = [ "audienceType", "audience", "useEnv", + "apiId", + "apiHash", "homeserver", "userId", "accessToken", @@ -175,6 +177,8 @@ export function registerChannelsCli(program: Command) { .option("--webhook-url ", "Google Chat webhook URL") .option("--audience-type ", "Google Chat audience type (app-url|project-number)") .option("--audience ", "Google Chat audience value (app URL or project number)") + .option("--api-id ", "Telegram user API id (my.telegram.org)") + .option("--api-hash ", "Telegram user API hash (my.telegram.org)") .option("--homeserver ", "Matrix homeserver URL") .option("--user-id ", "Matrix user ID") .option("--access-token ", "Matrix access token") diff --git a/src/commands/channels/add-mutators.ts b/src/commands/channels/add-mutators.ts index f6d9d3a56..fe25c5cdb 100644 --- a/src/commands/channels/add-mutators.ts +++ b/src/commands/channels/add-mutators.ts @@ -40,6 +40,8 @@ export function applyChannelAccountConfig(params: { audienceType?: string; audience?: string; useEnv?: boolean; + apiId?: number; + apiHash?: string; homeserver?: string; userId?: string; accessToken?: string; @@ -77,6 +79,8 @@ export function applyChannelAccountConfig(params: { audienceType: params.audienceType, audience: params.audience, useEnv: params.useEnv, + apiId: params.apiId, + apiHash: params.apiHash, homeserver: params.homeserver, userId: params.userId, accessToken: params.accessToken, diff --git a/src/commands/channels/add.ts b/src/commands/channels/add.ts index 274df1775..dbbd93592 100644 --- a/src/commands/channels/add.ts +++ b/src/commands/channels/add.ts @@ -37,6 +37,8 @@ export type ChannelsAddOptions = { audienceType?: string; audience?: string; useEnv?: boolean; + apiId?: string | number; + apiHash?: string; homeserver?: string; userId?: string; accessToken?: string; @@ -181,7 +183,12 @@ export async function channelsAddCommand( : undefined; const groupChannels = parseList(opts.groupChannels); const dmAllowlist = parseList(opts.dmAllowlist); - + const apiId = + typeof opts.apiId === "number" + ? opts.apiId + : typeof opts.apiId === "string" && opts.apiId.trim() + ? Number.parseInt(opts.apiId, 10) + : undefined; const validationError = plugin.setup.validateInput?.({ cfg: nextConfig, accountId, @@ -204,6 +211,8 @@ export async function channelsAddCommand( webhookUrl: opts.webhookUrl, audienceType: opts.audienceType, audience: opts.audience, + apiId, + apiHash: opts.apiHash, homeserver: opts.homeserver, userId: opts.userId, accessToken: opts.accessToken, @@ -247,6 +256,8 @@ export async function channelsAddCommand( webhookUrl: opts.webhookUrl, audienceType: opts.audienceType, audience: opts.audience, + apiId, + apiHash: opts.apiHash, homeserver: opts.homeserver, userId: opts.userId, accessToken: opts.accessToken, From a4bea1416256a58992af6fe18ec66d374754e2fc Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 04:29:13 +0000 Subject: [PATCH 02/46] Telegram-user: add phone code login --- extensions/telegram-user/src/login.ts | 73 ++++++++++++++++++++------- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/extensions/telegram-user/src/login.ts b/extensions/telegram-user/src/login.ts index b26fd911f..1f7a06553 100644 --- a/extensions/telegram-user/src/login.ts +++ b/extensions/telegram-user/src/login.ts @@ -1,9 +1,21 @@ import qrcode from "qrcode-terminal"; +import { createInterface } from "node:readline/promises"; +import { stdin as input, stdout as output } from "node:process"; import type { RuntimeEnv } from "clawdbot/plugin-sdk"; import { createTelegramUserClient } from "./client.js"; import { ensureTelegramUserSessionDir } from "./session.js"; +async function promptText(message: string): Promise { + const rl = createInterface({ input, output }); + try { + const value = await rl.question(message); + return value.trim(); + } finally { + rl.close(); + } +} + export async function loginTelegramUser(params: { apiId: number; apiHash: string; @@ -15,25 +27,52 @@ export async function loginTelegramUser(params: { const client = createTelegramUserClient({ apiId, apiHash, storagePath }); let lastUrl = ""; - const password = process.env.TELEGRAM_USER_PASSWORD?.trim() || undefined; + const passwordEnv = process.env.TELEGRAM_USER_PASSWORD?.trim() || undefined; + const phoneEnv = process.env.TELEGRAM_USER_PHONE?.trim() || undefined; + const codeEnv = process.env.TELEGRAM_USER_CODE?.trim() || undefined; try { - const user = await client.start({ - qrCodeHandler: (url, expires) => { - if (url === lastUrl) return; - lastUrl = url; - runtime.log(`Scan this QR in Telegram (expires ${expires.toLocaleTimeString()}):`); - qrcode.generate(url, { small: true }); - }, - ...(password ? { password } : {}), - invalidCodeCallback: async (type) => { - if (type === "password") { - runtime.error?.( - "Telegram 2FA password rejected. Set TELEGRAM_USER_PASSWORD and rerun.", - ); - } - }, - }); + const user = await client.start( + phoneEnv + ? { + phone: phoneEnv, + code: codeEnv ? codeEnv : async () => await promptText("Telegram code: "), + password: passwordEnv ? passwordEnv : async () => await promptText("2FA password: "), + codeSentCallback: (code) => { + runtime.log( + `Telegram code sent via ${code.type}. Check your device and enter it here.`, + ); + }, + invalidCodeCallback: async (type) => { + if (type === "password" && passwordEnv) { + runtime.error?.( + "Telegram 2FA password rejected. Update TELEGRAM_USER_PASSWORD and rerun.", + ); + } + if (type === "code" && codeEnv) { + runtime.error?.( + "Telegram code rejected. Update TELEGRAM_USER_CODE and rerun.", + ); + } + }, + } + : { + qrCodeHandler: (url, expires) => { + if (url === lastUrl) return; + lastUrl = url; + runtime.log(`Scan this QR in Telegram (expires ${expires.toLocaleTimeString()}):`); + qrcode.generate(url, { small: true }); + }, + ...(passwordEnv ? { password: passwordEnv } : {}), + invalidCodeCallback: async (type) => { + if (type === "password") { + runtime.error?.( + "Telegram 2FA password rejected. Set TELEGRAM_USER_PASSWORD and rerun.", + ); + } + }, + }, + ); runtime.log(`Telegram user logged in as ${user.displayName}.`); } finally { await client.destroy(); From b953e0bff8a00cf4766c55f8f8cb0f6cf035d51f Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 04:33:46 +0000 Subject: [PATCH 03/46] 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, + }, + }, + }), +}; From 055c4f17a5efc431e71df09b4340bba0b82735fa Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 04:42:50 +0000 Subject: [PATCH 04/46] Telegram-user: fix onboarding parse error --- extensions/telegram-user/src/onboarding.ts | 48 +++++++++++----------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/extensions/telegram-user/src/onboarding.ts b/extensions/telegram-user/src/onboarding.ts index 5380188e5..4c8e9ae85 100644 --- a/extensions/telegram-user/src/onboarding.ts +++ b/extensions/telegram-user/src/onboarding.ts @@ -19,6 +19,7 @@ import { import type { CoreConfig } from "./types.js"; const channel = "telegram-user" as const; +type TelegramUserChannelConfig = NonNullable["telegram-user"]; function setTelegramUserDmPolicy( cfg: ClawdbotConfig, @@ -26,23 +27,22 @@ function setTelegramUserDmPolicy( accountId?: string, ): ClawdbotConfig { const resolvedAccountId = normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID; + const current = cfg.channels?.["telegram-user"] as TelegramUserChannelConfig | undefined; const allowFrom = policy === "open" - ? addWildcardAllowFrom( - (cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"])?.allowFrom, - ) + ? addWildcardAllowFrom(current?.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 } : {}), - }, + channels: { + ...cfg.channels, + "telegram-user": { + ...(current ?? {}), + dmPolicy: policy, + ...(allowFrom ? { allowFrom } : {}), + }, }, }; } @@ -52,13 +52,11 @@ function setTelegramUserDmPolicy( channels: { ...cfg.channels, "telegram-user": { - ...(cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"]), + ...(current ?? {}), accounts: { - ...((cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"]) - ?.accounts ?? {}), + ...(current?.accounts ?? {}), [resolvedAccountId]: { - ...((cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"]) - ?.accounts?.[resolvedAccountId] ?? {}), + ...(current?.accounts?.[resolvedAccountId] ?? {}), dmPolicy: policy, ...(allowFrom ? { allowFrom } : {}), }, @@ -118,6 +116,7 @@ async function promptTelegramUserAllowFrom(params: { ...parsed, ]; const unique = [...new Set(merged)]; + const current = params.cfg.channels?.["telegram-user"] as TelegramUserChannelConfig | undefined; if (accountId === DEFAULT_ACCOUNT_ID) { return { @@ -125,7 +124,7 @@ async function promptTelegramUserAllowFrom(params: { channels: { ...params.cfg.channels, "telegram-user": { - ...(params.cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"]), + ...(current ?? {}), enabled: true, dmPolicy: "allowlist", allowFrom: unique, @@ -139,14 +138,12 @@ async function promptTelegramUserAllowFrom(params: { channels: { ...params.cfg.channels, "telegram-user": { - ...(params.cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"]), + ...(current ?? {}), enabled: true, accounts: { - ...((params.cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"]) - ?.accounts ?? {}), + ...(current?.accounts ?? {}), [accountId]: { - ...((params.cfg.channels?.["telegram-user"] as CoreConfig["channels"]?.["telegram-user"]) - ?.accounts?.[accountId] ?? {}), + ...(current?.accounts?.[accountId] ?? {}), enabled: true, dmPolicy: "allowlist", allowFrom: unique, @@ -261,13 +258,14 @@ export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = { } } + const current = next.channels?.["telegram-user"] as TelegramUserChannelConfig | undefined; if (resolvedAccountId === DEFAULT_ACCOUNT_ID) { next = { ...next, channels: { ...next.channels, "telegram-user": { - ...next.channels?.["telegram-user"], + ...(current ?? {}), enabled: true, ...(useEnv ? {} @@ -284,12 +282,12 @@ export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = { channels: { ...next.channels, "telegram-user": { - ...next.channels?.["telegram-user"], + ...(current ?? {}), enabled: true, accounts: { - ...next.channels?.["telegram-user"]?.accounts, + ...(current?.accounts ?? {}), [resolvedAccountId]: { - ...next.channels?.["telegram-user"]?.accounts?.[resolvedAccountId], + ...(current?.accounts?.[resolvedAccountId] ?? {}), enabled: true, ...(useEnv ? {} From 1a77217ba615b20af2c807dee04490d214a92221 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 04:50:37 +0000 Subject: [PATCH 05/46] Telegram-user: prompt login during onboarding --- extensions/telegram-user/src/onboarding.ts | 57 +++++++++++++++++++--- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/extensions/telegram-user/src/onboarding.ts b/extensions/telegram-user/src/onboarding.ts index 4c8e9ae85..6ac988a8d 100644 --- a/extensions/telegram-user/src/onboarding.ts +++ b/extensions/telegram-user/src/onboarding.ts @@ -16,6 +16,8 @@ import { resolveDefaultTelegramUserAccountId, resolveTelegramUserAccount, } from "./accounts.js"; +import { loginTelegramUser } from "./login.js"; +import { resolveTelegramUserSessionPath } from "./session.js"; import type { CoreConfig } from "./types.js"; const channel = "telegram-user" as const; @@ -182,7 +184,14 @@ export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = { selectionHint: configured ? "configured" : "needs credentials", }; }, - configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds, forceAllowFrom }) => { + configure: async ({ + cfg, + runtime, + prompter, + accountOverrides, + shouldPromptAccountIds, + forceAllowFrom, + }) => { const override = accountOverrides["telegram-user"]?.trim(); const defaultAccountId = resolveDefaultTelegramUserAccountId(cfg as CoreConfig); let accountId = override ? normalizeAccountId(override) : defaultAccountId; @@ -310,13 +319,45 @@ export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = { }); } - await prompter.note( - [ - "Next: link the account via QR or phone code.", - "Run: clawdbot channels login --channel telegram-user", - ].join("\n"), - "Telegram user login", - ); + const wantsLogin = await prompter.confirm({ + message: "Link Telegram user now (QR or phone code)?", + initialValue: !configured, + }); + if (wantsLogin) { + const refreshed = resolveTelegramUserAccount({ + cfg: next, + accountId: resolvedAccountId, + }); + if (!refreshed.credentials.apiId || !refreshed.credentials.apiHash) { + await prompter.note( + "Telegram API ID/hash missing. Add credentials first, then retry login.", + "Telegram user login", + ); + } else { + try { + await loginTelegramUser({ + apiId: refreshed.credentials.apiId, + apiHash: refreshed.credentials.apiHash, + storagePath: resolveTelegramUserSessionPath(resolvedAccountId), + runtime, + }); + } catch (err) { + runtime.error(`Telegram user login failed: ${String(err)}`); + await prompter.note( + `Run \`clawdbot channels login --channel telegram-user\` later to link.`, + "Telegram user login", + ); + } + } + } else { + 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 }; }, From a266cd7be4f60be16f33ee0f5270b60d436c645b Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 04:57:39 +0000 Subject: [PATCH 06/46] Telegram-user: add logout support --- extensions/telegram-user/src/channel.ts | 99 +++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index dadfb7674..cf6ec193c 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -313,5 +313,104 @@ export const telegramUserPlugin: ChannelPlugin = { setActiveTelegramUserClient(null); } }, + logoutAccount: async ({ accountId, cfg, runtime }) => { + const sessionPath = resolveTelegramUserSessionPath(accountId); + let cleared = false; + if (fs.existsSync(sessionPath)) { + try { + fs.rmSync(sessionPath, { force: true }); + cleared = true; + } catch (err) { + runtime.error?.(`Failed to remove Telegram user session: ${String(err)}`); + } + } + + const nextCfg = { ...cfg } as ClawdbotConfig; + const nextSection = cfg.channels?.["telegram-user"] + ? { ...cfg.channels["telegram-user"] } + : undefined; + let changed = false; + + if (nextSection) { + if (accountId === DEFAULT_ACCOUNT_ID) { + if ("apiId" in nextSection) { + if (nextSection.apiId) cleared = true; + delete nextSection.apiId; + changed = true; + } + if ("apiHash" in nextSection) { + if (nextSection.apiHash) cleared = true; + delete nextSection.apiHash; + changed = true; + } + } + + const accounts = + nextSection.accounts && typeof nextSection.accounts === "object" + ? { ...nextSection.accounts } + : undefined; + if (accounts && accountId in accounts) { + const entry = accounts[accountId]; + if (entry && typeof entry === "object") { + const nextEntry = { ...entry } as Record; + if ("apiId" in nextEntry) { + const apiId = nextEntry.apiId; + if (typeof apiId === "number" && Number.isFinite(apiId)) { + cleared = true; + } + delete nextEntry.apiId; + changed = true; + } + if ("apiHash" in nextEntry) { + const apiHash = nextEntry.apiHash; + if (typeof apiHash === "string" ? apiHash.trim() : apiHash) { + cleared = true; + } + delete nextEntry.apiHash; + changed = true; + } + if (Object.keys(nextEntry).length === 0) { + delete accounts[accountId]; + changed = true; + } else { + accounts[accountId] = nextEntry as typeof entry; + } + } + } + if (accounts) { + if (Object.keys(accounts).length === 0) { + delete nextSection.accounts; + changed = true; + } else { + nextSection.accounts = accounts; + } + } + } + + if (changed) { + if (nextSection && Object.keys(nextSection).length > 0) { + nextCfg.channels = { ...nextCfg.channels, "telegram-user": nextSection }; + } else { + const nextChannels = { ...nextCfg.channels }; + delete nextChannels["telegram-user"]; + if (Object.keys(nextChannels).length > 0) { + nextCfg.channels = nextChannels; + } else { + delete nextCfg.channels; + } + } + await getTelegramUserRuntime().config.writeConfigFile(nextCfg); + } + + const envApiId = process.env.TELEGRAM_USER_API_ID?.trim(); + const envApiHash = process.env.TELEGRAM_USER_API_HASH?.trim(); + const loggedOut = !fs.existsSync(sessionPath); + + return { + cleared, + loggedOut, + envCredentials: Boolean(envApiId && envApiHash), + }; + }, }, }; From 26ac86cbb5aaa782f823d1dfbb9a5f9c28c42b5e Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 05:15:34 +0000 Subject: [PATCH 07/46] Telegram-user: prompt login mode --- extensions/telegram-user/src/login.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/extensions/telegram-user/src/login.ts b/extensions/telegram-user/src/login.ts index 1f7a06553..b7c70305f 100644 --- a/extensions/telegram-user/src/login.ts +++ b/extensions/telegram-user/src/login.ts @@ -16,6 +16,14 @@ async function promptText(message: string): Promise { } } +async function promptLoginMode(): Promise<"qr" | "phone"> { + const response = await promptText("Login method (qr/phone) [qr]: "); + const normalized = response.trim().toLowerCase(); + if (!normalized) return "qr"; + if (normalized === "phone" || normalized === "otp") return "phone"; + return "qr"; +} + export async function loginTelegramUser(params: { apiId: number; apiHash: string; @@ -28,10 +36,16 @@ export async function loginTelegramUser(params: { let lastUrl = ""; const passwordEnv = process.env.TELEGRAM_USER_PASSWORD?.trim() || undefined; - const phoneEnv = process.env.TELEGRAM_USER_PHONE?.trim() || undefined; + let phoneEnv = process.env.TELEGRAM_USER_PHONE?.trim() || undefined; const codeEnv = process.env.TELEGRAM_USER_CODE?.trim() || undefined; try { + if (!phoneEnv) { + const mode = await promptLoginMode(); + if (mode === "phone") { + phoneEnv = await promptText("Telegram phone number (E.164): "); + } + } const user = await client.start( phoneEnv ? { From c9fe83b3f732fc7e8fa6885b23dcd8202db29d48 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 05:22:52 +0000 Subject: [PATCH 08/46] Telegram-user: add login method picker --- extensions/telegram-user/package.json | 1 + extensions/telegram-user/src/login.ts | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/extensions/telegram-user/package.json b/extensions/telegram-user/package.json index 54f2c7eaa..65f64a576 100644 --- a/extensions/telegram-user/package.json +++ b/extensions/telegram-user/package.json @@ -28,6 +28,7 @@ "@mtcute/core": "^0.27.6", "@mtcute/dispatcher": "^0.27.6", "@mtcute/node": "^0.27.6", + "@clack/prompts": "^0.8.2", "qrcode-terminal": "^0.12.0", "clawdbot": "workspace:*" } diff --git a/extensions/telegram-user/src/login.ts b/extensions/telegram-user/src/login.ts index b7c70305f..f2d8cbc90 100644 --- a/extensions/telegram-user/src/login.ts +++ b/extensions/telegram-user/src/login.ts @@ -1,6 +1,7 @@ import qrcode from "qrcode-terminal"; import { createInterface } from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; +import { isCancel, select } from "@clack/prompts"; import type { RuntimeEnv } from "clawdbot/plugin-sdk"; import { createTelegramUserClient } from "./client.js"; @@ -17,11 +18,17 @@ async function promptText(message: string): Promise { } async function promptLoginMode(): Promise<"qr" | "phone"> { - const response = await promptText("Login method (qr/phone) [qr]: "); - const normalized = response.trim().toLowerCase(); - if (!normalized) return "qr"; - if (normalized === "phone" || normalized === "otp") return "phone"; - return "qr"; + if (!input.isTTY || !output.isTTY) return "qr"; + const response = await select({ + message: "Telegram login method", + options: [ + { value: "qr", label: "QR code (scan with Telegram)" }, + { value: "phone", label: "Phone code (SMS/Telegram)" }, + ], + initialValue: "qr", + }); + if (isCancel(response)) return "qr"; + return response; } export async function loginTelegramUser(params: { From d2b3a5102ece0e627e3ec7ebb3dac0622f556dc0 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 06:19:44 +0000 Subject: [PATCH 09/46] Telegram-user: add ack reactions + fix typing --- .../telegram-user/src/monitor/handler.ts | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index e8d2a0046..63f7f69aa 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -2,6 +2,7 @@ import type { TelegramClient } from "@mtcute/node"; import type { MessageContext } from "@mtcute/dispatcher"; import type { RuntimeEnv } from "clawdbot/plugin-sdk"; +import { resolveAckReaction } from "clawdbot/plugin-sdk"; import { getTelegramUserRuntime } from "../runtime.js"; import type { CoreConfig, TelegramUserAccountConfig } from "../types.js"; import { sendMediaTelegramUser, sendMessageTelegramUser } from "../send.js"; @@ -57,6 +58,14 @@ function isSenderAllowed(params: { return parsed.usernames.has(username.replace(/^@/, "")); } +function resolveTelegramUserPeer(target: string): number | string { + if (/^-?\d+$/.test(target)) { + const parsed = Number.parseInt(target, 10); + if (Number.isFinite(parsed)) return parsed; + } + return target; +} + async function resolveMediaAttachment(params: { client: TelegramClient; mediaMaxMb: number; @@ -110,6 +119,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara if ("isSelf" in sender && sender.isSelf) return; const senderId = String(sender.id); + const senderPeer = resolveTelegramUserPeer(senderId); const senderUsername = "username" in sender ? sender.username : null; const senderName = "displayName" in sender ? sender.displayName : senderId; const storeAllowFrom = await core.channel.pairing @@ -170,6 +180,24 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara id: senderId, }, }); + const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions"; + const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false; + const ackReaction = resolveAckReaction(cfg, route.agentId); + const shouldAckReaction = + Boolean(ackReaction) && (ackReactionScope === "all" || ackReactionScope === "direct"); + const ackReactionPromise = shouldAckReaction + ? client + .sendReaction({ + chatId: senderPeer, + message: msg.id, + emoji: ackReaction, + }) + .then(() => true) + .catch((err) => { + runtime.error?.(`telegram-user ack reaction failed: ${String(err)}`); + return false; + }) + : null; const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId: route.agentId, }); @@ -278,7 +306,9 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara } }, onReplyStart: async () => { - await client.sendTyping(senderId).catch(() => undefined); + await client.sendTyping(senderPeer).catch((err) => { + runtime.error?.(`telegram-user typing failed: ${String(err)}`); + }); }, onError: (err) => { runtime.error?.(`telegram-user reply failed: ${String(err)}`); @@ -292,6 +322,21 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara replyOptions, }); markDispatchIdle(); + + if (removeAckAfterReply && ackReactionPromise) { + const didAck = await ackReactionPromise; + if (didAck) { + await client + .sendReaction({ + chatId: senderPeer, + message: msg.id, + emoji: null, + }) + .catch((err) => { + runtime.error?.(`telegram-user ack reaction cleanup failed: ${String(err)}`); + }); + } + } } catch (err) { runtime.error?.(`telegram-user handler failed: ${String(err)}`); } From bd3a8c3c91c4cce51fc9c7db6082616559fc4d4e Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Sat, 24 Jan 2026 00:57:51 +0000 Subject: [PATCH 10/46] deps: add telegram-user lockfile --- pnpm-lock.yaml | 349 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 339 insertions(+), 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95b940c97..091cf70b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -415,6 +415,27 @@ importers: extensions/telegram: {} + extensions/telegram-user: + dependencies: + '@clack/prompts': + specifier: ^0.8.2 + version: 0.8.2 + '@mtcute/core': + specifier: ^0.27.6 + version: 0.27.6 + '@mtcute/dispatcher': + specifier: ^0.27.6 + version: 0.27.6 + '@mtcute/node': + specifier: ^0.27.6 + version: 0.27.6(ws@8.19.0) + clawdbot: + specifier: workspace:* + version: link:../.. + qrcode-terminal: + specifier: ^0.12.0 + version: 0.12.0 + extensions/tlon: dependencies: '@urbit/aura': @@ -832,12 +853,18 @@ packages: '@cacheable/utils@2.3.3': resolution: {integrity: sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A==} + '@clack/core@0.3.5': + resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} + '@clack/core@0.5.0': resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} '@clack/prompts@0.11.0': resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} + '@clack/prompts@0.8.2': + resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} + '@cloudflare/workers-types@4.20260120.0': resolution: {integrity: sha512-B8pueG+a5S+mdK3z8oKu1ShcxloZ7qWb68IEyLLaepvdryIbNC7JVPcY0bWsjS56UQVKc5fnyRge3yZIwc9bxw==} @@ -1046,6 +1073,26 @@ packages: '@eshaz/web-worker@1.2.2': resolution: {integrity: sha512-WxXiHFmD9u/owrzempiDlBB1ZYqiLnm9s6aPc8AlFQalq2tKmqdmMr9GXOupDgzXtqnBipj8Un0gkIm7Sjf8mw==} + '@fuman/io@0.0.17': + resolution: {integrity: sha512-VmMnfHtXzBfEddEfptn/oYshUzWqW2XUkdVnwKuHWphEQTQZrOWxC7G12FI9U2EhEYt4nRdrUTYk65U8GVJWYw==} + + '@fuman/net@0.0.17': + resolution: {integrity: sha512-x/kK3kWQ+gy5rfsoS6QVCsodh9n/XJeM3c6m1YHPUiQ0gWWQd4CC1bcQ/rh2UHh9DQyJJeWjCQXWH2xmsVCcFQ==} + + '@fuman/node@0.0.17': + resolution: {integrity: sha512-XXRlJthuCnJBnIrg/tZcqCfv/cPuXuNOVUN521oJgKrW8FyFmt+lAt2MlYw3TROumGNRMtvn3ySjdQRpBT2sLw==} + peerDependencies: + ws: ^8.18.1 + peerDependenciesMeta: + ws: + optional: true + + '@fuman/utils@0.0.15': + resolution: {integrity: sha512-3H3WzkfG7iLKCa/yNV4s80lYD4yr5hgiNzU13ysLY2BcDqFjM08XGYuLd5wFVp4V8+DA/fe8gIDW96To/JwDyA==} + + '@fuman/utils@0.0.17': + resolution: {integrity: sha512-hy1Xu1146nOspVam8FC6p4yakb1FV1V3KrS85RzcHiK7AccFKR43Fgtv8exC8Ybsw6MtMU+MRNyaPqVhA+7TsA==} + '@glideapps/ts-necessities@2.2.3': resolution: {integrity: sha512-gXi0awOZLHk3TbW55GZLCPP6O+y/b5X1pBXKBVckFONSwF1z1E5ND2BGJsghQFah+pW7pkkyFb2VhUQI2qhL5w==} @@ -1493,6 +1540,33 @@ packages: resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==} engines: {node: '>=14.0.0'} + '@mtcute/core@0.27.6': + resolution: {integrity: sha512-OqjQ2hchF15yJAjcAgBuWx4RCnvMED0P3kiUfU3EsDMMIJlh8TgOgm0QspIda/Uz7icZ5+pi0rHyCYblw2MMKw==} + + '@mtcute/dispatcher@0.27.6': + resolution: {integrity: sha512-5ZmI5cmyeWVYY5BtPlGYB0b4oxmF86xiP/c1Wo3VQ0SElYuknf+xZDlFxt6AMlHk9d/v2rxEd+dBXB5kRczpUA==} + + '@mtcute/file-id@0.27.6': + resolution: {integrity: sha512-ZSPxbGjS6YdcZv4xW0zHJ/iR28nEBisG3G6gDTwVS4gU51SJ4vlGcwGjF1uLyEeuGGbPFemQHLCP6CMSzqMRvA==} + + '@mtcute/html-parser@0.27.6': + resolution: {integrity: sha512-zxTuT0nv0CBR4qy7KyKB9vGQ++DxeiofKJEwHFSj5oG/7qUARm21G5GZaVlel/v7oRzx6V3u9mKDzdlbv8BcxA==} + + '@mtcute/markdown-parser@0.27.6': + resolution: {integrity: sha512-YB4HXeDGQi+ilbOp1qDJ/iP3VfBFrsR+gEyQcaQo/PAR4NLtD+rZ5veWM/OSVjbawYl2OpFpfXzQdINAAlaEJg==} + + '@mtcute/node@0.27.6': + resolution: {integrity: sha512-fDnufwcRJyqMr7rpCIiSW6GIRR1j+tgM8Og1Rx38U16Ftmu3gB7Xt5K7lHJJWQHDhv59w8AiU+NksiPdTXbxxQ==} + + '@mtcute/tl-runtime@0.24.3': + resolution: {integrity: sha512-61J3cgYgNOQT532GdIiuezRrSC7v6cc9MfvWv9GO27bRGf7JUKWVbFt4U0KzQ9Tp0J1uMOUfi1EbQKkYKacIKQ==} + + '@mtcute/tl@221.0.0': + resolution: {integrity: sha512-Wp01L9nznTMLl2s9rbKnzQ8pij72eF4HK2XIziOQoJXiObPKZxQdxvMj+C6l0ArxMFmpT0H0/3EL5RB7O6VPwg==} + + '@mtcute/wasm@0.27.0': + resolution: {integrity: sha512-1v4eO1N1BVRQ8L+cyUsMAeLXs5suTGXyVv/tftkbd/mGGHxc+fvOWItp3Fmq+GIwN7m4VX7kztuMMLhHxv2i2Q==} + '@napi-rs/canvas-android-arm64@0.1.88': resolution: {integrity: sha512-KEaClPnZuVxJ8smUWjV1wWFkByBO/D+vy4lN+Dm5DFH514oqwukxKGeck9xcKJhaWJGjfruGmYGiwRe//+/zQQ==} engines: {node: '>= 10'} @@ -2701,6 +2775,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/events@3.0.0': + resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==} + '@types/express-serve-static-core@4.19.8': resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} @@ -3100,6 +3177,10 @@ packages: before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + better-sqlite3@12.6.2: + resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} + bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} @@ -3107,6 +3188,12 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} @@ -3150,6 +3237,9 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -3201,6 +3291,9 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -3374,6 +3467,10 @@ packages: supports-color: optional: true + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -3463,6 +3560,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -3531,6 +3631,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -3590,6 +3694,9 @@ packages: resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} engines: {node: '>=20'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + filename-reserved-regex@3.0.0: resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3661,6 +3768,9 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@11.3.3: resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} @@ -3713,6 +3823,9 @@ packages: getpass@0.1.7: resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -4201,6 +4314,9 @@ packages: long@4.0.0: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -4323,6 +4439,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -4348,6 +4468,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} @@ -4390,6 +4513,9 @@ packages: engines: {node: ^18 || >=20} hasBin: true + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -4398,6 +4524,10 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} + engines: {node: '>=10'} + node-addon-api@8.5.0: resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} engines: {node: ^18 || ^20 || >= 21} @@ -4723,6 +4853,11 @@ packages: resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} engines: {node: '>=12'} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + pretty-bytes@6.1.1: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} @@ -4787,6 +4922,9 @@ packages: psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -5030,6 +5168,12 @@ packages: peerDependencies: signal-polyfill: ^0.2.0 + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + simple-git@3.30.0: resolution: {integrity: sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==} @@ -5191,6 +5335,13 @@ packages: tailwindcss@4.1.17: resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + tar@7.5.4: resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==} engines: {node: '>=18'} @@ -6472,6 +6623,11 @@ snapshots: hashery: 1.4.0 keyv: 5.6.0 + '@clack/core@0.3.5': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@clack/core@0.5.0': dependencies: picocolors: 1.1.1 @@ -6483,6 +6639,12 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@clack/prompts@0.8.2': + dependencies: + '@clack/core': 0.3.5 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@cloudflare/workers-types@4.20260120.0': optional: true @@ -6647,6 +6809,27 @@ snapshots: '@eshaz/web-worker@1.2.2': optional: true + '@fuman/io@0.0.17': + dependencies: + '@fuman/utils': 0.0.17 + + '@fuman/net@0.0.17': + dependencies: + '@fuman/io': 0.0.17 + '@fuman/utils': 0.0.17 + + '@fuman/node@0.0.17(ws@8.19.0)': + dependencies: + '@fuman/io': 0.0.17 + '@fuman/net': 0.0.17 + '@fuman/utils': 0.0.17 + optionalDependencies: + ws: 8.19.0 + + '@fuman/utils@0.0.15': {} + + '@fuman/utils@0.0.17': {} + '@glideapps/ts-necessities@2.2.3': {} '@google/genai@1.34.0': @@ -7113,6 +7296,63 @@ snapshots: '@mozilla/readability@0.6.0': {} + '@mtcute/core@0.27.6': + dependencies: + '@fuman/io': 0.0.17 + '@fuman/net': 0.0.17 + '@fuman/utils': 0.0.17 + '@mtcute/file-id': 0.27.6 + '@mtcute/tl': 221.0.0 + '@mtcute/tl-runtime': 0.24.3 + '@types/events': 3.0.0 + long: 5.2.3 + + '@mtcute/dispatcher@0.27.6': + dependencies: + '@fuman/utils': 0.0.17 + '@mtcute/core': 0.27.6 + + '@mtcute/file-id@0.27.6': + dependencies: + '@fuman/utils': 0.0.17 + '@mtcute/tl-runtime': 0.24.3 + long: 5.2.3 + + '@mtcute/html-parser@0.27.6': + dependencies: + '@mtcute/core': 0.27.6 + htmlparser2: 10.1.0 + long: 5.2.3 + + '@mtcute/markdown-parser@0.27.6': + dependencies: + '@mtcute/core': 0.27.6 + long: 5.2.3 + + '@mtcute/node@0.27.6(ws@8.19.0)': + dependencies: + '@fuman/net': 0.0.17 + '@fuman/node': 0.0.17(ws@8.19.0) + '@fuman/utils': 0.0.17 + '@mtcute/core': 0.27.6 + '@mtcute/html-parser': 0.27.6 + '@mtcute/markdown-parser': 0.27.6 + '@mtcute/wasm': 0.27.0 + better-sqlite3: 12.6.2 + transitivePeerDependencies: + - ws + + '@mtcute/tl-runtime@0.24.3': + dependencies: + '@fuman/utils': 0.0.15 + long: 5.2.3 + + '@mtcute/tl@221.0.0': + dependencies: + long: 5.2.3 + + '@mtcute/wasm@0.27.0': {} + '@napi-rs/canvas-android-arm64@0.1.88': optional: true @@ -8464,6 +8704,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/events@3.0.0': {} + '@types/express-serve-static-core@4.19.8': dependencies: '@types/node': 25.0.10 @@ -8955,10 +9197,25 @@ snapshots: before-after-hook@4.0.0: optional: true + better-sqlite3@12.6.2: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + bignumber.js@9.3.1: {} binary-extensions@2.3.0: {} + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + bluebird@3.7.2: {} body-parser@1.20.4: @@ -9018,6 +9275,11 @@ snapshots: buffer-from@1.1.2: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -9082,6 +9344,8 @@ snapshots: dependencies: readdirp: 5.0.0 + chownr@1.1.4: {} + chownr@3.0.0: {} chromium-bidi@13.0.1(devtools-protocol@0.0.1561482): @@ -9252,8 +9516,11 @@ snapshots: dependencies: ms: 2.1.3 - deep-extend@0.6.0: - optional: true + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} deepmerge@4.3.1: {} @@ -9330,6 +9597,10 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + entities@4.5.0: {} entities@7.0.1: {} @@ -9403,6 +9674,8 @@ snapshots: events@3.3.0: {} + expand-template@2.0.3: {} + expect-type@1.3.0: {} express@4.22.1: @@ -9521,6 +9794,8 @@ snapshots: transitivePeerDependencies: - supports-color + file-uri-to-path@1.0.0: {} + filename-reserved-regex@3.0.0: optional: true @@ -9606,6 +9881,8 @@ snapshots: fresh@2.0.0: {} + fs-constants@1.0.0: {} + fs-extra@11.3.3: dependencies: graceful-fs: 4.2.11 @@ -9680,6 +9957,8 @@ snapshots: dependencies: assert-plus: 1.0.0 + github-from-package@0.0.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -9860,8 +10139,7 @@ snapshots: inherits@2.0.4: {} - ini@1.3.8: - optional: true + ini@1.3.8: {} ipaddr.js@1.9.1: {} @@ -10204,6 +10482,8 @@ snapshots: long@4.0.0: {} + long@5.2.3: {} + long@5.3.2: {} lowdb@1.0.0: @@ -10308,6 +10588,8 @@ snapshots: mimic-function@5.0.1: optional: true + mimic-response@3.1.0: {} + minimalistic-assert@1.0.1: {} minimatch@10.1.1: @@ -10318,8 +10600,7 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minimist@1.2.8: - optional: true + minimist@1.2.8: {} minipass@7.1.2: {} @@ -10329,6 +10610,8 @@ snapshots: mitt@3.0.1: {} + mkdirp-classic@0.5.3: {} + mkdirp@3.0.1: {} module-details-from-path@1.0.4: {} @@ -10380,10 +10663,16 @@ snapshots: nanoid@5.1.6: optional: true + napi-build-utils@2.0.0: {} + negotiator@0.6.3: {} negotiator@1.0.0: {} + node-abi@3.87.0: + dependencies: + semver: 7.7.3 + node-addon-api@8.5.0: optional: true @@ -10749,6 +11038,21 @@ snapshots: postgres@3.4.8: {} + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.87.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + pretty-bytes@6.1.1: optional: true @@ -10834,6 +11138,11 @@ snapshots: dependencies: punycode: 2.3.1 + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode.js@2.3.1: {} punycode@2.3.1: {} @@ -10900,7 +11209,6 @@ snapshots: ini: 1.3.8 minimist: 1.2.8 strip-json-comments: 2.0.1 - optional: true readable-stream@2.3.8: dependencies: @@ -10917,7 +11225,6 @@ snapshots: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - optional: true readable-stream@4.5.2: dependencies: @@ -11225,6 +11532,14 @@ snapshots: dependencies: signal-polyfill: 0.2.2 + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + simple-git@3.30.0: dependencies: '@kwsites/file-exists': 1.1.1 @@ -11365,8 +11680,7 @@ snapshots: dependencies: ansi-regex: 6.2.2 - strip-json-comments@2.0.1: - optional: true + strip-json-comments@2.0.1: {} strnum@2.1.2: {} @@ -11393,6 +11707,21 @@ snapshots: tailwindcss@4.1.17: {} + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + tar@7.5.4: dependencies: '@isaacs/fs-minipass': 4.0.1 From d7b7242e9e8aadb2687bde1dca56c5549bbad8df Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 06:51:03 +0000 Subject: [PATCH 11/46] Telegram-user: add group support --- extensions/telegram-user/src/channel.ts | 4 +- extensions/telegram-user/src/config-schema.ts | 24 ++ .../telegram-user/src/monitor/handler.ts | 273 +++++++++++++++--- extensions/telegram-user/src/monitor/index.ts | 14 +- extensions/telegram-user/src/types.ts | 35 ++- 5 files changed, 306 insertions(+), 44 deletions(-) diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index cf6ec193c..ce77e2ff9 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -40,7 +40,7 @@ const meta = { detailLabel: "Telegram User", docsPath: "/channels/telegram-user", docsLabel: "telegram-user", - blurb: "login as a Telegram user via QR; DM-only for now.", + blurb: "login as a Telegram user via QR or phone code; supports DMs + groups.", order: 12, quickstartAllowFrom: true, }; @@ -68,7 +68,7 @@ export const telegramUserPlugin: ChannelPlugin = { }, }, capabilities: { - chatTypes: ["direct"], + chatTypes: ["direct", "group"], reactions: false, threads: false, media: true, diff --git a/extensions/telegram-user/src/config-schema.ts b/extensions/telegram-user/src/config-schema.ts index d40d029ca..a1b791566 100644 --- a/extensions/telegram-user/src/config-schema.ts +++ b/extensions/telegram-user/src/config-schema.ts @@ -2,6 +2,27 @@ import { z } from "zod"; const allowFromEntry = z.union([z.string(), z.number()]); +const TelegramUserTopicSchema = z + .object({ + requireMention: z.boolean().optional(), + skills: z.array(z.string()).optional(), + enabled: z.boolean().optional(), + allowFrom: z.array(allowFromEntry).optional(), + systemPrompt: z.string().optional(), + }) + .strict(); + +const TelegramUserGroupSchema = z + .object({ + requireMention: z.boolean().optional(), + skills: z.array(z.string()).optional(), + topics: z.record(z.string(), TelegramUserTopicSchema.optional()).optional(), + enabled: z.boolean().optional(), + allowFrom: z.array(allowFromEntry).optional(), + systemPrompt: z.string().optional(), + }) + .strict(); + const TelegramUserAccountSchema = z .object({ name: z.string().optional(), @@ -12,6 +33,9 @@ const TelegramUserAccountSchema = z allowFrom: z.array(allowFromEntry).optional(), textChunkLimit: z.number().int().positive().optional(), mediaMaxMb: z.number().positive().optional(), + groupAllowFrom: z.array(allowFromEntry).optional(), + groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional(), + groups: z.record(z.string(), TelegramUserGroupSchema.optional()).optional(), }) .strict(); diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 63f7f69aa..01309a365 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -2,7 +2,7 @@ import type { TelegramClient } from "@mtcute/node"; import type { MessageContext } from "@mtcute/dispatcher"; import type { RuntimeEnv } from "clawdbot/plugin-sdk"; -import { resolveAckReaction } from "clawdbot/plugin-sdk"; +import { resolveAckReaction, resolveMentionGatingWithBypass } from "clawdbot/plugin-sdk"; import { getTelegramUserRuntime } from "../runtime.js"; import type { CoreConfig, TelegramUserAccountConfig } from "../types.js"; import { sendMediaTelegramUser, sendMessageTelegramUser } from "../send.js"; @@ -16,6 +16,7 @@ type TelegramUserHandlerParams = { runtime: RuntimeEnv; accountId: string; accountConfig: TelegramUserAccountConfig; + self?: { id: number; username?: string | null }; }; function normalizeAllowEntry(raw: string): string { @@ -42,7 +43,7 @@ function parseAllowlist(entries: Array | undefined) { const username = entry.startsWith("@") ? entry.slice(1) : entry; if (username) usernames.add(username); } - return { hasWildcard, usernames, ids }; + return { hasWildcard, usernames, ids, hasEntries: normalized.length > 0 }; } function isSenderAllowed(params: { @@ -66,6 +67,46 @@ function resolveTelegramUserPeer(target: string): number | string { return target; } +function firstDefined(...values: Array): T | undefined { + for (const value of values) { + if (typeof value !== "undefined") return value; + } + return undefined; +} + +function buildTelegramUserGroupPeerId(chatId: number | string, threadId?: number) { + return threadId != null ? `${chatId}:topic:${threadId}` : String(chatId); +} + +function buildTelegramUserGroupFrom(chatId: number | string, threadId?: number) { + return `telegram-user:group:${buildTelegramUserGroupPeerId(chatId, threadId)}`; +} + +function buildTelegramUserGroupLabel( + title: string | undefined, + chatId: number | string, + threadId?: number, +) { + const topicSuffix = threadId != null ? ` topic:${threadId}` : ""; + if (title) return `${title} id:${chatId}${topicSuffix}`; + return `group:${chatId}${topicSuffix}`; +} + +function resolveTelegramUserGroupConfig( + accountConfig: TelegramUserAccountConfig, + chatId: number | string, + threadId?: number, +) { + const groups = accountConfig.groups ?? {}; + const chatKey = String(chatId); + const groupConfig = groups[chatKey] ?? groups["*"]; + if (!threadId) return { groupConfig, topicConfig: undefined }; + const topicKey = String(threadId); + const topicConfig = + groupConfig?.topics?.[topicKey] ?? groups["*"]?.topics?.[topicKey]; + return { groupConfig, topicConfig }; +} + async function resolveMediaAttachment(params: { client: TelegramClient; mediaMaxMb: number; @@ -102,17 +143,21 @@ async function resolveMediaAttachment(params: { } export function createTelegramUserMessageHandler(params: TelegramUserHandlerParams) { - const { client, cfg, runtime, accountId, accountConfig } = params; + const { client, cfg, runtime, accountId, accountConfig, self } = params; const core = getTelegramUserRuntime(); const textLimit = accountConfig.textChunkLimit ?? DEFAULT_TEXT_LIMIT; const mediaMaxMb = accountConfig.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB; const dmPolicy = accountConfig.dmPolicy ?? "pairing"; const allowFrom = accountConfig.allowFrom ?? []; + const groupAllowFrom = accountConfig.groupAllowFrom ?? allowFrom; return async (msg: MessageContext) => { try { if (msg.isOutgoing || msg.isService) return; - if (msg.chat.type !== "user") return; + const isDirect = msg.chat.type === "user"; + const isGroup = + msg.chat.type === "chat" && msg.chat.chatType !== "channel"; + if (!isDirect && !isGroup) return; const sender = await msg.getCompleteSender().catch(() => msg.sender); if (sender.type !== "user") return; @@ -126,32 +171,95 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara .readAllowFromStore("telegram-user") .catch(() => []); const combinedAllowFrom = [...allowFrom, ...storeAllowFrom]; + const chatId = msg.chat.type === "chat" ? msg.chat.id : undefined; + const threadId = + isGroup && msg.isTopicMessage + ? msg.replyToMessage?.threadId ?? undefined + : undefined; + const { groupConfig, topicConfig } = + isGroup && chatId != null + ? resolveTelegramUserGroupConfig(accountConfig, chatId, threadId) + : { groupConfig: undefined, topicConfig: undefined }; - if (dmPolicy === "disabled") return; - if ( - dmPolicy !== "open" && - !isSenderAllowed({ allowFrom: combinedAllowFrom, senderId, senderUsername }) - ) { - if (dmPolicy === "pairing") { - const pairing = await core.channel.pairing.upsertPairingRequest({ - channel: "telegram-user", - id: senderId, - meta: { - username: senderUsername ?? undefined, - name: senderName, - }, + const groupAllowOverride = firstDefined( + topicConfig?.allowFrom, + groupConfig?.allowFrom, + ); + const groupAllowEntries = [ + ...((groupAllowOverride ?? groupAllowFrom) as Array), + ...storeAllowFrom, + ]; + const effectiveGroupAllow = parseAllowlist(groupAllowEntries); + const effectiveDmAllow = parseAllowlist(combinedAllowFrom); + + if (isDirect) { + if (dmPolicy === "disabled") return; + if ( + dmPolicy !== "open" && + !isSenderAllowed({ + allowFrom: combinedAllowFrom, + senderId, + senderUsername, + }) + ) { + if (dmPolicy === "pairing") { + const pairing = await core.channel.pairing.upsertPairingRequest({ + channel: "telegram-user", + id: senderId, + meta: { + username: senderUsername ?? undefined, + name: senderName, + }, + }); + const reply = core.channel.pairing.buildPairingReply({ + channel: "telegram-user", + idLine: `Telegram user id: ${senderId}`, + code: pairing.code, + }); + await sendMessageTelegramUser(`telegram-user:${senderId}`, reply, { + client, + accountId, + }); + } + return; + } + } else if (isGroup) { + if (groupConfig?.enabled === false) return; + if (topicConfig?.enabled === false) return; + if (typeof groupAllowOverride !== "undefined") { + const allowed = isSenderAllowed({ + allowFrom: groupAllowEntries, + senderId, + senderUsername, }); - const reply = core.channel.pairing.buildPairingReply({ + if (!allowed) return; + } + const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; + const groupPolicy = + accountConfig.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; + if (groupPolicy === "disabled") return; + if (groupPolicy === "allowlist") { + if (!senderId) return; + if (!effectiveGroupAllow.hasEntries) return; + if ( + !isSenderAllowed({ + allowFrom: groupAllowEntries, + senderId, + senderUsername, + }) + ) { + return; + } + } + if (chatId != null) { + const groupAllowlist = core.channel.groups.resolveGroupPolicy({ + cfg, channel: "telegram-user", - idLine: `Telegram user id: ${senderId}`, - code: pairing.code, - }); - await sendMessageTelegramUser(`telegram-user:${senderId}`, reply, { - client, + groupId: String(chatId), accountId, }); + if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) return; } - return; } const text = msg.text?.trim() ?? ""; @@ -171,24 +279,95 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara direction: "inbound", }); + const groupPeerId = + isGroup && chatId != null + ? buildTelegramUserGroupPeerId(chatId, threadId) + : null; const route = core.channel.routing.resolveAgentRoute({ cfg, channel: "telegram-user", accountId, peer: { - kind: "dm", - id: senderId, + kind: isGroup ? "group" : "dm", + id: isGroup && groupPeerId ? groupPeerId : senderId, }, }); + const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg, route.agentId); + const hasAnyMention = msg.entities.some( + (ent) => ent.kind === "mention" || ent.kind === "text_mention", + ); + const hasControlCommandInMessage = core.channel.text.hasControlCommand(text, cfg, { + botUsername: self?.username?.trim().toLowerCase(), + }); + const allowForCommands = isGroup ? effectiveGroupAllow : effectiveDmAllow; + const senderAllowedForCommands = isSenderAllowed({ + allowFrom: isGroup ? groupAllowEntries : combinedAllowFrom, + senderId, + senderUsername, + }); + const useAccessGroups = cfg.commands?.useAccessGroups !== false; + const commandAuthorized = core.channel.commands.resolveCommandAuthorizedFromAuthorizers({ + useAccessGroups, + authorizers: [{ configured: allowForCommands.hasEntries, allowed: senderAllowedForCommands }], + }); + if (isGroup && hasControlCommandInMessage && !commandAuthorized) return; + + const computedWasMentioned = + msg.isMention || core.channel.mentions.matchesMentionPatterns(text, mentionRegexes); + const baseRequireMention = isGroup + ? core.channel.groups.resolveRequireMention({ + cfg, + channel: "telegram-user", + groupId: chatId != null ? String(chatId) : undefined, + accountId, + }) + : false; + const requireMention = firstDefined( + topicConfig?.requireMention, + groupConfig?.requireMention, + baseRequireMention, + ); + const replySenderId = + msg.replyToMessage?.sender?.type === "user" + ? msg.replyToMessage.sender.id + : undefined; + const implicitMention = + isGroup && Boolean(requireMention) && self?.id != null && replySenderId === self.id; + const canDetectMention = + Boolean(self?.username) || mentionRegexes.length > 0 || msg.isMention; + const mentionGate = resolveMentionGatingWithBypass({ + isGroup, + requireMention: Boolean(requireMention), + canDetectMention, + wasMentioned: computedWasMentioned, + implicitMention, + hasAnyMention, + allowTextCommands: true, + hasControlCommand: hasControlCommandInMessage, + commandAuthorized, + }); + const effectiveWasMentioned = mentionGate.effectiveWasMentioned; + if (isGroup && requireMention && canDetectMention && mentionGate.shouldSkip) { + return; + } + const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions"; const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false; const ackReaction = resolveAckReaction(cfg, route.agentId); - const shouldAckReaction = - Boolean(ackReaction) && (ackReactionScope === "all" || ackReactionScope === "direct"); - const ackReactionPromise = shouldAckReaction + const shouldAckReaction = () => { + if (!ackReaction) return false; + if (ackReactionScope === "all") return true; + if (ackReactionScope === "direct") return !isGroup; + if (ackReactionScope === "group-all") return isGroup; + if (ackReactionScope === "group-mentions") { + return isGroup && Boolean(requireMention) && canDetectMention && effectiveWasMentioned; + } + return false; + }; + const ackReactionPromise = shouldAckReaction() ? client .sendReaction({ - chatId: senderPeer, + chatId: isGroup && chatId != null ? chatId : senderPeer, message: msg.id, emoji: ackReaction, }) @@ -206,6 +385,10 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara storePath, sessionKey: route.sessionKey, }); + const groupTitle = msg.chat.type === "chat" ? msg.chat.title : undefined; + const conversationLabel = isGroup && chatId != null + ? buildTelegramUserGroupLabel(groupTitle, chatId, threadId) + : senderName; const body = core.channel.reply.formatAgentEnvelope({ channel: "Telegram User", from: senderName, @@ -219,12 +402,14 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara Body: body, RawBody: text, CommandBody: text, - From: `telegram-user:${senderId}`, - To: `telegram-user:${senderId}`, + From: isGroup && chatId != null ? buildTelegramUserGroupFrom(chatId, threadId) : `telegram-user:${senderId}`, + To: isGroup && chatId != null ? buildTelegramUserGroupFrom(chatId, threadId) : `telegram-user:${senderId}`, SessionKey: route.sessionKey, AccountId: route.accountId, - ChatType: "direct", - ConversationLabel: senderName, + ChatType: isGroup ? "group" : "direct", + ConversationLabel: conversationLabel, + GroupSubject: isGroup ? groupTitle ?? undefined : undefined, + GroupSystemPrompt: isGroup ? groupConfig?.systemPrompt ?? undefined : undefined, SenderName: senderName, SenderId: senderId, SenderUsername: senderUsername ?? undefined, @@ -236,10 +421,14 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara MediaPath: media?.path, MediaType: media?.contentType, MediaUrl: media?.path, - CommandAuthorized: true, + CommandAuthorized: commandAuthorized, CommandSource: "text" as const, OriginatingChannel: "telegram-user" as const, - OriginatingTo: `telegram-user:${senderId}`, + OriginatingTo: + isGroup && chatId != null + ? buildTelegramUserGroupFrom(chatId, threadId) + : `telegram-user:${senderId}`, + WasMentioned: isGroup ? effectiveWasMentioned : undefined, }); void core.channel.session @@ -262,6 +451,10 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara }); let hasReplied = false; + const replyTarget = + isGroup && chatId != null ? `telegram-user:${chatId}` : `telegram-user:${senderId}`; + const typingTarget = isGroup && chatId != null ? chatId : senderPeer; + const typingParams = isGroup && threadId != null ? { threadId } : undefined; const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({ responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId) @@ -272,7 +465,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const replyText = payload.text ?? ""; const mediaUrl = payload.mediaUrl; if (mediaUrl) { - await sendMediaTelegramUser(`telegram-user:${senderId}`, replyText, { + await sendMediaTelegramUser(replyTarget, replyText, { client, accountId, replyToId, @@ -291,7 +484,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara for (const chunk of core.channel.text.chunkMarkdownText(replyText, textLimit)) { const trimmed = chunk.trim(); if (!trimmed) continue; - await sendMessageTelegramUser(`telegram-user:${senderId}`, trimmed, { + await sendMessageTelegramUser(replyTarget, trimmed, { client, accountId, replyToId, @@ -306,7 +499,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara } }, onReplyStart: async () => { - await client.sendTyping(senderPeer).catch((err) => { + await client.sendTyping(typingTarget, "typing", typingParams).catch((err) => { runtime.error?.(`telegram-user typing failed: ${String(err)}`); }); }, @@ -328,7 +521,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara if (didAck) { await client .sendReaction({ - chatId: senderPeer, + chatId: isGroup && chatId != null ? chatId : senderPeer, message: msg.id, emoji: null, }) diff --git a/extensions/telegram-user/src/monitor/index.ts b/extensions/telegram-user/src/monitor/index.ts index 55d3d4203..3aff04f2a 100644 --- a/extensions/telegram-user/src/monitor/index.ts +++ b/extensions/telegram-user/src/monitor/index.ts @@ -67,15 +67,27 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts await client.start(); const dispatcher = Dispatcher.for(client); + const self = await client.getMe().catch(() => undefined); const handleMessage = createTelegramUserMessageHandler({ client, cfg, runtime, accountId: account.accountId, accountConfig: account.config, + self: self + ? { id: self.id, username: "username" in self ? self.username : undefined } + : undefined, }); - dispatcher.onNewMessage(filters.chat("user"), handleMessage); + dispatcher.onNewMessage( + filters.or( + filters.chat("user"), + filters.chat("group"), + filters.chat("supergroup"), + filters.chat("gigagroup"), + ), + handleMessage, + ); await new Promise((resolve, reject) => { client.onError.add((err) => { diff --git a/extensions/telegram-user/src/types.ts b/extensions/telegram-user/src/types.ts index 2c7439af7..06085e1ed 100644 --- a/extensions/telegram-user/src/types.ts +++ b/extensions/telegram-user/src/types.ts @@ -1,4 +1,21 @@ -export type DmPolicy = "pairing" | "allowlist" | "open" | "disabled"; +import type { DmPolicy, GroupPolicy } from "clawdbot/plugin-sdk"; + +export type TelegramUserTopicConfig = { + requireMention?: boolean; + skills?: string[]; + enabled?: boolean; + allowFrom?: Array; + systemPrompt?: string; +}; + +export type TelegramUserGroupConfig = { + requireMention?: boolean; + skills?: string[]; + topics?: Record; + enabled?: boolean; + allowFrom?: Array; + systemPrompt?: string; +}; export type TelegramUserAccountConfig = { /** Optional display name for this account (used in CLI/UI lists). */ @@ -17,6 +34,12 @@ export type TelegramUserAccountConfig = { textChunkLimit?: number; /** Max outbound media size in MB. */ mediaMaxMb?: number; + /** Optional allowlist for Telegram group senders (user ids or usernames). */ + groupAllowFrom?: Array; + /** Controls how group messages are handled (open | disabled | allowlist). */ + groupPolicy?: GroupPolicy; + /** Group-specific overrides (keyed by chat id). */ + groups?: Record; }; export type TelegramUserConfig = TelegramUserAccountConfig & { @@ -25,7 +48,17 @@ export type TelegramUserConfig = TelegramUserAccountConfig & { export type CoreConfig = { channels?: { + defaults?: { + groupPolicy?: GroupPolicy; + }; "telegram-user"?: TelegramUserConfig; }; + commands?: { + useAccessGroups?: boolean; + }; + messages?: { + ackReactionScope?: "off" | "group-mentions" | "group-all" | "direct" | "all"; + removeAckAfterReply?: boolean; + }; [key: string]: unknown; }; From 9b94a751c8d3dbc9b3d43668537a68482e7a2bf2 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 06:52:05 +0000 Subject: [PATCH 12/46] Telegram-user: handle media groups --- .../telegram-user/src/monitor/handler.ts | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 01309a365..078d3a54e 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -142,6 +142,28 @@ async function resolveMediaAttachment(params: { }; } +async function resolveMediaAttachments(params: { + client: TelegramClient; + mediaMaxMb: number; + messages: MessageContext[]; + runtime: RuntimeEnv; +}): Promise> { + const results: Array<{ path: string; contentType?: string }> = []; + for (const message of params.messages) { + if (!message.media) continue; + const resolved = await resolveMediaAttachment({ + client: params.client, + mediaMaxMb: params.mediaMaxMb, + media: message.media, + }).catch((err) => { + params.runtime.error?.(`telegram-user media download failed: ${String(err)}`); + return null; + }); + if (resolved) results.push(resolved); + } + return results; +} + export function createTelegramUserMessageHandler(params: TelegramUserHandlerParams) { const { client, cfg, runtime, accountId, accountConfig, self } = params; const core = getTelegramUserRuntime(); @@ -154,6 +176,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara return async (msg: MessageContext) => { try { if (msg.isOutgoing || msg.isService) return; + const messageGroup = msg.isMessageGroup ? msg.messages : [msg]; const isDirect = msg.chat.type === "user"; const isGroup = msg.chat.type === "chat" && msg.chat.chatType !== "channel"; @@ -262,15 +285,16 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara } } - const text = msg.text?.trim() ?? ""; - const media = await resolveMediaAttachment({ + const primaryMessage = + messageGroup.find((entry) => entry.text?.trim()) ?? msg; + const text = primaryMessage.text?.trim() ?? ""; + const allMedia = await resolveMediaAttachments({ client, mediaMaxMb, - media: msg.media, - }).catch((err) => { - runtime.error?.(`telegram-user media download failed: ${String(err)}`); - return null; + messages: messageGroup, + runtime, }); + const media = allMedia[0] ?? null; if (!text && !media) return; core.channel.activity.record({ @@ -389,13 +413,15 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const conversationLabel = isGroup && chatId != null ? buildTelegramUserGroupLabel(groupTitle, chatId, threadId) : senderName; + const mediaSuffix = + !text && allMedia.length > 1 ? ` (${allMedia.length} items)` : ""; const body = core.channel.reply.formatAgentEnvelope({ channel: "Telegram User", from: senderName, timestamp: msg.date, previousTimestamp, envelope: envelopeOptions, - body: text || "(media)", + body: text || `(media${mediaSuffix})`, }); const ctxPayload = core.channel.reply.finalizeInboundContext({ From f45dca1205f049a5250d483b4d246fdad8298b70 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 06:53:06 +0000 Subject: [PATCH 13/46] Telegram-user: add location context --- .../telegram-user/src/monitor/handler.ts | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 078d3a54e..33331515e 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -2,7 +2,13 @@ import type { TelegramClient } from "@mtcute/node"; import type { MessageContext } from "@mtcute/dispatcher"; import type { RuntimeEnv } from "clawdbot/plugin-sdk"; -import { resolveAckReaction, resolveMentionGatingWithBypass } from "clawdbot/plugin-sdk"; +import { + formatLocationText, + resolveAckReaction, + resolveMentionGatingWithBypass, + toLocationContext, + type NormalizedLocation, +} from "clawdbot/plugin-sdk"; import { getTelegramUserRuntime } from "../runtime.js"; import type { CoreConfig, TelegramUserAccountConfig } from "../types.js"; import { sendMediaTelegramUser, sendMessageTelegramUser } from "../send.js"; @@ -107,6 +113,45 @@ function resolveTelegramUserGroupConfig( return { groupConfig, topicConfig }; } +function extractTelegramUserLocation( + media: MessageContext["media"], +): NormalizedLocation | null { + if (!media) return null; + const typed = media as { type?: string }; + if (typed.type === "venue") { + const venue = media as { + location: { latitude: number; longitude: number; radius?: number }; + title: string; + address: string; + }; + return { + latitude: venue.location.latitude, + longitude: venue.location.longitude, + accuracy: venue.location.radius, + name: venue.title, + address: venue.address, + source: "place", + isLive: false, + }; + } + if (typed.type === "location" || typed.type === "live_location") { + const location = media as { + latitude: number; + longitude: number; + radius?: number; + }; + const isLive = typed.type === "live_location"; + return { + latitude: location.latitude, + longitude: location.longitude, + accuracy: location.radius, + source: isLive ? "live" : "pin", + isLive, + }; + } + return null; +} + async function resolveMediaAttachment(params: { client: TelegramClient; mediaMaxMb: number; @@ -288,6 +333,8 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const primaryMessage = messageGroup.find((entry) => entry.text?.trim()) ?? msg; const text = primaryMessage.text?.trim() ?? ""; + const locationData = extractTelegramUserLocation(primaryMessage.media); + const locationText = locationData ? formatLocationText(locationData) : undefined; const allMedia = await resolveMediaAttachments({ client, mediaMaxMb, @@ -295,7 +342,8 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara runtime, }); const media = allMedia[0] ?? null; - if (!text && !media) return; + const rawBody = [text, locationText].filter(Boolean).join("\n").trim(); + if (!rawBody && !media) return; core.channel.activity.record({ channel: "telegram-user", @@ -414,14 +462,14 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara ? buildTelegramUserGroupLabel(groupTitle, chatId, threadId) : senderName; const mediaSuffix = - !text && allMedia.length > 1 ? ` (${allMedia.length} items)` : ""; + !rawBody && allMedia.length > 1 ? ` (${allMedia.length} items)` : ""; const body = core.channel.reply.formatAgentEnvelope({ channel: "Telegram User", from: senderName, timestamp: msg.date, previousTimestamp, envelope: envelopeOptions, - body: text || `(media${mediaSuffix})`, + body: rawBody || `(media${mediaSuffix})`, }); const ctxPayload = core.channel.reply.finalizeInboundContext({ @@ -455,6 +503,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara ? buildTelegramUserGroupFrom(chatId, threadId) : `telegram-user:${senderId}`, WasMentioned: isGroup ? effectiveWasMentioned : undefined, + ...(locationData ? toLocationContext(locationData) : undefined), }); void core.channel.session From 0eeabe404a1f3b7db8f5cb893844ea149e004d2e Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 06:54:56 +0000 Subject: [PATCH 14/46] Telegram-user: enrich reply and media context --- .../telegram-user/src/monitor/handler.ts | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 33331515e..68d4fa424 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -152,6 +152,23 @@ function extractTelegramUserLocation( return null; } +function describeReplySender(sender: unknown): string | undefined { + const typed = sender as { + type?: string; + displayName?: string; + title?: string; + id?: number; + }; + if (!typed || typeof typed !== "object") return undefined; + if (typed.type === "anonymous" && typed.displayName) return typed.displayName; + if (typed.type === "user" && typed.displayName) return typed.displayName; + if (typed.type === "chat") { + if (typed.title) return typed.title; + if (typed.id != null) return `chat:${typed.id}`; + } + return undefined; +} + async function resolveMediaAttachment(params: { client: TelegramClient; mediaMaxMb: number; @@ -344,6 +361,17 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const media = allMedia[0] ?? null; const rawBody = [text, locationText].filter(Boolean).join("\n").trim(); if (!rawBody && !media) return; + const timestampMs = msg.date ? msg.date * 1000 : undefined; + const replyInfo = msg.replyToMessage ?? null; + const replyToId = replyInfo?.id != null ? String(replyInfo.id) : undefined; + const replyToSender = replyInfo?.sender + ? describeReplySender(replyInfo.sender) + : undefined; + let replyToBody: string | undefined; + if (replyToId) { + const replyMessage = await msg.getReplyTo().catch(() => null); + replyToBody = replyMessage?.text?.trim() || undefined; + } core.channel.activity.record({ channel: "telegram-user", @@ -466,7 +494,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const body = core.channel.reply.formatAgentEnvelope({ channel: "Telegram User", from: senderName, - timestamp: msg.date, + timestamp: timestampMs, previousTimestamp, envelope: envelopeOptions, body: rawBody || `(media${mediaSuffix})`, @@ -490,11 +518,21 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara Provider: "telegram-user" as const, Surface: "telegram-user" as const, MessageSid: String(msg.id), - ReplyToId: String(msg.id), - Timestamp: msg.date, + ReplyToId: replyToId ?? String(msg.id), + ReplyToBody: replyToBody, + ReplyToSender: replyToSender, + Timestamp: timestampMs, MediaPath: media?.path, MediaType: media?.contentType, MediaUrl: media?.path, + MediaPaths: allMedia.length > 0 ? allMedia.map((item) => item.path) : undefined, + MediaUrls: allMedia.length > 0 ? allMedia.map((item) => item.path) : undefined, + MediaTypes: + allMedia.length > 0 + ? (allMedia + .map((item) => item.contentType) + .filter(Boolean) as string[]) + : undefined, CommandAuthorized: commandAuthorized, CommandSource: "text" as const, OriginatingChannel: "telegram-user" as const, @@ -503,6 +541,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara ? buildTelegramUserGroupFrom(chatId, threadId) : `telegram-user:${senderId}`, WasMentioned: isGroup ? effectiveWasMentioned : undefined, + MessageThreadId: threadId, ...(locationData ? toLocationContext(locationData) : undefined), }); From b04142bfd5d66daf3a13dccfae148edc3eb556f2 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 06:56:44 +0000 Subject: [PATCH 15/46] Telegram-user: add group skill filters --- .../telegram-user/src/monitor/handler.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 68d4fa424..70ac96ecf 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -257,6 +257,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara .catch(() => []); const combinedAllowFrom = [...allowFrom, ...storeAllowFrom]; const chatId = msg.chat.type === "chat" ? msg.chat.id : undefined; + const isForum = msg.chat.type === "chat" && msg.chat.isForum === true; const threadId = isGroup && msg.isTopicMessage ? msg.replyToMessage?.threadId ?? undefined @@ -489,6 +490,16 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const conversationLabel = isGroup && chatId != null ? buildTelegramUserGroupLabel(groupTitle, chatId, threadId) : senderName; + const skillFilter = firstDefined( + topicConfig?.skills, + groupConfig?.skills, + ); + const systemPromptParts = [ + groupConfig?.systemPrompt?.trim() || null, + topicConfig?.systemPrompt?.trim() || null, + ].filter((entry): entry is string => Boolean(entry)); + const groupSystemPrompt = + systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined; const mediaSuffix = !rawBody && allMedia.length > 1 ? ` (${allMedia.length} items)` : ""; const body = core.channel.reply.formatAgentEnvelope({ @@ -511,7 +522,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara ChatType: isGroup ? "group" : "direct", ConversationLabel: conversationLabel, GroupSubject: isGroup ? groupTitle ?? undefined : undefined, - GroupSystemPrompt: isGroup ? groupConfig?.systemPrompt ?? undefined : undefined, + GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined, SenderName: senderName, SenderId: senderId, SenderUsername: senderUsername ?? undefined, @@ -542,6 +553,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara : `telegram-user:${senderId}`, WasMentioned: isGroup ? effectiveWasMentioned : undefined, MessageThreadId: threadId, + IsForum: isForum, ...(locationData ? toLocationContext(locationData) : undefined), }); @@ -626,7 +638,10 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara ctx: ctxPayload, cfg, dispatcher, - replyOptions, + replyOptions: { + ...replyOptions, + skillFilter, + }, }); markDispatchIdle(); From 7ce441260b0a7e172ff2105bee51c47d441dfb13 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 06:57:02 +0000 Subject: [PATCH 16/46] Telegram-user: skip last-route for groups --- .../telegram-user/src/monitor/handler.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 70ac96ecf..bb2c7dffa 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -567,14 +567,16 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara runtime.error?.(`telegram-user failed to update session meta: ${String(err)}`); }); - await core.channel.session.updateLastRoute({ - storePath, - sessionKey: route.mainSessionKey, - channel: "telegram-user", - to: `telegram-user:${senderId}`, - accountId: route.accountId, - ctx: ctxPayload, - }); + if (!isGroup) { + await core.channel.session.updateLastRoute({ + storePath, + sessionKey: route.mainSessionKey, + channel: "telegram-user", + to: `telegram-user:${senderId}`, + accountId: route.accountId, + ctx: ctxPayload, + }); + } let hasReplied = false; const replyTarget = From fe245ffb2903ea9981ab3cac020540a500315d1f Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 06:58:22 +0000 Subject: [PATCH 17/46] Telegram-user: add group mention adapter --- extensions/telegram-user/src/channel.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index ce77e2ff9..797c79633 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -141,6 +141,15 @@ export const telegramUserPlugin: ChannelPlugin = { }; }, }, + groups: { + resolveRequireMention: ({ cfg, groupId, accountId }) => + getTelegramUserRuntime().channel.groups.resolveRequireMention({ + cfg, + channel: "telegram-user", + groupId, + accountId, + }), + }, outbound: { deliveryMode: "direct", chunker: (text, limit) => From d621f96be23344b19d5840c1b81c8cfa141ac28b Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 08:03:20 +0000 Subject: [PATCH 18/46] Telegram-user: add voice typing cues --- .../telegram-user/src/monitor/handler.ts | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index bb2c7dffa..c79babc55 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -152,6 +152,22 @@ function extractTelegramUserLocation( return null; } +function formatTelegramUserPoll(media: MessageContext["media"]): string | null { + if (!media) return null; + const typed = media as { type?: string }; + if (typed.type !== "poll") return null; + const poll = media as { + question: string; + answers: Array<{ text: string }>; + isMultiple?: boolean; + isQuiz?: boolean; + }; + const mode = poll.isQuiz ? "quiz" : poll.isMultiple ? "multi" : null; + const header = `📊 Poll${mode ? ` (${mode})` : ""}: ${poll.question}`; + const options = poll.answers.map((ans, idx) => `${idx + 1}) ${ans.text}`); + return [header, ...options].join("\n"); +} + function describeReplySender(sender: unknown): string | undefined { const typed = sender as { type?: string; @@ -175,6 +191,15 @@ async function resolveMediaAttachment(params: { media: MessageContext["media"]; }) { if (!params.media) return null; + const typed = params.media as { type?: string }; + if ( + typed.type === "location" || + typed.type === "live_location" || + typed.type === "venue" || + typed.type === "poll" + ) { + return null; + } const core = getTelegramUserRuntime(); const maxBytes = Math.max(1, params.mediaMaxMb) * 1024 * 1024; if ("fileSize" in params.media && typeof params.media.fileSize === "number") { @@ -353,6 +378,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const text = primaryMessage.text?.trim() ?? ""; const locationData = extractTelegramUserLocation(primaryMessage.media); const locationText = locationData ? formatLocationText(locationData) : undefined; + const pollText = formatTelegramUserPoll(primaryMessage.media); const allMedia = await resolveMediaAttachments({ client, mediaMaxMb, @@ -360,7 +386,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara runtime, }); const media = allMedia[0] ?? null; - const rawBody = [text, locationText].filter(Boolean).join("\n").trim(); + const rawBody = [text, locationText, pollText].filter(Boolean).join("\n").trim(); if (!rawBody && !media) return; const timestampMs = msg.date ? msg.date * 1000 : undefined; const replyInfo = msg.replyToMessage ?? null; @@ -593,6 +619,13 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const replyText = payload.text ?? ""; const mediaUrl = payload.mediaUrl; if (mediaUrl) { + if (payload.audioAsVoice) { + await client + .sendTyping(typingTarget, "record_voice", typingParams) + .catch((err) => { + runtime.error?.(`telegram-user voice typing failed: ${String(err)}`); + }); + } await sendMediaTelegramUser(replyTarget, replyText, { client, accountId, From 0e3ab476ad73cab4fa6c593f23f885daccc5f1dd Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 08:05:03 +0000 Subject: [PATCH 19/46] Telegram-user: add poll sending --- extensions/telegram-user/src/channel.ts | 8 ++++ extensions/telegram-user/src/send.ts | 63 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index 797c79633..57c128d0e 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -27,6 +27,7 @@ import { normalizeTelegramUserMessagingTarget, sendMediaTelegramUser, sendMessageTelegramUser, + sendPollTelegramUser, } from "./send.js"; import { resolveTelegramUserSessionPath } from "./session.js"; import { getTelegramUserRuntime } from "./runtime.js"; @@ -155,6 +156,7 @@ export const telegramUserPlugin: ChannelPlugin = { chunker: (text, limit) => getTelegramUserRuntime().channel.text.chunkMarkdownText(text, limit), textChunkLimit: 4000, + pollMaxOptions: 10, sendText: async ({ to, text, accountId }) => { const result = await sendMessageTelegramUser(to, text, { accountId: accountId ?? undefined }); return { channel: "telegram-user", ...result }; @@ -166,6 +168,12 @@ export const telegramUserPlugin: ChannelPlugin = { }); return { channel: "telegram-user", ...result }; }, + sendPoll: async ({ to, poll, accountId }) => { + const result = await sendPollTelegramUser(to, poll, { + accountId: accountId ?? undefined, + }); + return { channel: "telegram-user", ...result }; + }, }, auth: { login: async ({ cfg, accountId, runtime }) => { diff --git a/extensions/telegram-user/src/send.ts b/extensions/telegram-user/src/send.ts index e52412d7f..9b26a8bab 100644 --- a/extensions/telegram-user/src/send.ts +++ b/extensions/telegram-user/src/send.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import type { TelegramClient } from "@mtcute/node"; import { InputMedia } from "@mtcute/core"; +import type { PollInput } from "clawdbot/plugin-sdk"; import { getTelegramUserRuntime } from "./runtime.js"; import { resolveTelegramUserAccount } from "./accounts.js"; @@ -13,6 +14,12 @@ export type TelegramUserSendResult = { chatId: string; }; +type NormalizedPollInput = { + question: string; + options: string[]; + maxSelections: number; +}; + export type TelegramUserSendOpts = { client?: TelegramClient; accountId?: string; @@ -47,6 +54,32 @@ function resolveTelegramUserPeer(target: string): number | string { return target; } +function normalizePollInput(input: PollInput): NormalizedPollInput { + const question = input.question.trim(); + if (!question) { + throw new Error("Poll question is required"); + } + const options = (input.options ?? []).map((option) => option.trim()).filter(Boolean); + if (options.length < 2) { + throw new Error("Poll requires at least 2 options"); + } + if (options.length > 10) { + throw new Error("Poll supports at most 10 options"); + } + const maxSelectionsRaw = input.maxSelections; + const maxSelections = + typeof maxSelectionsRaw === "number" && Number.isFinite(maxSelectionsRaw) + ? Math.floor(maxSelectionsRaw) + : 1; + if (maxSelections < 1) { + throw new Error("maxSelections must be at least 1"); + } + if (maxSelections > options.length) { + throw new Error("maxSelections cannot exceed option count"); + } + return { question, options, maxSelections }; +} + async function resolveClient(params: { client?: TelegramClient; cfg: CoreConfig; @@ -126,3 +159,33 @@ export async function sendMediaTelegramUser( } } } + +export async function sendPollTelegramUser( + to: string, + poll: PollInput, + opts: TelegramUserSendOpts = {}, +): Promise { + const cfg = getTelegramUserRuntime().config.loadConfig() as CoreConfig; + const { client, stopOnDone } = await resolveClient({ + client: opts.client, + cfg, + accountId: opts.accountId, + }); + try { + const target = resolveTelegramUserPeer(normalizeTarget(to)); + const normalized = normalizePollInput(poll); + const input = InputMedia.poll({ + question: normalized.question, + answers: normalized.options, + multiple: normalized.maxSelections > 1, + }); + const message = await client.sendMedia(target, input, { + ...(opts.replyToId ? { replyTo: opts.replyToId } : {}), + }); + return { messageId: String(message.id), chatId: String(target) }; + } finally { + if (stopOnDone) { + await client.destroy(); + } + } +} From d84d44643af901bc73b834a7230d4b83af1467cd Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 10:04:10 +0000 Subject: [PATCH 20/46] Telegram-user: normalize group targets --- extensions/telegram-user/src/send.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/extensions/telegram-user/src/send.ts b/extensions/telegram-user/src/send.ts index 9b26a8bab..a80491e71 100644 --- a/extensions/telegram-user/src/send.ts +++ b/extensions/telegram-user/src/send.ts @@ -30,10 +30,10 @@ export type TelegramUserSendOpts = { const normalizeTarget = (raw: string): string => { const trimmed = raw.trim(); if (!trimmed) throw new Error("Recipient is required for Telegram User sends"); - return trimmed - .replace(/^(telegram-user|telegram|tg):/i, "") - .replace(/^user:/i, "") - .trim(); + const withoutProvider = trimmed.replace(/^(telegram-user|telegram|tg):/i, "").trim(); + const withoutPrefix = withoutProvider.replace(/^(user|group|channel|chat):/i, "").trim(); + const topicSplit = withoutPrefix.split(/:topic:/i); + return (topicSplit[0] ?? withoutPrefix).trim(); }; export function normalizeTelegramUserMessagingTarget(raw: string): string { @@ -43,6 +43,8 @@ export function normalizeTelegramUserMessagingTarget(raw: string): string { export function looksLikeTelegramUserTargetId(value: string): boolean { const trimmed = value.trim(); if (!trimmed) return false; + if (/^telegram-user:/i.test(trimmed)) return true; + if (/^(user|group|channel|chat):/i.test(trimmed)) return true; return /^-?\d+$/.test(trimmed) || /^@?[a-z0-9_]{5,}$/i.test(trimmed); } From e1a2a1b9b6008d8bb93bbca430aa689c1aa94525 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 10:11:09 +0000 Subject: [PATCH 21/46] Telegram-user: expose poll action --- extensions/telegram-user/src/channel.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index 57c128d0e..1621be728 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -70,6 +70,7 @@ export const telegramUserPlugin: ChannelPlugin = { }, capabilities: { chatTypes: ["direct", "group"], + polls: true, reactions: false, threads: false, media: true, @@ -151,6 +152,12 @@ export const telegramUserPlugin: ChannelPlugin = { accountId, }), }, + actions: { + listActions: ({ cfg }) => { + if (!cfg.channels?.["telegram-user"]) return []; + return ["poll"]; + }, + }, outbound: { deliveryMode: "direct", chunker: (text, limit) => From 8bc17b39eb7ac65566edb3583344cdc1f14f0f5c Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 10:27:54 +0000 Subject: [PATCH 22/46] Telegram-user: add poll hint for agents --- extensions/telegram-user/src/channel.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index 1621be728..04ed1e156 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -158,6 +158,11 @@ export const telegramUserPlugin: ChannelPlugin = { return ["poll"]; }, }, + agentPrompt: { + messageToolHints: () => [ + "Telegram user polls only work in groups/channels (DM polls return MEDIA_INVALID). Use the group id for polls.", + ], + }, outbound: { deliveryMode: "direct", chunker: (text, limit) => From fb9d36e433cf888bf0c46949fb5cc35a84a357cc Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 10:37:20 +0000 Subject: [PATCH 23/46] Telegram-user: add threading tool context --- extensions/telegram-user/src/channel.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index 04ed1e156..0a31e3f14 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -152,6 +152,17 @@ export const telegramUserPlugin: ChannelPlugin = { accountId, }), }, + threading: { + resolveReplyToMode: ({ cfg }) => cfg.channels?.["telegram-user"]?.replyToMode ?? "first", + buildToolContext: ({ context, hasRepliedRef }) => { + const threadId = context.MessageThreadId ?? context.ReplyToId; + return { + currentChannelId: context.To?.trim() || undefined, + currentThreadTs: threadId != null ? String(threadId) : undefined, + hasRepliedRef, + }; + }, + }, actions: { listActions: ({ cfg }) => { if (!cfg.channels?.["telegram-user"]) return []; @@ -161,6 +172,7 @@ export const telegramUserPlugin: ChannelPlugin = { agentPrompt: { messageToolHints: () => [ "Telegram user polls only work in groups/channels (DM polls return MEDIA_INVALID). Use the group id for polls.", + "When ChatType is group, use currentChannelId as the target for message/poll actions.", ], }, outbound: { From af88af01fea2976edc5920351229711b7ef029bc Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 10:46:45 +0000 Subject: [PATCH 24/46] Telegram-user: ignore sends after shutdown --- .../telegram-user/src/monitor/handler.ts | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index c79babc55..d127a03d3 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -73,6 +73,11 @@ function resolveTelegramUserPeer(target: string): number | string { return target; } +function isDestroyedClientError(err: unknown): boolean { + const message = err instanceof Error ? err.message : String(err); + return /client is destroyed/i.test(message); +} + function firstDefined(...values: Array): T | undefined { for (const value of values) { if (typeof value !== "undefined") return value; @@ -626,13 +631,18 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara runtime.error?.(`telegram-user voice typing failed: ${String(err)}`); }); } - await sendMediaTelegramUser(replyTarget, replyText, { - client, - accountId, - replyToId, - mediaUrl, - maxBytes: mediaMaxMb * 1024 * 1024, - }); + try { + await sendMediaTelegramUser(replyTarget, replyText, { + client, + accountId, + replyToId, + mediaUrl, + maxBytes: mediaMaxMb * 1024 * 1024, + }); + } catch (err) { + if (isDestroyedClientError(err)) return; + throw err; + } hasReplied = true; core.channel.activity.record({ channel: "telegram-user", @@ -645,11 +655,16 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara for (const chunk of core.channel.text.chunkMarkdownText(replyText, textLimit)) { const trimmed = chunk.trim(); if (!trimmed) continue; - await sendMessageTelegramUser(replyTarget, trimmed, { - client, - accountId, - replyToId, - }); + try { + await sendMessageTelegramUser(replyTarget, trimmed, { + client, + accountId, + replyToId, + }); + } catch (err) { + if (isDestroyedClientError(err)) return; + throw err; + } hasReplied = true; core.channel.activity.record({ channel: "telegram-user", From 4087f875c5e75a98c02e720ad85843ad7a78f9ea Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 11:04:18 +0000 Subject: [PATCH 25/46] Telegram-user: ignore typing after shutdown --- extensions/telegram-user/src/monitor/handler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index d127a03d3..18519a112 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -676,6 +676,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara }, onReplyStart: async () => { await client.sendTyping(typingTarget, "typing", typingParams).catch((err) => { + if (isDestroyedClientError(err)) return; runtime.error?.(`telegram-user typing failed: ${String(err)}`); }); }, From d65ac6af1d83a610a346ba0f23719ef63b83ac01 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 11:08:29 +0000 Subject: [PATCH 26/46] Telegram-user: guard typing send errors --- extensions/telegram-user/src/monitor/handler.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 18519a112..ef2378a74 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -625,11 +625,12 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const mediaUrl = payload.mediaUrl; if (mediaUrl) { if (payload.audioAsVoice) { - await client - .sendTyping(typingTarget, "record_voice", typingParams) - .catch((err) => { - runtime.error?.(`telegram-user voice typing failed: ${String(err)}`); - }); + try { + await client.sendTyping(typingTarget, "record_voice", typingParams); + } catch (err) { + if (isDestroyedClientError(err)) return; + runtime.error?.(`telegram-user voice typing failed: ${String(err)}`); + } } try { await sendMediaTelegramUser(replyTarget, replyText, { @@ -675,10 +676,12 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara } }, onReplyStart: async () => { - await client.sendTyping(typingTarget, "typing", typingParams).catch((err) => { + try { + await client.sendTyping(typingTarget, "typing", typingParams); + } catch (err) { if (isDestroyedClientError(err)) return; runtime.error?.(`telegram-user typing failed: ${String(err)}`); - }); + } }, onError: (err) => { runtime.error?.(`telegram-user reply failed: ${String(err)}`); From 62873a11b757a0d59c487334ce151e62214b3892 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 11:17:58 +0000 Subject: [PATCH 27/46] Telegram-user: skip typing when client destroyed --- .../telegram-user/src/monitor/handler.ts | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index ef2378a74..c66cd9f10 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -78,6 +78,28 @@ function isDestroyedClientError(err: unknown): boolean { return /client is destroyed/i.test(message); } +function isClientDestroyed(client: TelegramClient): boolean { + const candidate = client as TelegramClient & { destroyed?: boolean }; + return candidate.destroyed === true; +} + +async function safeSendTyping(params: { + client: TelegramClient; + target: number | string; + status: Parameters[1]; + typingParams?: Parameters[2]; + runtime: TelegramUserHandlerParams["runtime"]; + logLabel: string; +}) { + if (isClientDestroyed(params.client)) return; + try { + await params.client.sendTyping(params.target, params.status, params.typingParams); + } catch (err) { + if (isDestroyedClientError(err)) return; + params.runtime.error?.(`telegram-user ${params.logLabel} failed: ${String(err)}`); + } +} + function firstDefined(...values: Array): T | undefined { for (const value of values) { if (typeof value !== "undefined") return value; @@ -625,12 +647,14 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const mediaUrl = payload.mediaUrl; if (mediaUrl) { if (payload.audioAsVoice) { - try { - await client.sendTyping(typingTarget, "record_voice", typingParams); - } catch (err) { - if (isDestroyedClientError(err)) return; - runtime.error?.(`telegram-user voice typing failed: ${String(err)}`); - } + await safeSendTyping({ + client, + target: typingTarget, + status: "record_voice", + typingParams, + runtime, + logLabel: "voice typing", + }); } try { await sendMediaTelegramUser(replyTarget, replyText, { @@ -676,12 +700,14 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara } }, onReplyStart: async () => { - try { - await client.sendTyping(typingTarget, "typing", typingParams); - } catch (err) { - if (isDestroyedClientError(err)) return; - runtime.error?.(`telegram-user typing failed: ${String(err)}`); - } + await safeSendTyping({ + client, + target: typingTarget, + status: "typing", + typingParams, + runtime, + logLabel: "typing", + }); }, onError: (err) => { runtime.error?.(`telegram-user reply failed: ${String(err)}`); From 39d9eb4589dc76291383cfcce84701bcfc57b86c Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 11:25:27 +0000 Subject: [PATCH 28/46] Telegram-user: ignore sends after client destroy --- extensions/telegram-user/src/send.ts | 47 ++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/extensions/telegram-user/src/send.ts b/extensions/telegram-user/src/send.ts index a80491e71..5572f69c8 100644 --- a/extensions/telegram-user/src/send.ts +++ b/extensions/telegram-user/src/send.ts @@ -20,6 +20,11 @@ type NormalizedPollInput = { maxSelections: number; }; +function isDestroyedClientError(err: unknown): boolean { + const message = err instanceof Error ? err.message : String(err); + return /client is destroyed/i.test(message); +} + export type TelegramUserSendOpts = { client?: TelegramClient; accountId?: string; @@ -121,9 +126,17 @@ export async function sendMessageTelegramUser( }); try { const target = resolveTelegramUserPeer(normalizeTarget(to)); - const message = await client.sendText(target, text, { - ...(opts.replyToId ? { replyTo: opts.replyToId } : {}), - }); + let message: Awaited> | null = null; + try { + message = await client.sendText(target, text, { + ...(opts.replyToId ? { replyTo: opts.replyToId } : {}), + }); + } catch (err) { + if (!isDestroyedClientError(err)) throw err; + } + if (!message) { + return { messageId: "", chatId: String(target) }; + } return { messageId: String(message.id), chatId: String(target) }; } finally { if (stopOnDone) { @@ -151,9 +164,17 @@ export async function sendMediaTelegramUser( fileMime: media.contentType, caption: text, }); - const message = await client.sendMedia(target, input, { - ...(opts.replyToId ? { replyTo: opts.replyToId } : {}), - }); + let message: Awaited> | null = null; + try { + message = await client.sendMedia(target, input, { + ...(opts.replyToId ? { replyTo: opts.replyToId } : {}), + }); + } catch (err) { + if (!isDestroyedClientError(err)) throw err; + } + if (!message) { + return { messageId: "", chatId: String(target) }; + } return { messageId: String(message.id), chatId: String(target) }; } finally { if (stopOnDone) { @@ -181,9 +202,17 @@ export async function sendPollTelegramUser( answers: normalized.options, multiple: normalized.maxSelections > 1, }); - const message = await client.sendMedia(target, input, { - ...(opts.replyToId ? { replyTo: opts.replyToId } : {}), - }); + let message: Awaited> | null = null; + try { + message = await client.sendMedia(target, input, { + ...(opts.replyToId ? { replyTo: opts.replyToId } : {}), + }); + } catch (err) { + if (!isDestroyedClientError(err)) throw err; + } + if (!message) { + return { messageId: "", chatId: String(target) }; + } return { messageId: String(message.id), chatId: String(target) }; } finally { if (stopOnDone) { From 63929bd70c1b5ae3bf1a3cf12d7a71dcfaddcdf5 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 23 Jan 2026 17:17:18 +0000 Subject: [PATCH 29/46] Telegram user: preserve threads and update docs --- docs/channels/index.md | 2 +- docs/channels/telegram-user.md | 6 +-- extensions/telegram-user/package.json | 2 +- extensions/telegram-user/src/channel.ts | 21 ++++++---- extensions/telegram-user/src/config-schema.ts | 1 + .../telegram-user/src/monitor/handler.ts | 2 + extensions/telegram-user/src/send.ts | 41 +++++++++++++++---- extensions/telegram-user/src/types.ts | 2 + 8 files changed, 57 insertions(+), 20 deletions(-) diff --git a/docs/channels/index.md b/docs/channels/index.md index 4402e777c..203b43259 100644 --- a/docs/channels/index.md +++ b/docs/channels/index.md @@ -13,7 +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). +- [Telegram User](/channels/telegram-user) — MTProto user account with DM + group support (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 index 1d959950b..36d59ae37 100644 --- a/docs/channels/telegram-user.md +++ b/docs/channels/telegram-user.md @@ -1,10 +1,10 @@ --- -summary: "Connect a Telegram user account via MTProto (DM-only)" +summary: "Connect a Telegram user account via MTProto (DMs + groups)" --- # 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. +Use this when you need user-level DMs or want to message from your own account in groups. ## Requirements @@ -64,5 +64,5 @@ See [Pairing](/start/pairing) for details. ## Limitations -- DM-only (no groups or channels yet). +- Broadcast channels are not supported. - Calls are not supported. diff --git a/extensions/telegram-user/package.json b/extensions/telegram-user/package.json index 65f64a576..3585d1f73 100644 --- a/extensions/telegram-user/package.json +++ b/extensions/telegram-user/package.json @@ -14,7 +14,7 @@ "detailLabel": "Telegram User", "docsPath": "/channels/telegram-user", "docsLabel": "telegram-user", - "blurb": "login as a Telegram user via QR; DM-only for now.", + "blurb": "login as a Telegram user via QR or phone code; supports DMs + groups.", "order": 12, "quickstartAllowFrom": true }, diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index 0a31e3f14..2199a98cd 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -181,20 +181,25 @@ export const telegramUserPlugin: ChannelPlugin = { getTelegramUserRuntime().channel.text.chunkMarkdownText(text, limit), textChunkLimit: 4000, pollMaxOptions: 10, - sendText: async ({ to, text, accountId }) => { - const result = await sendMessageTelegramUser(to, text, { accountId: accountId ?? undefined }); - return { channel: "telegram-user", ...result }; - }, - sendMedia: async ({ to, text, mediaUrl, accountId }) => { - const result = await sendMediaTelegramUser(to, text, { + sendText: async ({ to, text, accountId, threadId }) => { + const result = await sendMessageTelegramUser(to, text, { accountId: accountId ?? undefined, - mediaUrl, + threadId, }); return { channel: "telegram-user", ...result }; }, - sendPoll: async ({ to, poll, accountId }) => { + sendMedia: async ({ to, text, mediaUrl, accountId, threadId }) => { + const result = await sendMediaTelegramUser(to, text, { + accountId: accountId ?? undefined, + mediaUrl, + threadId, + }); + return { channel: "telegram-user", ...result }; + }, + sendPoll: async ({ to, poll, accountId, threadId }) => { const result = await sendPollTelegramUser(to, poll, { accountId: accountId ?? undefined, + threadId, }); return { channel: "telegram-user", ...result }; }, diff --git a/extensions/telegram-user/src/config-schema.ts b/extensions/telegram-user/src/config-schema.ts index a1b791566..134f97050 100644 --- a/extensions/telegram-user/src/config-schema.ts +++ b/extensions/telegram-user/src/config-schema.ts @@ -31,6 +31,7 @@ const TelegramUserAccountSchema = z apiHash: z.string().optional(), dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(), allowFrom: z.array(allowFromEntry).optional(), + replyToMode: z.enum(["off", "first", "all"]).optional(), textChunkLimit: z.number().int().positive().optional(), mediaMaxMb: z.number().positive().optional(), groupAllowFrom: z.array(allowFromEntry).optional(), diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index c66cd9f10..748bba880 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -661,6 +661,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara client, accountId, replyToId, + threadId, mediaUrl, maxBytes: mediaMaxMb * 1024 * 1024, }); @@ -685,6 +686,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara client, accountId, replyToId, + threadId, }); } catch (err) { if (isDestroyedClientError(err)) return; diff --git a/extensions/telegram-user/src/send.ts b/extensions/telegram-user/src/send.ts index 5572f69c8..c2ec04885 100644 --- a/extensions/telegram-user/src/send.ts +++ b/extensions/telegram-user/src/send.ts @@ -29,17 +29,37 @@ export type TelegramUserSendOpts = { client?: TelegramClient; accountId?: string; replyToId?: number; + threadId?: string | number | null; mediaUrl?: string; }; -const normalizeTarget = (raw: string): string => { +function normalizeTarget(raw: string): string { const trimmed = raw.trim(); if (!trimmed) throw new Error("Recipient is required for Telegram User sends"); const withoutProvider = trimmed.replace(/^(telegram-user|telegram|tg):/i, "").trim(); const withoutPrefix = withoutProvider.replace(/^(user|group|channel|chat):/i, "").trim(); - const topicSplit = withoutPrefix.split(/:topic:/i); - return (topicSplit[0] ?? withoutPrefix).trim(); -}; + if (!withoutPrefix) throw new Error("Recipient is required for Telegram User sends"); + return withoutPrefix; +} + +function parseThreadId(value: string | number | null | undefined): number | undefined { + if (typeof value === "number") { + return Number.isFinite(value) ? Math.trunc(value) : undefined; + } + const trimmed = typeof value === "string" ? value.trim() : ""; + if (!trimmed) return undefined; + const parsed = Number.parseInt(trimmed, 10); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function resolveTargetAndThread(raw: string, threadId?: string | number | null) { + const normalized = normalizeTarget(raw); + const [base, topicRaw] = normalized.split(/:topic:/i); + const parsedThreadId = parseThreadId(threadId ?? topicRaw); + const target = (base ?? normalized).trim(); + if (!target) throw new Error("Recipient is required for Telegram User sends"); + return { target, threadId: parsedThreadId }; +} export function normalizeTelegramUserMessagingTarget(raw: string): string { return normalizeTarget(raw); @@ -50,6 +70,7 @@ export function looksLikeTelegramUserTargetId(value: string): boolean { if (!trimmed) return false; if (/^telegram-user:/i.test(trimmed)) return true; if (/^(user|group|channel|chat):/i.test(trimmed)) return true; + if (/^-?\d+:topic:\d+$/i.test(trimmed)) return true; return /^-?\d+$/.test(trimmed) || /^@?[a-z0-9_]{5,}$/i.test(trimmed); } @@ -125,11 +146,13 @@ export async function sendMessageTelegramUser( accountId: opts.accountId, }); try { - const target = resolveTelegramUserPeer(normalizeTarget(to)); + const resolved = resolveTargetAndThread(to, opts.threadId); + const target = resolveTelegramUserPeer(resolved.target); let message: Awaited> | null = null; try { message = await client.sendText(target, text, { ...(opts.replyToId ? { replyTo: opts.replyToId } : {}), + ...(resolved.threadId ? { threadId: resolved.threadId } : {}), }); } catch (err) { if (!isDestroyedClientError(err)) throw err; @@ -157,7 +180,8 @@ export async function sendMediaTelegramUser( accountId: opts.accountId, }); try { - const target = resolveTelegramUserPeer(normalizeTarget(to)); + const resolved = resolveTargetAndThread(to, opts.threadId); + const target = resolveTelegramUserPeer(resolved.target); const media = await getTelegramUserRuntime().media.loadWebMedia(opts.mediaUrl, opts.maxBytes); const input = InputMedia.auto(media.buffer, { fileName: media.fileName ?? undefined, @@ -168,6 +192,7 @@ export async function sendMediaTelegramUser( try { message = await client.sendMedia(target, input, { ...(opts.replyToId ? { replyTo: opts.replyToId } : {}), + ...(resolved.threadId ? { threadId: resolved.threadId } : {}), }); } catch (err) { if (!isDestroyedClientError(err)) throw err; @@ -195,7 +220,8 @@ export async function sendPollTelegramUser( accountId: opts.accountId, }); try { - const target = resolveTelegramUserPeer(normalizeTarget(to)); + const resolved = resolveTargetAndThread(to, opts.threadId); + const target = resolveTelegramUserPeer(resolved.target); const normalized = normalizePollInput(poll); const input = InputMedia.poll({ question: normalized.question, @@ -206,6 +232,7 @@ export async function sendPollTelegramUser( try { message = await client.sendMedia(target, input, { ...(opts.replyToId ? { replyTo: opts.replyToId } : {}), + ...(resolved.threadId ? { threadId: resolved.threadId } : {}), }); } catch (err) { if (!isDestroyedClientError(err)) throw err; diff --git a/extensions/telegram-user/src/types.ts b/extensions/telegram-user/src/types.ts index 06085e1ed..26649e011 100644 --- a/extensions/telegram-user/src/types.ts +++ b/extensions/telegram-user/src/types.ts @@ -30,6 +30,8 @@ export type TelegramUserAccountConfig = { dmPolicy?: DmPolicy; /** Allowlist for DM senders (user ids or usernames, or "*"). */ allowFrom?: Array; + /** Control reply threading when reply tags are present (off|first|all). */ + replyToMode?: "off" | "first" | "all"; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; /** Max outbound media size in MB. */ From 39a46275502ef72ea59919e47bff5a1a6c9957c0 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Sat, 24 Jan 2026 14:42:11 +0000 Subject: [PATCH 30/46] Telegram user: add group tool policy + DM open validation --- extensions/telegram-user/src/channel.ts | 48 +++++++++++++++++++ extensions/telegram-user/src/config-schema.ts | 38 +++++++++++++-- extensions/telegram-user/src/types.ts | 3 +- 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index 2199a98cd..0aa2094ac 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -8,9 +8,11 @@ import { formatPairingApproveHint, normalizeAccountId, setAccountEnabledInConfigSection, + type ChannelGroupContext, type ChannelPlugin, type ChannelSetupInput, type ClawdbotConfig, + type GroupToolPolicyConfig, } from "clawdbot/plugin-sdk"; import { @@ -51,6 +53,36 @@ type TelegramUserSetupInput = ChannelSetupInput & { apiHash?: string; }; +function normalizeTelegramUserGroupKey(raw?: string | null): string | undefined { + if (!raw) return undefined; + const trimmed = raw.trim(); + if (!trimmed) return undefined; + const withoutPrefix = trimmed.replace(/^telegram-user:group:/i, ""); + const [base] = withoutPrefix.split(/:topic:/i); + const normalized = base?.trim(); + return normalized ? normalized : undefined; +} + +function resolveTelegramUserGroupToolPolicy( + params: ChannelGroupContext, +): GroupToolPolicyConfig | undefined { + const account = resolveTelegramUserAccount({ + cfg: params.cfg as CoreConfig, + accountId: params.accountId, + }); + const groups = account.config.groups ?? {}; + const groupId = normalizeTelegramUserGroupKey(params.groupId); + const groupChannel = normalizeTelegramUserGroupKey(params.groupChannel); + const candidates = [groupId, groupChannel, "*"].filter( + (value): value is string => Boolean(value), + ); + for (const key of candidates) { + const entry = groups[key]; + if (entry?.tools) return entry.tools; + } + return undefined; +} + const isSessionLinked = async (accountId: string): Promise => { const sessionPath = resolveTelegramUserSessionPath(accountId); return fs.existsSync(sessionPath); @@ -142,6 +174,21 @@ export const telegramUserPlugin: ChannelPlugin = { raw.replace(/^(telegram-user|telegram|tg):/i, "").toLowerCase(), }; }, + collectWarnings: ({ account, cfg }) => { + const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; + const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; + if (groupPolicy !== "open") return []; + const groupAllowlistConfigured = + account.config.groups && Object.keys(account.config.groups).length > 0; + if (groupAllowlistConfigured) { + return [ + `- Telegram user groups: groupPolicy="open" allows any member in allowed groups to trigger (mention-gated). Set channels.telegram-user.groupPolicy="allowlist" + channels.telegram-user.groupAllowFrom to restrict senders.`, + ]; + } + return [ + `- Telegram user groups: groupPolicy="open" with no channels.telegram-user.groups allowlist; any group can add + ping (mention-gated). Set channels.telegram-user.groupPolicy="allowlist" + channels.telegram-user.groupAllowFrom or configure channels.telegram-user.groups.`, + ]; + }, }, groups: { resolveRequireMention: ({ cfg, groupId, accountId }) => @@ -151,6 +198,7 @@ export const telegramUserPlugin: ChannelPlugin = { groupId, accountId, }), + resolveToolPolicy: resolveTelegramUserGroupToolPolicy, }, threading: { resolveReplyToMode: ({ cfg }) => cfg.channels?.["telegram-user"]?.replyToMode ?? "first", diff --git a/extensions/telegram-user/src/config-schema.ts b/extensions/telegram-user/src/config-schema.ts index 134f97050..0f42c0762 100644 --- a/extensions/telegram-user/src/config-schema.ts +++ b/extensions/telegram-user/src/config-schema.ts @@ -1,5 +1,12 @@ import { z } from "zod"; +import { + DmPolicySchema, + GroupPolicySchema, + ToolPolicySchema, + requireOpenAllowFrom, +} from "clawdbot/plugin-sdk"; + const allowFromEntry = z.union([z.string(), z.number()]); const TelegramUserTopicSchema = z @@ -16,6 +23,7 @@ const TelegramUserGroupSchema = z .object({ requireMention: z.boolean().optional(), skills: z.array(z.string()).optional(), + tools: ToolPolicySchema, topics: z.record(z.string(), TelegramUserTopicSchema.optional()).optional(), enabled: z.boolean().optional(), allowFrom: z.array(allowFromEntry).optional(), @@ -23,23 +31,43 @@ const TelegramUserGroupSchema = z }) .strict(); -const TelegramUserAccountSchema = z +const TelegramUserAccountSchemaBase = z .object({ name: z.string().optional(), enabled: z.boolean().optional(), apiId: z.number().int().positive().optional(), apiHash: z.string().optional(), - dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(), + dmPolicy: DmPolicySchema.optional().default("pairing"), allowFrom: z.array(allowFromEntry).optional(), replyToMode: z.enum(["off", "first", "all"]).optional(), textChunkLimit: z.number().int().positive().optional(), mediaMaxMb: z.number().positive().optional(), groupAllowFrom: z.array(allowFromEntry).optional(), - groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional(), + groupPolicy: GroupPolicySchema.optional().default("allowlist"), groups: z.record(z.string(), TelegramUserGroupSchema.optional()).optional(), }) .strict(); -export const TelegramUserConfigSchema = TelegramUserAccountSchema.extend({ - accounts: z.record(z.string(), TelegramUserAccountSchema.optional()).optional(), +const TelegramUserAccountSchema = TelegramUserAccountSchemaBase.superRefine((value, ctx) => { + requireOpenAllowFrom({ + policy: value.dmPolicy, + allowFrom: value.allowFrom, + ctx, + path: ["allowFrom"], + message: + 'channels.telegram-user.dmPolicy="open" requires channels.telegram-user.allowFrom to include "*"', + }); +}); + +export const TelegramUserConfigSchema = TelegramUserAccountSchemaBase.extend({ + accounts: z.record(z.string(), TelegramUserAccountSchema.optional()).optional(), +}).superRefine((value, ctx) => { + requireOpenAllowFrom({ + policy: value.dmPolicy, + allowFrom: value.allowFrom, + ctx, + path: ["allowFrom"], + message: + 'channels.telegram-user.dmPolicy="open" requires channels.telegram-user.allowFrom to include "*"', + }); }); diff --git a/extensions/telegram-user/src/types.ts b/extensions/telegram-user/src/types.ts index 26649e011..59a6e32a9 100644 --- a/extensions/telegram-user/src/types.ts +++ b/extensions/telegram-user/src/types.ts @@ -1,4 +1,4 @@ -import type { DmPolicy, GroupPolicy } from "clawdbot/plugin-sdk"; +import type { DmPolicy, GroupPolicy, GroupToolPolicyConfig } from "clawdbot/plugin-sdk"; export type TelegramUserTopicConfig = { requireMention?: boolean; @@ -11,6 +11,7 @@ export type TelegramUserTopicConfig = { export type TelegramUserGroupConfig = { requireMention?: boolean; skills?: string[]; + tools?: GroupToolPolicyConfig; topics?: Record; enabled?: boolean; allowFrom?: Array; From 66a64463abd1dd39b3977c6b3cc1c9c4e978e9c0 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Mon, 26 Jan 2026 06:42:04 +0000 Subject: [PATCH 31/46] telegram-user: fix plugin deps --- extensions/telegram-user/package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/telegram-user/package.json b/extensions/telegram-user/package.json index 3585d1f73..5aae90caa 100644 --- a/extensions/telegram-user/package.json +++ b/extensions/telegram-user/package.json @@ -30,6 +30,12 @@ "@mtcute/node": "^0.27.6", "@clack/prompts": "^0.8.2", "qrcode-terminal": "^0.12.0", + "zod": "^4.3.6" + }, + "devDependencies": { "clawdbot": "workspace:*" + }, + "peerDependencies": { + "clawdbot": ">=2026.1.25" } } From 3cb14cf81af82216ac4e4cc2f5d43b4a438d8b3f Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Mon, 26 Jan 2026 06:55:21 +0000 Subject: [PATCH 32/46] telegram-user: align capabilities and media limits --- extensions/telegram-user/src/channel.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index 0aa2094ac..161f6fae0 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -7,6 +7,7 @@ import { deleteAccountFromConfigSection, formatPairingApproveHint, normalizeAccountId, + resolveChannelMediaMaxBytes, setAccountEnabledInConfigSection, type ChannelGroupContext, type ChannelPlugin, @@ -101,10 +102,10 @@ export const telegramUserPlugin: ChannelPlugin = { }, }, capabilities: { - chatTypes: ["direct", "group"], + chatTypes: ["direct", "group", "thread"], polls: true, - reactions: false, - threads: false, + reactions: true, + threads: true, media: true, nativeCommands: false, blockStreaming: true, @@ -236,11 +237,21 @@ export const telegramUserPlugin: ChannelPlugin = { }); return { channel: "telegram-user", ...result }; }, - sendMedia: async ({ to, text, mediaUrl, accountId, threadId }) => { + sendMedia: async ({ cfg, to, text, mediaUrl, accountId, threadId }) => { + const maxBytes = resolveChannelMediaMaxBytes({ + cfg, + resolveChannelLimitMb: ({ cfg, accountId }) => + resolveTelegramUserAccount({ + cfg: cfg as CoreConfig, + accountId, + }).config.mediaMaxMb, + accountId, + }); const result = await sendMediaTelegramUser(to, text, { accountId: accountId ?? undefined, mediaUrl, threadId, + ...(maxBytes ? { maxBytes } : {}), }); return { channel: "telegram-user", ...result }; }, From eeeacadb368b60af99e6f6a4f377ffeaeb7dff2e Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Mon, 26 Jan 2026 06:59:15 +0000 Subject: [PATCH 33/46] telegram-user: add outbound media/capabilities tests --- extensions/telegram-user/src/channel.test.ts | 78 ++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 extensions/telegram-user/src/channel.test.ts diff --git a/extensions/telegram-user/src/channel.test.ts b/extensions/telegram-user/src/channel.test.ts new file mode 100644 index 000000000..e114a2312 --- /dev/null +++ b/extensions/telegram-user/src/channel.test.ts @@ -0,0 +1,78 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; + +const sendMediaTelegramUser = vi.fn< + typeof import("./send.js").sendMediaTelegramUser +>(); + +vi.mock("./send.js", () => { + return { + looksLikeTelegramUserTargetId: () => true, + normalizeTelegramUserMessagingTarget: (raw: string) => raw, + sendMessageTelegramUser: vi.fn(async () => ({ messageId: "m1", chatId: "c1" })), + sendPollTelegramUser: vi.fn(async () => ({ messageId: "m2", chatId: "c2" })), + sendMediaTelegramUser, + }; +}); + +describe("telegram-user channel plugin", () => { + beforeEach(() => { + sendMediaTelegramUser.mockReset(); + }); + + it("declares thread/reaction capabilities consistent with handler behavior", async () => { + const mod = await import("./channel.js"); + expect(mod.telegramUserPlugin.capabilities?.reactions).toBe(true); + expect(mod.telegramUserPlugin.capabilities?.threads).toBe(true); + expect(mod.telegramUserPlugin.capabilities?.chatTypes).toContain("thread"); + }); + + it("enforces mediaMaxMb in outbound sendMedia", async () => { + sendMediaTelegramUser.mockResolvedValue({ messageId: "m3", chatId: "c3" }); + + const cfg = { + channels: { + "telegram-user": { + mediaMaxMb: 7, + }, + }, + } satisfies Partial as unknown as ClawdbotConfig; + + const mod = await import("./channel.js"); + await mod.telegramUserPlugin.outbound?.sendMedia?.({ + cfg, + to: "telegram-user:123", + text: "hello", + mediaUrl: "file:///tmp/example.jpg", + accountId: "default", + }); + + expect(sendMediaTelegramUser).toHaveBeenCalledTimes(1); + const [, , opts] = sendMediaTelegramUser.mock.calls[0] ?? []; + expect(opts?.maxBytes).toBe(7 * 1024 * 1024); + }); + + it("omits maxBytes when mediaMaxMb is not configured", async () => { + sendMediaTelegramUser.mockResolvedValue({ messageId: "m4", chatId: "c4" }); + + const cfg = { + channels: { + "telegram-user": {}, + }, + } satisfies Partial as unknown as ClawdbotConfig; + + const mod = await import("./channel.js"); + await mod.telegramUserPlugin.outbound?.sendMedia?.({ + cfg, + to: "telegram-user:123", + text: "hello", + mediaUrl: "file:///tmp/example.jpg", + accountId: "default", + }); + + expect(sendMediaTelegramUser).toHaveBeenCalledTimes(1); + const [, , opts] = sendMediaTelegramUser.mock.calls[0] ?? []; + expect(opts).not.toHaveProperty("maxBytes"); + }); +}); From a414f8e7f5d436a3b32cf7df0f4826cc5544809a Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Mon, 26 Jan 2026 07:07:20 +0000 Subject: [PATCH 34/46] telegram-user: add config directory listing --- extensions/telegram-user/src/channel.test.ts | 40 ++++++++++- extensions/telegram-user/src/channel.ts | 9 +++ .../telegram-user/src/directory-config.ts | 68 +++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 extensions/telegram-user/src/directory-config.ts diff --git a/extensions/telegram-user/src/channel.test.ts b/extensions/telegram-user/src/channel.test.ts index e114a2312..777a817c8 100644 --- a/extensions/telegram-user/src/channel.test.ts +++ b/extensions/telegram-user/src/channel.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; +import type { ClawdbotConfig, RuntimeEnv } from "clawdbot/plugin-sdk"; const sendMediaTelegramUser = vi.fn< typeof import("./send.js").sendMediaTelegramUser @@ -75,4 +75,42 @@ describe("telegram-user channel plugin", () => { const [, , opts] = sendMediaTelegramUser.mock.calls[0] ?? []; expect(opts).not.toHaveProperty("maxBytes"); }); + + it("lists peers and groups from config like the telegram plugin directory", async () => { + const cfg = { + channels: { + "telegram-user": { + allowFrom: ["123", "@alice", "telegram-user:456", "user:@bob", "*"], + groupAllowFrom: ["tg:carol", 789], + groups: { + "-1001": {}, + "*": {}, + }, + }, + }, + } satisfies Partial as unknown as ClawdbotConfig; + + const mod = await import("./channel.js"); + const runtime = { + log: () => {}, + warn: () => {}, + error: () => {}, + exit: (): never => { + throw new Error("exit called"); + }, + } satisfies RuntimeEnv; + const peers = await mod.telegramUserPlugin.directory?.listPeers?.({ + cfg, + runtime, + }); + const groups = await mod.telegramUserPlugin.directory?.listGroups?.({ + cfg, + runtime, + }); + + expect(peers?.map((p) => p.id).sort()).toEqual( + ["123", "456", "@alice", "@bob", "@carol", "789"].sort(), + ); + expect(groups?.map((g) => g.id)).toEqual(["-1001"]); + }); }); diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index 161f6fae0..ab545bf24 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -23,6 +23,10 @@ import { type ResolvedTelegramUserAccount, } from "./accounts.js"; import { TelegramUserConfigSchema } from "./config-schema.js"; +import { + listTelegramUserDirectoryGroupsFromConfig, + listTelegramUserDirectoryPeersFromConfig, +} from "./directory-config.js"; import { loginTelegramUser } from "./login.js"; import { monitorTelegramUserProvider } from "./monitor/index.js"; import { @@ -117,6 +121,11 @@ export const telegramUserPlugin: ChannelPlugin = { hint: "", }, }, + directory: { + self: async () => null, + listPeers: async (params) => listTelegramUserDirectoryPeersFromConfig(params), + listGroups: async (params) => listTelegramUserDirectoryGroupsFromConfig(params), + }, reload: { configPrefixes: ["channels.telegram-user"] }, configSchema: buildChannelConfigSchema(TelegramUserConfigSchema), config: { diff --git a/extensions/telegram-user/src/directory-config.ts b/extensions/telegram-user/src/directory-config.ts new file mode 100644 index 000000000..e28755e63 --- /dev/null +++ b/extensions/telegram-user/src/directory-config.ts @@ -0,0 +1,68 @@ +import type { ChannelDirectoryEntry, ClawdbotConfig } from "clawdbot/plugin-sdk"; + +import { resolveTelegramUserAccount } from "./accounts.js"; +import type { CoreConfig } from "./types.js"; + +export type TelegramUserDirectoryConfigParams = { + cfg: ClawdbotConfig; + accountId?: string | null; + query?: string | null; + limit?: number | null; +}; + +function normalizePeerEntry(raw: string): string | null { + const trimmed = raw.trim(); + if (!trimmed) return null; + const cleaned = trimmed + .replace(/^(telegram-user|telegram|tg):/i, "") + .replace(/^user:/i, "") + .trim(); + if (!cleaned) return null; + if (/^-?\d+$/.test(cleaned)) return cleaned; + const withoutAt = cleaned.replace(/^@/, ""); + if (!withoutAt) return null; + return `@${withoutAt}`; +} + +export async function listTelegramUserDirectoryPeersFromConfig( + params: TelegramUserDirectoryConfigParams, +): Promise { + const account = resolveTelegramUserAccount({ + cfg: params.cfg as CoreConfig, + accountId: params.accountId, + }); + const q = params.query?.trim().toLowerCase() || ""; + const raw = [ + ...(account.config.allowFrom ?? []).map((entry) => String(entry)), + ...(account.config.groupAllowFrom ?? []).map((entry) => String(entry)), + ]; + return Array.from( + new Set( + raw + .map((entry) => entry.trim()) + .filter((entry) => Boolean(entry) && entry !== "*"), + ), + ) + .map((entry) => normalizePeerEntry(entry)) + .filter((id): id is string => Boolean(id)) + .filter((id) => (q ? id.toLowerCase().includes(q) : true)) + .slice(0, params.limit && params.limit > 0 ? params.limit : undefined) + .map((id) => ({ kind: "user", id }) as const); +} + +export async function listTelegramUserDirectoryGroupsFromConfig( + params: TelegramUserDirectoryConfigParams, +): Promise { + const account = resolveTelegramUserAccount({ + cfg: params.cfg as CoreConfig, + accountId: params.accountId, + }); + const q = params.query?.trim().toLowerCase() || ""; + return Object.keys(account.config.groups ?? {}) + .map((id) => id.trim()) + .filter((id) => Boolean(id) && id !== "*") + .filter((id) => (q ? id.toLowerCase().includes(q) : true)) + .slice(0, params.limit && params.limit > 0 ? params.limit : undefined) + .map((id) => ({ kind: "group", id }) as const); +} + From 164c4d3310b7a71eb0f840fb5193abfb1ffbe35e Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Mon, 26 Jan 2026 12:10:24 +0000 Subject: [PATCH 35/46] telegram-user: hint filePath/media for sends --- extensions/telegram-user/src/channel.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index ab545bf24..59d8519a6 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -231,6 +231,7 @@ export const telegramUserPlugin: ChannelPlugin = { messageToolHints: () => [ "Telegram user polls only work in groups/channels (DM polls return MEDIA_INVALID). Use the group id for polls.", "When ChatType is group, use currentChannelId as the target for message/poll actions.", + "To send files, use `message` action=send with `filePath` (local path) or `media` (URL); put any caption in `message`.", ], }, outbound: { From 525c1489596b65cd579625a4671aa78a3ac17066 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Mon, 26 Jan 2026 19:11:24 +0000 Subject: [PATCH 36/46] telegram-user: avoid mtcute signal exit-hook --- extensions/telegram-user/src/client.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/extensions/telegram-user/src/client.ts b/extensions/telegram-user/src/client.ts index 5a3b09dea..4716c1969 100644 --- a/extensions/telegram-user/src/client.ts +++ b/extensions/telegram-user/src/client.ts @@ -1,13 +1,31 @@ -import { TelegramClient } from "@mtcute/node"; +import { BaseTelegramClient, NodePlatform, TelegramClient } from "@mtcute/node"; + +class ClawdbotTelegramUserPlatform extends NodePlatform { + // mtcute's NodePlatform.beforeExit installs SIGINT/SIGTERM handlers that re-send the signal, + // which can race with Clawdbot's graceful shutdown and close sqlite while writes are pending. + // We only hook into process exit events (no signal handlers) and rely on Clawdbot to stop cleanly. + override beforeExit(fn: () => void): () => void { + const onBeforeExit = () => fn(); + const onExit = () => fn(); + process.once("beforeExit", onBeforeExit); + process.once("exit", onExit); + return () => { + process.off("beforeExit", onBeforeExit); + process.off("exit", onExit); + }; + } +} export function createTelegramUserClient(params: { apiId: number; apiHash: string; storagePath: string; }) { - return new TelegramClient({ + const client = new BaseTelegramClient({ apiId: params.apiId, apiHash: params.apiHash, storage: params.storagePath, + platform: new ClawdbotTelegramUserPlatform(), }); + return new TelegramClient({ client }); } From a3184b920aa8ed99010a839a276f4a78b4f02ef7 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Tue, 27 Jan 2026 00:34:23 +0000 Subject: [PATCH 37/46] telegram-user: load mtcute deps via ESM --- extensions/telegram-user/src/client.ts | 47 ++++++++++++------- extensions/telegram-user/src/login.ts | 2 +- extensions/telegram-user/src/monitor/index.ts | 13 ++++- extensions/telegram-user/src/send.ts | 14 +++++- 4 files changed, 54 insertions(+), 22 deletions(-) diff --git a/extensions/telegram-user/src/client.ts b/extensions/telegram-user/src/client.ts index 4716c1969..156083c49 100644 --- a/extensions/telegram-user/src/client.ts +++ b/extensions/telegram-user/src/client.ts @@ -1,26 +1,39 @@ -import { BaseTelegramClient, NodePlatform, TelegramClient } from "@mtcute/node"; +type MtcuteNode = typeof import("@mtcute/node"); -class ClawdbotTelegramUserPlatform extends NodePlatform { - // mtcute's NodePlatform.beforeExit installs SIGINT/SIGTERM handlers that re-send the signal, - // which can race with Clawdbot's graceful shutdown and close sqlite while writes are pending. - // We only hook into process exit events (no signal handlers) and rely on Clawdbot to stop cleanly. - override beforeExit(fn: () => void): () => void { - const onBeforeExit = () => fn(); - const onExit = () => fn(); - process.once("beforeExit", onBeforeExit); - process.once("exit", onExit); - return () => { - process.off("beforeExit", onBeforeExit); - process.off("exit", onExit); - }; - } +let mtcuteNodePromise: Promise | null = null; + +async function loadMtcuteNode(): Promise { + mtcuteNodePromise ??= import("@mtcute/node"); + return mtcuteNodePromise; } -export function createTelegramUserClient(params: { +export async function createTelegramUserClient(params: { apiId: number; apiHash: string; storagePath: string; -}) { +}): Promise { + // When loaded via jiti (plugin loader), dependencies often resolve through the "require" export + // condition. mtcute prints a deprecation warning from its CommonJS bundle. Dynamic import forces + // the "import" condition (ESM), eliminating the warning. + const { BaseTelegramClient, TelegramClient, NodePlatform } = await loadMtcuteNode(); + + class ClawdbotTelegramUserPlatform extends NodePlatform { + // mtcute's default NodePlatform.beforeExit installs SIGINT/SIGTERM handlers that re-send the + // signal, which can race with Clawdbot's graceful shutdown and close sqlite while writes are + // pending. We only hook into process exit events (no signal handlers) and rely on Clawdbot to + // stop cleanly. + override beforeExit(fn: () => void): () => void { + const onBeforeExit = () => fn(); + const onExit = () => fn(); + process.once("beforeExit", onBeforeExit); + process.once("exit", onExit); + return () => { + process.off("beforeExit", onBeforeExit); + process.off("exit", onExit); + }; + } + } + const client = new BaseTelegramClient({ apiId: params.apiId, apiHash: params.apiHash, diff --git a/extensions/telegram-user/src/login.ts b/extensions/telegram-user/src/login.ts index f2d8cbc90..e8363942e 100644 --- a/extensions/telegram-user/src/login.ts +++ b/extensions/telegram-user/src/login.ts @@ -39,7 +39,7 @@ export async function loginTelegramUser(params: { }) { const { apiId, apiHash, storagePath, runtime } = params; ensureTelegramUserSessionDir({ sessionPath: storagePath }); - const client = createTelegramUserClient({ apiId, apiHash, storagePath }); + const client = await createTelegramUserClient({ apiId, apiHash, storagePath }); let lastUrl = ""; const passwordEnv = process.env.TELEGRAM_USER_PASSWORD?.trim() || undefined; diff --git a/extensions/telegram-user/src/monitor/index.ts b/extensions/telegram-user/src/monitor/index.ts index 3aff04f2a..5e7486089 100644 --- a/extensions/telegram-user/src/monitor/index.ts +++ b/extensions/telegram-user/src/monitor/index.ts @@ -1,5 +1,4 @@ import fs from "node:fs"; -import { Dispatcher, filters } from "@mtcute/dispatcher"; import type { RuntimeEnv } from "clawdbot/plugin-sdk"; import { createTelegramUserClient } from "../client.js"; @@ -10,6 +9,15 @@ import { setActiveTelegramUserClient } from "../active-client.js"; import { createTelegramUserMessageHandler } from "./handler.js"; import type { CoreConfig } from "../types.js"; +type MtcuteDispatcher = typeof import("@mtcute/dispatcher"); + +let mtcuteDispatcherPromise: Promise | null = null; + +async function loadMtcuteDispatcher(): Promise { + mtcuteDispatcherPromise ??= import("@mtcute/dispatcher"); + return mtcuteDispatcherPromise; +} + export type MonitorTelegramUserOpts = { runtime?: RuntimeEnv; abortSignal?: AbortSignal; @@ -48,7 +56,7 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts "Telegram user session missing. Run `clawdbot channels login --channel telegram-user` first.", ); } - const client = createTelegramUserClient({ apiId, apiHash, storagePath }); + const client = await createTelegramUserClient({ apiId, apiHash, storagePath }); setActiveTelegramUserClient(client); const stop = async () => { @@ -66,6 +74,7 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts await client.start(); + const { Dispatcher, filters } = await loadMtcuteDispatcher(); const dispatcher = Dispatcher.for(client); const self = await client.getMe().catch(() => undefined); const handleMessage = createTelegramUserMessageHandler({ diff --git a/extensions/telegram-user/src/send.ts b/extensions/telegram-user/src/send.ts index c2ec04885..2afbd4645 100644 --- a/extensions/telegram-user/src/send.ts +++ b/extensions/telegram-user/src/send.ts @@ -1,6 +1,5 @@ import fs from "node:fs"; import type { TelegramClient } from "@mtcute/node"; -import { InputMedia } from "@mtcute/core"; import type { PollInput } from "clawdbot/plugin-sdk"; import { getTelegramUserRuntime } from "./runtime.js"; @@ -14,6 +13,15 @@ export type TelegramUserSendResult = { chatId: string; }; +type MtcuteCore = typeof import("@mtcute/core"); + +let mtcuteCorePromise: Promise | null = null; + +async function loadMtcuteCore(): Promise { + mtcuteCorePromise ??= import("@mtcute/core"); + return mtcuteCorePromise; +} + type NormalizedPollInput = { question: string; options: string[]; @@ -129,7 +137,7 @@ async function resolveClient(params: { "Telegram user session missing. Run `clawdbot channels login --channel telegram-user` first.", ); } - const client = createTelegramUserClient({ apiId, apiHash, storagePath }); + const client = await createTelegramUserClient({ apiId, apiHash, storagePath }); await client.start(); return { client, stopOnDone: true }; } @@ -180,6 +188,7 @@ export async function sendMediaTelegramUser( accountId: opts.accountId, }); try { + const { InputMedia } = await loadMtcuteCore(); const resolved = resolveTargetAndThread(to, opts.threadId); const target = resolveTelegramUserPeer(resolved.target); const media = await getTelegramUserRuntime().media.loadWebMedia(opts.mediaUrl, opts.maxBytes); @@ -220,6 +229,7 @@ export async function sendPollTelegramUser( accountId: opts.accountId, }); try { + const { InputMedia } = await loadMtcuteCore(); const resolved = resolveTargetAndThread(to, opts.threadId); const target = resolveTelegramUserPeer(resolved.target); const normalized = normalizePollInput(poll); From 7f4e402c4d380fbe5f624258dcb7cc312d3bb0bd Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Tue, 27 Jan 2026 05:13:55 +0000 Subject: [PATCH 38/46] telegram-user: sync pnpm lockfile --- pnpm-lock.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 091cf70b9..47acbe8f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -429,12 +429,16 @@ importers: '@mtcute/node': specifier: ^0.27.6 version: 0.27.6(ws@8.19.0) - clawdbot: - specifier: workspace:* - version: link:../.. qrcode-terminal: specifier: ^0.12.0 version: 0.12.0 + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + clawdbot: + specifier: workspace:* + version: link:../.. extensions/tlon: dependencies: From 29d9d875a790ca3a9808ecda85aff51f59a3479b Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Tue, 27 Jan 2026 10:10:58 +0000 Subject: [PATCH 39/46] telegram-user: align outbound reply + chunking --- extensions/telegram-user/src/channel.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index 59d8519a6..45d59fb89 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -58,6 +58,12 @@ type TelegramUserSetupInput = ChannelSetupInput & { apiHash?: string; }; +function parseReplyToId(replyToId?: string | null): number | undefined { + if (!replyToId) return undefined; + const parsed = Number.parseInt(replyToId, 10); + return Number.isFinite(parsed) ? parsed : undefined; +} + function normalizeTelegramUserGroupKey(raw?: string | null): string | undefined { if (!raw) return undefined; const trimmed = raw.trim(); @@ -238,16 +244,20 @@ export const telegramUserPlugin: ChannelPlugin = { deliveryMode: "direct", chunker: (text, limit) => getTelegramUserRuntime().channel.text.chunkMarkdownText(text, limit), + chunkerMode: "markdown", textChunkLimit: 4000, pollMaxOptions: 10, - sendText: async ({ to, text, accountId, threadId }) => { + sendText: async ({ to, text, accountId, threadId, replyToId }) => { + const parsedReplyToId = parseReplyToId(replyToId); const result = await sendMessageTelegramUser(to, text, { accountId: accountId ?? undefined, threadId, + ...(parsedReplyToId ? { replyToId: parsedReplyToId } : {}), }); return { channel: "telegram-user", ...result }; }, - sendMedia: async ({ cfg, to, text, mediaUrl, accountId, threadId }) => { + sendMedia: async ({ cfg, to, text, mediaUrl, accountId, threadId, replyToId }) => { + const parsedReplyToId = parseReplyToId(replyToId); const maxBytes = resolveChannelMediaMaxBytes({ cfg, resolveChannelLimitMb: ({ cfg, accountId }) => @@ -261,14 +271,17 @@ export const telegramUserPlugin: ChannelPlugin = { accountId: accountId ?? undefined, mediaUrl, threadId, + ...(parsedReplyToId ? { replyToId: parsedReplyToId } : {}), ...(maxBytes ? { maxBytes } : {}), }); return { channel: "telegram-user", ...result }; }, - sendPoll: async ({ to, poll, accountId, threadId }) => { + sendPoll: async ({ to, poll, accountId, threadId, replyToId }) => { + const parsedReplyToId = parseReplyToId(replyToId); const result = await sendPollTelegramUser(to, poll, { accountId: accountId ?? undefined, threadId, + ...(parsedReplyToId ? { replyToId: parsedReplyToId } : {}), }); return { channel: "telegram-user", ...result }; }, From 65c3718c96de3c3b65b6e1d13645e71303569e1d Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Tue, 27 Jan 2026 10:11:04 +0000 Subject: [PATCH 40/46] telegram-user: support voice-note media --- .../telegram-user/src/monitor/handler.ts | 1 + extensions/telegram-user/src/send.test.ts | 110 ++++++++++++++++++ extensions/telegram-user/src/send.ts | 49 +++++++- 3 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 extensions/telegram-user/src/send.test.ts diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 748bba880..acbb2d7fd 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -663,6 +663,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara replyToId, threadId, mediaUrl, + audioAsVoice: payload.audioAsVoice === true, maxBytes: mediaMaxMb * 1024 * 1024, }); } catch (err) { diff --git a/extensions/telegram-user/src/send.test.ts b/extensions/telegram-user/src/send.test.ts new file mode 100644 index 000000000..ea47c58e0 --- /dev/null +++ b/extensions/telegram-user/src/send.test.ts @@ -0,0 +1,110 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const loadWebMedia = vi.fn(); + +vi.mock("./runtime.js", () => { + return { + getTelegramUserRuntime: () => ({ + config: { loadConfig: () => ({}) }, + media: { + loadWebMedia: (...args: unknown[]) => loadWebMedia(...args), + }, + }), + }; +}); + +const inputMediaAuto = vi.fn((file: unknown, params: unknown) => ({ + type: "auto", + file, + ...(params && typeof params === "object" ? params : {}), +})); +const inputMediaVoice = vi.fn((file: unknown, params: unknown) => ({ + type: "voice", + file, + ...(params && typeof params === "object" ? params : {}), +})); + +vi.mock("@mtcute/core", () => { + return { + InputMedia: { + auto: (...args: unknown[]) => inputMediaAuto(...args), + voice: (...args: unknown[]) => inputMediaVoice(...args), + poll: () => ({ type: "poll" }), + }, + }; +}); + +describe("telegram-user send", () => { + beforeEach(() => { + loadWebMedia.mockReset(); + inputMediaAuto.mockClear(); + inputMediaVoice.mockClear(); + }); + + it("sends audio media as voice note when audioAsVoice is set", async () => { + loadWebMedia.mockResolvedValue({ + buffer: Buffer.from("voice"), + contentType: "audio/ogg", + fileName: "note.ogg", + }); + + const sendMedia = vi.fn(async () => ({ id: 123 })); + const { sendMediaTelegramUser } = await import("./send.js"); + await sendMediaTelegramUser("telegram-user:123", "hi", { + client: { sendMedia } as unknown as import("@mtcute/node").TelegramClient, + mediaUrl: "https://example.com/note.ogg", + audioAsVoice: true, + }); + + expect(inputMediaVoice).toHaveBeenCalledTimes(1); + expect(sendMedia).toHaveBeenCalledTimes(1); + const [, media] = sendMedia.mock.calls[0] ?? []; + expect(media).toMatchObject({ type: "voice" }); + }); + + it("falls back to normal media when audioAsVoice is set but media is not voice-compatible", async () => { + loadWebMedia.mockResolvedValue({ + buffer: Buffer.from("img"), + contentType: "image/png", + fileName: "image.png", + }); + + const sendMedia = vi.fn(async () => ({ id: 123 })); + const { sendMediaTelegramUser } = await import("./send.js"); + await sendMediaTelegramUser("telegram-user:123", "hi", { + client: { sendMedia } as unknown as import("@mtcute/node").TelegramClient, + mediaUrl: "https://example.com/image.png", + audioAsVoice: true, + }); + + expect(inputMediaVoice).toHaveBeenCalledTimes(0); + expect(inputMediaAuto).toHaveBeenCalledTimes(1); + }); + + it("falls back to auto when voice messages are forbidden", async () => { + loadWebMedia.mockResolvedValue({ + buffer: Buffer.from("voice"), + contentType: "audio/ogg", + fileName: "note.ogg", + }); + + const sendMedia = vi.fn(async (_to: unknown, media: unknown) => { + if (media && typeof media === "object" && (media as { type?: string }).type === "voice") { + throw new Error("VOICE_MESSAGES_FORBIDDEN"); + } + return { id: 123 }; + }); + + const { sendMediaTelegramUser } = await import("./send.js"); + await sendMediaTelegramUser("telegram-user:123", "hi", { + client: { sendMedia } as unknown as import("@mtcute/node").TelegramClient, + mediaUrl: "https://example.com/note.ogg", + audioAsVoice: true, + }); + + expect(inputMediaVoice).toHaveBeenCalledTimes(1); + expect(inputMediaAuto).toHaveBeenCalledTimes(1); + expect(sendMedia).toHaveBeenCalledTimes(2); + }); +}); + diff --git a/extensions/telegram-user/src/send.ts b/extensions/telegram-user/src/send.ts index 2afbd4645..5d55b0354 100644 --- a/extensions/telegram-user/src/send.ts +++ b/extensions/telegram-user/src/send.ts @@ -39,6 +39,7 @@ export type TelegramUserSendOpts = { replyToId?: number; threadId?: string | number | null; mediaUrl?: string; + audioAsVoice?: boolean; }; function normalizeTarget(raw: string): string { @@ -69,6 +70,24 @@ function resolveTargetAndThread(raw: string, threadId?: string | number | null) return { target, threadId: parsedThreadId }; } +function isVoiceMessagesForbidden(err: unknown): boolean { + const message = err instanceof Error ? err.message : String(err); + return /VOICE_MESSAGES_FORBIDDEN/i.test(message); +} + +function shouldSendAsVoice(params: { + wantsVoice: boolean; + contentType?: string | null; + fileName?: string | null; +}): boolean { + if (!params.wantsVoice) return false; + const contentType = params.contentType?.toLowerCase() ?? ""; + const fileName = params.fileName?.toLowerCase() ?? ""; + if (/(^|\/)(ogg|opus)(;|$)/.test(contentType)) return true; + if (/\.(ogg|opus|oga)$/.test(fileName)) return true; + return false; +} + export function normalizeTelegramUserMessagingTarget(raw: string): string { return normalizeTarget(raw); } @@ -192,11 +211,24 @@ export async function sendMediaTelegramUser( const resolved = resolveTargetAndThread(to, opts.threadId); const target = resolveTelegramUserPeer(resolved.target); const media = await getTelegramUserRuntime().media.loadWebMedia(opts.mediaUrl, opts.maxBytes); - const input = InputMedia.auto(media.buffer, { - fileName: media.fileName ?? undefined, - fileMime: media.contentType, - caption: text, + const wantsVoice = shouldSendAsVoice({ + wantsVoice: opts.audioAsVoice === true, + contentType: media.contentType, + fileName: media.fileName, }); + const buildAuto = () => + InputMedia.auto(media.buffer, { + fileName: media.fileName ?? undefined, + fileMime: media.contentType, + caption: text, + }); + const buildVoice = () => + InputMedia.voice(media.buffer, { + fileName: media.fileName ?? undefined, + fileMime: media.contentType, + caption: text, + }); + const input = wantsVoice ? buildVoice() : buildAuto(); let message: Awaited> | null = null; try { message = await client.sendMedia(target, input, { @@ -204,7 +236,14 @@ export async function sendMediaTelegramUser( ...(resolved.threadId ? { threadId: resolved.threadId } : {}), }); } catch (err) { - if (!isDestroyedClientError(err)) throw err; + if (wantsVoice && isVoiceMessagesForbidden(err)) { + message = await client.sendMedia(target, buildAuto(), { + ...(opts.replyToId ? { replyTo: opts.replyToId } : {}), + ...(resolved.threadId ? { threadId: resolved.threadId } : {}), + }); + } else if (!isDestroyedClientError(err)) { + throw err; + } } if (!message) { return { messageId: "", chatId: String(target) }; From b649960e7c1862610cf7d6be2d5dedefa253c61b Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Tue, 27 Jan 2026 10:47:45 +0000 Subject: [PATCH 41/46] telegram-user: avoid crash on config reload --- .../telegram-user/src/monitor/handler.ts | 15 +++++++- extensions/telegram-user/src/monitor/index.ts | 34 ++++++++++++++++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index acbb2d7fd..1cffa5a5a 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -22,6 +22,7 @@ type TelegramUserHandlerParams = { runtime: RuntimeEnv; accountId: string; accountConfig: TelegramUserAccountConfig; + abortSignal?: AbortSignal; self?: { id: number; username?: string | null }; }; @@ -89,8 +90,10 @@ async function safeSendTyping(params: { status: Parameters[1]; typingParams?: Parameters[2]; runtime: TelegramUserHandlerParams["runtime"]; + abortSignal?: AbortSignal; logLabel: string; }) { + if (params.abortSignal?.aborted) return; if (isClientDestroyed(params.client)) return; try { await params.client.sendTyping(params.target, params.status, params.typingParams); @@ -279,7 +282,7 @@ async function resolveMediaAttachments(params: { } export function createTelegramUserMessageHandler(params: TelegramUserHandlerParams) { - const { client, cfg, runtime, accountId, accountConfig, self } = params; + const { client, cfg, runtime, accountId, accountConfig, self, abortSignal } = params; const core = getTelegramUserRuntime(); const textLimit = accountConfig.textChunkLimit ?? DEFAULT_TEXT_LIMIT; const mediaMaxMb = accountConfig.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB; @@ -288,6 +291,8 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const groupAllowFrom = accountConfig.groupAllowFrom ?? allowFrom; return async (msg: MessageContext) => { + if (abortSignal?.aborted) return; + if (isClientDestroyed(client)) return; try { if (msg.isOutgoing || msg.isService) return; const messageGroup = msg.isMessageGroup ? msg.messages : [msg]; @@ -642,10 +647,14 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara .responsePrefix, humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId), deliver: async (payload) => { + if (abortSignal?.aborted) return; + if (isClientDestroyed(client)) return; const replyToId = hasReplied ? undefined : msg.id; const replyText = payload.text ?? ""; const mediaUrl = payload.mediaUrl; if (mediaUrl) { + if (abortSignal?.aborted) return; + if (isClientDestroyed(client)) return; if (payload.audioAsVoice) { await safeSendTyping({ client, @@ -653,6 +662,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara status: "record_voice", typingParams, runtime, + abortSignal, logLabel: "voice typing", }); } @@ -680,6 +690,8 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara } if (replyText) { for (const chunk of core.channel.text.chunkMarkdownText(replyText, textLimit)) { + if (abortSignal?.aborted) return; + if (isClientDestroyed(client)) return; const trimmed = chunk.trim(); if (!trimmed) continue; try { @@ -709,6 +721,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara status: "typing", typingParams, runtime, + abortSignal, logLabel: "typing", }); }, diff --git a/extensions/telegram-user/src/monitor/index.ts b/extensions/telegram-user/src/monitor/index.ts index 5e7486089..c9742c7dc 100644 --- a/extensions/telegram-user/src/monitor/index.ts +++ b/extensions/telegram-user/src/monitor/index.ts @@ -18,6 +18,11 @@ async function loadMtcuteDispatcher(): Promise { return mtcuteDispatcherPromise; } +function isDestroyedClientError(err: unknown): boolean { + const message = err instanceof Error ? err.message : String(err); + return /client is destroyed/i.test(message); +} + export type MonitorTelegramUserOpts = { runtime?: RuntimeEnv; abortSignal?: AbortSignal; @@ -33,6 +38,8 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts }); if (!account.enabled) return; + let shuttingDown = false; + const apiId = account.credentials.apiId; const apiHash = account.credentials.apiHash; if (!apiId || !apiHash) { @@ -60,6 +67,7 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts setActiveTelegramUserClient(client); const stop = async () => { + shuttingDown = true; setActiveTelegramUserClient(null); await client.destroy().catch(() => undefined); }; @@ -67,6 +75,7 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts opts.abortSignal?.addEventListener( "abort", () => { + shuttingDown = true; void stop(); }, { once: true }, @@ -83,6 +92,7 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts runtime, accountId: account.accountId, accountConfig: account.config, + abortSignal: opts.abortSignal, self: self ? { id: self.id, username: "username" in self ? self.username : undefined } : undefined, @@ -99,15 +109,31 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts ); await new Promise((resolve, reject) => { - client.onError.add((err) => { - runtime.error?.(`telegram-user client error: ${String(err)}`); + let settled = false; + const settleResolve = () => { + if (settled) return; + settled = true; + resolve(); + }; + const settleReject = (err: unknown) => { + if (settled) return; + settled = true; reject(err); + }; + + client.onError.add((err) => { + if (shuttingDown || opts.abortSignal?.aborted || isDestroyedClientError(err)) { + settleResolve(); + return; + } + runtime.error?.(`telegram-user client error: ${String(err)}`); + settleReject(err); }); if (opts.abortSignal?.aborted) { - resolve(); + settleResolve(); return; } - opts.abortSignal?.addEventListener("abort", () => resolve(), { once: true }); + opts.abortSignal?.addEventListener("abort", () => settleResolve(), { once: true }); }); await stop(); From d9ed9c46b2192adeb39f6d0f7b3c53220174ddbb Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Tue, 27 Jan 2026 17:21:18 +0000 Subject: [PATCH 42/46] telegram-user: ignore self messages --- extensions/telegram-user/src/monitor/handler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 1cffa5a5a..61237a2e6 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -304,6 +304,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const sender = await msg.getCompleteSender().catch(() => msg.sender); if (sender.type !== "user") return; if ("isSelf" in sender && sender.isSelf) return; + if (self?.id != null && sender.id === self.id) return; const senderId = String(sender.id); const senderPeer = resolveTelegramUserPeer(senderId); From 125e09ac03aed49010e22696fef91e3e32f7cc9b Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Tue, 27 Jan 2026 18:15:29 +0000 Subject: [PATCH 43/46] telegram-user: require explicit mention in groups --- .../telegram-user/src/monitor/handler.ts | 38 ++++++++++++++++++- extensions/telegram-user/src/monitor/index.ts | 19 +++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 61237a2e6..224f3eb73 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -23,7 +23,7 @@ type TelegramUserHandlerParams = { accountId: string; accountConfig: TelegramUserAccountConfig; abortSignal?: AbortSignal; - self?: { id: number; username?: string | null }; + self?: { id: number; username?: string | null; name?: string | null }; }; function normalizeAllowEntry(raw: string): string { @@ -84,6 +84,37 @@ function isClientDestroyed(client: TelegramClient): boolean { return candidate.destroyed === true; } +function escapeRegExp(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function buildTelegramUserSelfMentionRegexes(params: { + username?: string | null; + name?: string | null; +}): RegExp[] { + const patterns: string[] = []; + const username = params.username?.trim().replace(/^@/, ""); + if (username) { + patterns.push(String.raw`\b@?${escapeRegExp(username)}\b`); + } + const name = params.name?.trim(); + if (name) { + const parts = name.split(/\s+/).filter(Boolean).map(escapeRegExp); + if (parts.length > 0) { + patterns.push(String.raw`\b@?${parts.join(String.raw`\s+`)}\b`); + } + } + return patterns + .map((pattern) => { + try { + return new RegExp(pattern, "i"); + } catch { + return null; + } + }) + .filter((entry): entry is RegExp => Boolean(entry)); +} + async function safeSendTyping(params: { client: TelegramClient; target: number | string; @@ -452,7 +483,10 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara id: isGroup && groupPeerId ? groupPeerId : senderId, }, }); - const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg, route.agentId); + const mentionRegexes = [ + ...core.channel.mentions.buildMentionRegexes(cfg, route.agentId), + ...buildTelegramUserSelfMentionRegexes({ username: self?.username, name: self?.name }), + ]; const hasAnyMention = msg.entities.some( (ent) => ent.kind === "mention" || ent.kind === "text_mention", ); diff --git a/extensions/telegram-user/src/monitor/index.ts b/extensions/telegram-user/src/monitor/index.ts index c9742c7dc..1110c56b9 100644 --- a/extensions/telegram-user/src/monitor/index.ts +++ b/extensions/telegram-user/src/monitor/index.ts @@ -86,6 +86,19 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts const { Dispatcher, filters } = await loadMtcuteDispatcher(); const dispatcher = Dispatcher.for(client); const self = await client.getMe().catch(() => undefined); + const selfName = + self && typeof (self as unknown as { displayName?: unknown }).displayName === "string" + ? (self as unknown as { displayName: string }).displayName + : self && typeof (self as unknown as { firstName?: unknown }).firstName === "string" + ? [ + (self as unknown as { firstName?: string }).firstName, + typeof (self as unknown as { lastName?: unknown }).lastName === "string" + ? (self as unknown as { lastName: string }).lastName + : undefined, + ] + .filter((entry): entry is string => Boolean(entry && entry.trim())) + .join(" ") + : undefined; const handleMessage = createTelegramUserMessageHandler({ client, cfg, @@ -94,7 +107,11 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts accountConfig: account.config, abortSignal: opts.abortSignal, self: self - ? { id: self.id, username: "username" in self ? self.username : undefined } + ? { + id: self.id, + username: "username" in self ? self.username : undefined, + name: selfName, + } : undefined, }); From c434e499079a83282d4ba33c08752d081368c47d Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Wed, 28 Jan 2026 19:05:57 +0000 Subject: [PATCH 44/46] Telegram user: fix timestamps and moltbot naming --- .github/labeler.yml | 5 +++ docs/channels/telegram-user.md | 14 ++++---- extensions/telegram-user/index.ts | 4 +-- extensions/telegram-user/package.json | 12 +++---- extensions/telegram-user/src/active-client.ts | 24 +++++++++++--- extensions/telegram-user/src/channel.test.ts | 8 ++--- extensions/telegram-user/src/channel.ts | 17 +++++----- extensions/telegram-user/src/client.ts | 8 ++--- .../telegram-user/src/directory-config.ts | 5 ++- .../telegram-user/src/monitor/handler.test.ts | 23 +++++++++++++ .../telegram-user/src/monitor/handler.ts | 20 +++++++++--- extensions/telegram-user/src/monitor/index.ts | 6 ++-- extensions/telegram-user/src/onboarding.ts | 32 +++++++++---------- extensions/telegram-user/src/send.ts | 5 ++- 14 files changed, 120 insertions(+), 63 deletions(-) create mode 100644 extensions/telegram-user/src/monitor/handler.test.ts diff --git a/.github/labeler.yml b/.github/labeler.yml index 5c19fa418..73bc4fb75 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -68,6 +68,11 @@ - "src/telegram/**" - "extensions/telegram/**" - "docs/channels/telegram.md" +"channel: telegram-user": + - changed-files: + - any-glob-to-any-file: + - "extensions/telegram-user/**" + - "docs/channels/telegram-user.md" "channel: tlon": - changed-files: - any-glob-to-any-file: diff --git a/docs/channels/telegram-user.md b/docs/channels/telegram-user.md index 36d59ae37..ff058ff43 100644 --- a/docs/channels/telegram-user.md +++ b/docs/channels/telegram-user.md @@ -3,7 +3,7 @@ summary: "Connect a Telegram user account via MTProto (DMs + groups)" --- # Telegram User -Telegram User connects Clawdbot to a **personal Telegram account** using MTProto. +Telegram User connects Moltbot to a **personal Telegram account** using MTProto. Use this when you need user-level DMs or want to message from your own account in groups. ## Requirements @@ -16,7 +16,7 @@ Use this when you need user-level DMs or want to message from your own account i If the plugin is not bundled, install it: ```bash -clawdbot plugins install @clawdbot/telegram-user +moltbot plugins install @moltbot/telegram-user ``` ## Configure @@ -27,25 +27,25 @@ 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 +moltbot 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 +moltbot 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 +moltbot channels login --channel telegram-user ``` Phone login: ```bash export TELEGRAM_USER_PHONE="+15551234567" -clawdbot channels login --channel telegram-user +moltbot channels login --channel telegram-user ``` Optional env helpers: @@ -57,7 +57,7 @@ Optional env helpers: By default, DMs are protected with pairing. Approve requests with: ```bash -clawdbot pairing approve telegram-user +moltbot pairing approve telegram-user ``` See [Pairing](/start/pairing) for details. diff --git a/extensions/telegram-user/index.ts b/extensions/telegram-user/index.ts index db524a6ed..6bb1142df 100644 --- a/extensions/telegram-user/index.ts +++ b/extensions/telegram-user/index.ts @@ -1,4 +1,4 @@ -import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk"; +import type { MoltbotPluginApi } from "clawdbot/plugin-sdk"; import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk"; import { telegramUserPlugin } from "./src/channel.js"; @@ -9,7 +9,7 @@ const plugin = { name: "Telegram User", description: "Telegram MTProto user channel plugin", configSchema: emptyPluginConfigSchema(), - register(api: ClawdbotPluginApi) { + register(api: MoltbotPluginApi) { setTelegramUserRuntime(api.runtime); api.registerChannel({ plugin: telegramUserPlugin }); }, diff --git a/extensions/telegram-user/package.json b/extensions/telegram-user/package.json index 5aae90caa..c312860cb 100644 --- a/extensions/telegram-user/package.json +++ b/extensions/telegram-user/package.json @@ -1,9 +1,9 @@ { - "name": "@clawdbot/telegram-user", + "name": "@moltbot/telegram-user", "version": "2026.1.22", "type": "module", - "description": "Clawdbot Telegram user (MTProto) channel plugin", - "clawdbot": { + "description": "Moltbot Telegram user (MTProto) channel plugin", + "moltbot": { "extensions": [ "./index.ts" ], @@ -19,7 +19,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/telegram-user", + "npmSpec": "@moltbot/telegram-user", "localPath": "extensions/telegram-user", "defaultChoice": "npm" } @@ -33,9 +33,9 @@ "zod": "^4.3.6" }, "devDependencies": { - "clawdbot": "workspace:*" + "moltbot": "workspace:*" }, "peerDependencies": { - "clawdbot": ">=2026.1.25" + "moltbot": ">=2026.1.25" } } diff --git a/extensions/telegram-user/src/active-client.ts b/extensions/telegram-user/src/active-client.ts index 4eb60171f..9d28de517 100644 --- a/extensions/telegram-user/src/active-client.ts +++ b/extensions/telegram-user/src/active-client.ts @@ -1,11 +1,25 @@ import type { TelegramClient } from "@mtcute/node"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk"; -let activeClient: TelegramClient | null = null; +const activeClients = new Map(); -export function setActiveTelegramUserClient(next: TelegramClient | null) { - activeClient = next; +function resolveAccountKey(accountId?: string | null): string { + return normalizeAccountId(accountId ?? DEFAULT_ACCOUNT_ID); } -export function getActiveTelegramUserClient(): TelegramClient | null { - return activeClient; +export function setActiveTelegramUserClient( + accountId: string | null | undefined, + next: TelegramClient | null, +) { + const key = resolveAccountKey(accountId); + if (next) { + activeClients.set(key, next); + return; + } + activeClients.delete(key); +} + +export function getActiveTelegramUserClient(accountId?: string | null): TelegramClient | null { + const key = resolveAccountKey(accountId); + return activeClients.get(key) ?? null; } diff --git a/extensions/telegram-user/src/channel.test.ts b/extensions/telegram-user/src/channel.test.ts index 777a817c8..00c7fd3b4 100644 --- a/extensions/telegram-user/src/channel.test.ts +++ b/extensions/telegram-user/src/channel.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { ClawdbotConfig, RuntimeEnv } from "clawdbot/plugin-sdk"; +import type { MoltbotConfig, RuntimeEnv } from "clawdbot/plugin-sdk"; const sendMediaTelegramUser = vi.fn< typeof import("./send.js").sendMediaTelegramUser @@ -37,7 +37,7 @@ describe("telegram-user channel plugin", () => { mediaMaxMb: 7, }, }, - } satisfies Partial as unknown as ClawdbotConfig; + } satisfies Partial as unknown as MoltbotConfig; const mod = await import("./channel.js"); await mod.telegramUserPlugin.outbound?.sendMedia?.({ @@ -60,7 +60,7 @@ describe("telegram-user channel plugin", () => { channels: { "telegram-user": {}, }, - } satisfies Partial as unknown as ClawdbotConfig; + } satisfies Partial as unknown as MoltbotConfig; const mod = await import("./channel.js"); await mod.telegramUserPlugin.outbound?.sendMedia?.({ @@ -88,7 +88,7 @@ describe("telegram-user channel plugin", () => { }, }, }, - } satisfies Partial as unknown as ClawdbotConfig; + } satisfies Partial as unknown as MoltbotConfig; const mod = await import("./channel.js"); const runtime = { diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index 45d59fb89..372906a4d 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -7,12 +7,13 @@ import { deleteAccountFromConfigSection, formatPairingApproveHint, normalizeAccountId, + PAIRING_APPROVED_MESSAGE, resolveChannelMediaMaxBytes, setAccountEnabledInConfigSection, type ChannelGroupContext, type ChannelPlugin, type ChannelSetupInput, - type ClawdbotConfig, + type MoltbotConfig, type GroupToolPolicyConfig, } from "clawdbot/plugin-sdk"; @@ -108,7 +109,7 @@ export const telegramUserPlugin: ChannelPlugin = { normalizeAllowEntry: (entry) => entry.replace(/^(telegram-user|telegram|tg):/i, "").toLowerCase(), notifyApproval: async ({ id }) => { - await sendMessageTelegramUser(String(id), "Clawdbot: access approved.", {}); + await sendMessageTelegramUser(String(id), PAIRING_APPROVED_MESSAGE, {}); }, }, capabilities: { @@ -337,7 +338,7 @@ export const telegramUserPlugin: ChannelPlugin = { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({ - cfg: cfg as ClawdbotConfig, + cfg: cfg as MoltbotConfig, channelKey: "telegram-user", accountId, name, @@ -355,7 +356,7 @@ export const telegramUserPlugin: ChannelPlugin = { applyAccountConfig: ({ cfg, accountId, input }) => { const setupInput = input as TelegramUserSetupInput; const namedConfig = applyAccountNameToChannelSection({ - cfg: cfg as ClawdbotConfig, + cfg: cfg as MoltbotConfig, channelKey: "telegram-user", accountId, name: setupInput.name, @@ -432,13 +433,13 @@ export const telegramUserPlugin: ChannelPlugin = { throw err; } }, - stopAccount: async () => { + stopAccount: async ({ accountId }) => { const { getActiveTelegramUserClient, setActiveTelegramUserClient } = await import("./active-client.js"); - const active = getActiveTelegramUserClient(); + const active = getActiveTelegramUserClient(accountId); if (active) { await active.destroy().catch(() => undefined); - setActiveTelegramUserClient(null); + setActiveTelegramUserClient(accountId, null); } }, logoutAccount: async ({ accountId, cfg, runtime }) => { @@ -453,7 +454,7 @@ export const telegramUserPlugin: ChannelPlugin = { } } - const nextCfg = { ...cfg } as ClawdbotConfig; + const nextCfg = { ...cfg } as MoltbotConfig; const nextSection = cfg.channels?.["telegram-user"] ? { ...cfg.channels["telegram-user"] } : undefined; diff --git a/extensions/telegram-user/src/client.ts b/extensions/telegram-user/src/client.ts index 156083c49..708541e1b 100644 --- a/extensions/telegram-user/src/client.ts +++ b/extensions/telegram-user/src/client.ts @@ -17,10 +17,10 @@ export async function createTelegramUserClient(params: { // the "import" condition (ESM), eliminating the warning. const { BaseTelegramClient, TelegramClient, NodePlatform } = await loadMtcuteNode(); - class ClawdbotTelegramUserPlatform extends NodePlatform { + class MoltbotTelegramUserPlatform extends NodePlatform { // mtcute's default NodePlatform.beforeExit installs SIGINT/SIGTERM handlers that re-send the - // signal, which can race with Clawdbot's graceful shutdown and close sqlite while writes are - // pending. We only hook into process exit events (no signal handlers) and rely on Clawdbot to + // signal, which can race with Moltbot's graceful shutdown and close sqlite while writes are + // pending. We only hook into process exit events (no signal handlers) and rely on Moltbot to // stop cleanly. override beforeExit(fn: () => void): () => void { const onBeforeExit = () => fn(); @@ -38,7 +38,7 @@ export async function createTelegramUserClient(params: { apiId: params.apiId, apiHash: params.apiHash, storage: params.storagePath, - platform: new ClawdbotTelegramUserPlatform(), + platform: new MoltbotTelegramUserPlatform(), }); return new TelegramClient({ client }); } diff --git a/extensions/telegram-user/src/directory-config.ts b/extensions/telegram-user/src/directory-config.ts index e28755e63..e252df6f1 100644 --- a/extensions/telegram-user/src/directory-config.ts +++ b/extensions/telegram-user/src/directory-config.ts @@ -1,10 +1,10 @@ -import type { ChannelDirectoryEntry, ClawdbotConfig } from "clawdbot/plugin-sdk"; +import type { ChannelDirectoryEntry, MoltbotConfig } from "clawdbot/plugin-sdk"; import { resolveTelegramUserAccount } from "./accounts.js"; import type { CoreConfig } from "./types.js"; export type TelegramUserDirectoryConfigParams = { - cfg: ClawdbotConfig; + cfg: MoltbotConfig; accountId?: string | null; query?: string | null; limit?: number | null; @@ -65,4 +65,3 @@ export async function listTelegramUserDirectoryGroupsFromConfig( .slice(0, params.limit && params.limit > 0 ? params.limit : undefined) .map((id) => ({ kind: "group", id }) as const); } - diff --git a/extensions/telegram-user/src/monitor/handler.test.ts b/extensions/telegram-user/src/monitor/handler.test.ts new file mode 100644 index 000000000..41898dc4f --- /dev/null +++ b/extensions/telegram-user/src/monitor/handler.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; + +import { resolveTelegramUserTimestampMs } from "./handler.js"; + +describe("resolveTelegramUserTimestampMs", () => { + it("uses Date values directly", () => { + const date = new Date("2025-01-02T03:04:05Z"); + expect(resolveTelegramUserTimestampMs(date)).toBe(date.getTime()); + }); + + it("converts seconds to milliseconds", () => { + expect(resolveTelegramUserTimestampMs(1_710_000_000)).toBe(1_710_000_000 * 1000); + }); + + it("passes through millisecond values", () => { + expect(resolveTelegramUserTimestampMs(1_710_000_000_000)).toBe(1_710_000_000_000); + }); + + it("returns undefined for invalid dates", () => { + const invalid = new Date("invalid"); + expect(resolveTelegramUserTimestampMs(invalid)).toBeUndefined(); + }); +}); diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 224f3eb73..5bb3c2ae3 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -115,6 +115,20 @@ function buildTelegramUserSelfMentionRegexes(params: { .filter((entry): entry is RegExp => Boolean(entry)); } +export function resolveTelegramUserTimestampMs( + value: Date | number | null | undefined, +): number | undefined { + if (value == null) return undefined; + if (value instanceof Date) { + const ms = value.getTime(); + return Number.isFinite(ms) ? ms : undefined; + } + if (typeof value === "number" && Number.isFinite(value)) { + return value < 1_000_000_000_000 ? Math.round(value * 1000) : Math.round(value); + } + return undefined; +} + async function safeSendTyping(params: { client: TelegramClient; target: number | string; @@ -348,9 +362,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const chatId = msg.chat.type === "chat" ? msg.chat.id : undefined; const isForum = msg.chat.type === "chat" && msg.chat.isForum === true; const threadId = - isGroup && msg.isTopicMessage - ? msg.replyToMessage?.threadId ?? undefined - : undefined; + isGroup && isForum ? msg.replyToMessage?.threadId ?? undefined : undefined; const { groupConfig, topicConfig } = isGroup && chatId != null ? resolveTelegramUserGroupConfig(accountConfig, chatId, threadId) @@ -452,7 +464,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const media = allMedia[0] ?? null; const rawBody = [text, locationText, pollText].filter(Boolean).join("\n").trim(); if (!rawBody && !media) return; - const timestampMs = msg.date ? msg.date * 1000 : undefined; + const timestampMs = resolveTelegramUserTimestampMs(msg.date); const replyInfo = msg.replyToMessage ?? null; const replyToId = replyInfo?.id != null ? String(replyInfo.id) : undefined; const replyToSender = replyInfo?.sender diff --git a/extensions/telegram-user/src/monitor/index.ts b/extensions/telegram-user/src/monitor/index.ts index 1110c56b9..e7c0e46e5 100644 --- a/extensions/telegram-user/src/monitor/index.ts +++ b/extensions/telegram-user/src/monitor/index.ts @@ -60,15 +60,15 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts const storagePath = resolveTelegramUserSessionPath(account.accountId); if (!fs.existsSync(storagePath)) { throw new Error( - "Telegram user session missing. Run `clawdbot channels login --channel telegram-user` first.", + "Telegram user session missing. Run `moltbot channels login --channel telegram-user` first.", ); } const client = await createTelegramUserClient({ apiId, apiHash, storagePath }); - setActiveTelegramUserClient(client); + setActiveTelegramUserClient(account.accountId, client); const stop = async () => { shuttingDown = true; - setActiveTelegramUserClient(null); + setActiveTelegramUserClient(account.accountId, null); await client.destroy().catch(() => undefined); }; diff --git a/extensions/telegram-user/src/onboarding.ts b/extensions/telegram-user/src/onboarding.ts index 6ac988a8d..0c9f0933a 100644 --- a/extensions/telegram-user/src/onboarding.ts +++ b/extensions/telegram-user/src/onboarding.ts @@ -6,7 +6,7 @@ import { normalizeAccountId, type ChannelOnboardingAdapter, type ChannelOnboardingDmPolicy, - type ClawdbotConfig, + type MoltbotConfig, type DmPolicy, type WizardPrompter, } from "clawdbot/plugin-sdk"; @@ -24,10 +24,10 @@ const channel = "telegram-user" as const; type TelegramUserChannelConfig = NonNullable["telegram-user"]; function setTelegramUserDmPolicy( - cfg: ClawdbotConfig, + cfg: MoltbotConfig, policy: DmPolicy, accountId?: string, -): ClawdbotConfig { +): MoltbotConfig { const resolvedAccountId = normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID; const current = cfg.channels?.["telegram-user"] as TelegramUserChannelConfig | undefined; const allowFrom = @@ -38,13 +38,13 @@ function setTelegramUserDmPolicy( if (resolvedAccountId === DEFAULT_ACCOUNT_ID) { return { ...cfg, - channels: { - ...cfg.channels, - "telegram-user": { - ...(current ?? {}), - dmPolicy: policy, - ...(allowFrom ? { allowFrom } : {}), - }, + channels: { + ...cfg.channels, + "telegram-user": { + ...(current ?? {}), + dmPolicy: policy, + ...(allowFrom ? { allowFrom } : {}), + }, }, }; } @@ -73,7 +73,7 @@ async function noteTelegramUserAuthHelp(prompter: WizardPrompter): Promise [ "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`.", + "Login happens via `moltbot channels login --channel telegram-user`.", `Docs: ${formatDocsLink("/channels/telegram-user", "channels/telegram-user")}`, ].join("\n"), "Telegram user setup", @@ -94,10 +94,10 @@ function parseAllowFromInput(raw: string): string[] { } async function promptTelegramUserAllowFrom(params: { - cfg: ClawdbotConfig; + cfg: MoltbotConfig; prompter: WizardPrompter; accountId?: string; -}): Promise { +}): Promise { const accountId = normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID; const resolved = resolveTelegramUserAccount({ cfg: params.cfg as CoreConfig, @@ -197,7 +197,7 @@ export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = { let accountId = override ? normalizeAccountId(override) : defaultAccountId; if (shouldPromptAccountIds && !override) { accountId = await promptAccountId({ - cfg: cfg as ClawdbotConfig, + cfg: cfg as MoltbotConfig, prompter, label: "Telegram User", currentId: accountId ?? defaultAccountId, @@ -344,7 +344,7 @@ export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = { } catch (err) { runtime.error(`Telegram user login failed: ${String(err)}`); await prompter.note( - `Run \`clawdbot channels login --channel telegram-user\` later to link.`, + `Run \`moltbot channels login --channel telegram-user\` later to link.`, "Telegram user login", ); } @@ -353,7 +353,7 @@ export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = { await prompter.note( [ "Next: link the account via QR or phone code.", - "Run: clawdbot channels login --channel telegram-user", + "Run: moltbot channels login --channel telegram-user", ].join("\n"), "Telegram user login", ); diff --git a/extensions/telegram-user/src/send.ts b/extensions/telegram-user/src/send.ts index 5d55b0354..13e6558eb 100644 --- a/extensions/telegram-user/src/send.ts +++ b/extensions/telegram-user/src/send.ts @@ -6,6 +6,7 @@ import { getTelegramUserRuntime } from "./runtime.js"; import { resolveTelegramUserAccount } from "./accounts.js"; import { createTelegramUserClient } from "./client.js"; import { resolveTelegramUserSessionPath } from "./session.js"; +import { getActiveTelegramUserClient } from "./active-client.js"; import type { CoreConfig } from "./types.js"; export type TelegramUserSendResult = { @@ -145,6 +146,8 @@ async function resolveClient(params: { cfg: params.cfg, accountId: params.accountId, }); + const active = getActiveTelegramUserClient(account.accountId); + if (active) return { client: active, stopOnDone: false }; const apiId = account.credentials.apiId; const apiHash = account.credentials.apiHash; if (!apiId || !apiHash) { @@ -153,7 +156,7 @@ async function resolveClient(params: { const storagePath = resolveTelegramUserSessionPath(account.accountId); if (!fs.existsSync(storagePath)) { throw new Error( - "Telegram user session missing. Run `clawdbot channels login --channel telegram-user` first.", + "Telegram user session missing. Run `moltbot channels login --channel telegram-user` first.", ); } const client = await createTelegramUserClient({ apiId, apiHash, storagePath }); From a12e96b3d3176302b4cbc2c4ffd442c6d08a6a83 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 30 Jan 2026 12:16:39 +0000 Subject: [PATCH 45/46] telegram-user: harden login and monitor lifecycle --- extensions/telegram-user/src/login.ts | 14 +- .../telegram-user/src/monitor/handler.ts | 6 +- extensions/telegram-user/src/monitor/index.ts | 141 +++++++++--------- 3 files changed, 87 insertions(+), 74 deletions(-) diff --git a/extensions/telegram-user/src/login.ts b/extensions/telegram-user/src/login.ts index e8363942e..1ba88c9de 100644 --- a/extensions/telegram-user/src/login.ts +++ b/extensions/telegram-user/src/login.ts @@ -46,6 +46,10 @@ export async function loginTelegramUser(params: { let phoneEnv = process.env.TELEGRAM_USER_PHONE?.trim() || undefined; const codeEnv = process.env.TELEGRAM_USER_CODE?.trim() || undefined; + const passwordPrompt = passwordEnv + ? passwordEnv + : async () => await promptText("2FA password: "); + try { if (!phoneEnv) { const mode = await promptLoginMode(); @@ -53,12 +57,12 @@ export async function loginTelegramUser(params: { phoneEnv = await promptText("Telegram phone number (E.164): "); } } - const user = await client.start( + const user = await client.start( phoneEnv ? { phone: phoneEnv, code: codeEnv ? codeEnv : async () => await promptText("Telegram code: "), - password: passwordEnv ? passwordEnv : async () => await promptText("2FA password: "), + password: passwordPrompt, codeSentCallback: (code) => { runtime.log( `Telegram code sent via ${code.type}. Check your device and enter it here.`, @@ -84,11 +88,13 @@ export async function loginTelegramUser(params: { runtime.log(`Scan this QR in Telegram (expires ${expires.toLocaleTimeString()}):`); qrcode.generate(url, { small: true }); }, - ...(passwordEnv ? { password: passwordEnv } : {}), + password: passwordPrompt, invalidCodeCallback: async (type) => { if (type === "password") { runtime.error?.( - "Telegram 2FA password rejected. Set TELEGRAM_USER_PASSWORD and rerun.", + passwordEnv + ? "Telegram 2FA password rejected. Update TELEGRAM_USER_PASSWORD and rerun." + : "Telegram 2FA password rejected. Try again.", ); } }, diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 5bb3c2ae3..56aa3d5ed 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -361,8 +361,9 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara const combinedAllowFrom = [...allowFrom, ...storeAllowFrom]; const chatId = msg.chat.type === "chat" ? msg.chat.id : undefined; const isForum = msg.chat.type === "chat" && msg.chat.isForum === true; + const isTopicMessage = msg.isTopicMessage === true; const threadId = - isGroup && isForum ? msg.replyToMessage?.threadId ?? undefined : undefined; + isGroup && isForum && isTopicMessage ? msg.replyToMessage?.threadId ?? undefined : undefined; const { groupConfig, topicConfig } = isGroup && chatId != null ? resolveTelegramUserGroupConfig(accountConfig, chatId, threadId) @@ -499,7 +500,8 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara ...core.channel.mentions.buildMentionRegexes(cfg, route.agentId), ...buildTelegramUserSelfMentionRegexes({ username: self?.username, name: self?.name }), ]; - const hasAnyMention = msg.entities.some( + const entities = msg.entities ?? []; + const hasAnyMention = entities.some( (ent) => ent.kind === "mention" || ent.kind === "text_mention", ); const hasControlCommandInMessage = core.channel.text.hasControlCommand(text, cfg, { diff --git a/extensions/telegram-user/src/monitor/index.ts b/extensions/telegram-user/src/monitor/index.ts index e7c0e46e5..bc334018a 100644 --- a/extensions/telegram-user/src/monitor/index.ts +++ b/extensions/telegram-user/src/monitor/index.ts @@ -64,9 +64,11 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts ); } const client = await createTelegramUserClient({ apiId, apiHash, storagePath }); - setActiveTelegramUserClient(account.accountId, client); + let stopped = false; const stop = async () => { + if (stopped) return; + stopped = true; shuttingDown = true; setActiveTelegramUserClient(account.accountId, null); await client.destroy().catch(() => undefined); @@ -81,77 +83,80 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts { once: true }, ); - await client.start(); + try { + await client.start(); + setActiveTelegramUserClient(account.accountId, client); - const { Dispatcher, filters } = await loadMtcuteDispatcher(); - const dispatcher = Dispatcher.for(client); - const self = await client.getMe().catch(() => undefined); - const selfName = - self && typeof (self as unknown as { displayName?: unknown }).displayName === "string" - ? (self as unknown as { displayName: string }).displayName - : self && typeof (self as unknown as { firstName?: unknown }).firstName === "string" - ? [ - (self as unknown as { firstName?: string }).firstName, - typeof (self as unknown as { lastName?: unknown }).lastName === "string" - ? (self as unknown as { lastName: string }).lastName - : undefined, - ] - .filter((entry): entry is string => Boolean(entry && entry.trim())) - .join(" ") - : undefined; - const handleMessage = createTelegramUserMessageHandler({ - client, - cfg, - runtime, - accountId: account.accountId, - accountConfig: account.config, - abortSignal: opts.abortSignal, - self: self - ? { - id: self.id, - username: "username" in self ? self.username : undefined, - name: selfName, + const { Dispatcher, filters } = await loadMtcuteDispatcher(); + const dispatcher = Dispatcher.for(client); + const self = await client.getMe().catch(() => undefined); + const selfName = + self && typeof (self as unknown as { displayName?: unknown }).displayName === "string" + ? (self as unknown as { displayName: string }).displayName + : self && typeof (self as unknown as { firstName?: unknown }).firstName === "string" + ? [ + (self as unknown as { firstName?: string }).firstName, + typeof (self as unknown as { lastName?: unknown }).lastName === "string" + ? (self as unknown as { lastName: string }).lastName + : undefined, + ] + .filter((entry): entry is string => Boolean(entry && entry.trim())) + .join(" ") + : undefined; + const handleMessage = createTelegramUserMessageHandler({ + client, + cfg, + runtime, + accountId: account.accountId, + accountConfig: account.config, + abortSignal: opts.abortSignal, + self: self + ? { + id: self.id, + username: "username" in self ? self.username : undefined, + name: selfName, + } + : undefined, + }); + + dispatcher.onNewMessage( + filters.or( + filters.chat("user"), + filters.chat("group"), + filters.chat("supergroup"), + filters.chat("gigagroup"), + ), + handleMessage, + ); + + await new Promise((resolve, reject) => { + let settled = false; + const settleResolve = () => { + if (settled) return; + settled = true; + resolve(); + }; + const settleReject = (err: unknown) => { + if (settled) return; + settled = true; + reject(err); + }; + + client.onError.add((err) => { + if (shuttingDown || opts.abortSignal?.aborted || isDestroyedClientError(err)) { + settleResolve(); + return; } - : undefined, - }); - - dispatcher.onNewMessage( - filters.or( - filters.chat("user"), - filters.chat("group"), - filters.chat("supergroup"), - filters.chat("gigagroup"), - ), - handleMessage, - ); - - await new Promise((resolve, reject) => { - let settled = false; - const settleResolve = () => { - if (settled) return; - settled = true; - resolve(); - }; - const settleReject = (err: unknown) => { - if (settled) return; - settled = true; - reject(err); - }; - - client.onError.add((err) => { - if (shuttingDown || opts.abortSignal?.aborted || isDestroyedClientError(err)) { + runtime.error?.(`telegram-user client error: ${String(err)}`); + settleReject(err); + }); + if (opts.abortSignal?.aborted) { settleResolve(); return; } - runtime.error?.(`telegram-user client error: ${String(err)}`); - settleReject(err); + opts.abortSignal?.addEventListener("abort", () => settleResolve(), { once: true }); }); - if (opts.abortSignal?.aborted) { - settleResolve(); - return; - } - opts.abortSignal?.addEventListener("abort", () => settleResolve(), { once: true }); - }); - - await stop(); + } finally { + await stop(); + } } From c463b13ef77e597de27bf1203638984463affc56 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Fri, 30 Jan 2026 13:58:32 +0000 Subject: [PATCH 46/46] telegram-user: rebrand plugin to openclaw --- docs/channels/telegram-user.md | 14 ++++++------- extensions/telegram-user/index.ts | 6 +++--- ...wdbot.plugin.json => openclaw.plugin.json} | 0 extensions/telegram-user/package.json | 14 ++++++------- extensions/telegram-user/src/accounts.ts | 2 +- extensions/telegram-user/src/active-client.ts | 2 +- extensions/telegram-user/src/channel.test.ts | 8 ++++---- extensions/telegram-user/src/channel.ts | 10 +++++----- extensions/telegram-user/src/client.ts | 8 ++++---- extensions/telegram-user/src/config-schema.ts | 2 +- .../telegram-user/src/directory-config.ts | 4 ++-- extensions/telegram-user/src/login.ts | 2 +- .../telegram-user/src/monitor/handler.ts | 4 ++-- extensions/telegram-user/src/monitor/index.ts | 4 ++-- extensions/telegram-user/src/onboarding.ts | 20 +++++++++---------- extensions/telegram-user/src/runtime.ts | 2 +- extensions/telegram-user/src/send.ts | 4 ++-- extensions/telegram-user/src/session.ts | 2 +- extensions/telegram-user/src/types.ts | 2 +- pnpm-lock.yaml | 2 +- 20 files changed, 56 insertions(+), 56 deletions(-) rename extensions/telegram-user/{clawdbot.plugin.json => openclaw.plugin.json} (100%) diff --git a/docs/channels/telegram-user.md b/docs/channels/telegram-user.md index ff058ff43..02678951c 100644 --- a/docs/channels/telegram-user.md +++ b/docs/channels/telegram-user.md @@ -3,7 +3,7 @@ summary: "Connect a Telegram user account via MTProto (DMs + groups)" --- # Telegram User -Telegram User connects Moltbot to a **personal Telegram account** using MTProto. +Telegram User connects OpenClaw to a **personal Telegram account** using MTProto. Use this when you need user-level DMs or want to message from your own account in groups. ## Requirements @@ -16,7 +16,7 @@ Use this when you need user-level DMs or want to message from your own account i If the plugin is not bundled, install it: ```bash -moltbot plugins install @moltbot/telegram-user +openclaw plugins install @openclaw/telegram-user ``` ## Configure @@ -27,25 +27,25 @@ Option A: env vars (default account only) ```bash export TELEGRAM_USER_API_ID="123456" export TELEGRAM_USER_API_HASH="your_api_hash" -moltbot channels add --channel telegram-user --use-env +openclaw channels add --channel telegram-user --use-env ``` Option B: config ```bash -moltbot channels add --channel telegram-user --api-id 123456 --api-hash your_api_hash +openclaw channels add --channel telegram-user --api-id 123456 --api-hash your_api_hash ``` ## Login (QR or phone code) QR login (default): ```bash -moltbot channels login --channel telegram-user +openclaw channels login --channel telegram-user ``` Phone login: ```bash export TELEGRAM_USER_PHONE="+15551234567" -moltbot channels login --channel telegram-user +openclaw channels login --channel telegram-user ``` Optional env helpers: @@ -57,7 +57,7 @@ Optional env helpers: By default, DMs are protected with pairing. Approve requests with: ```bash -moltbot pairing approve telegram-user +openclaw pairing approve telegram-user ``` See [Pairing](/start/pairing) for details. diff --git a/extensions/telegram-user/index.ts b/extensions/telegram-user/index.ts index 6bb1142df..8d8ac04d7 100644 --- a/extensions/telegram-user/index.ts +++ b/extensions/telegram-user/index.ts @@ -1,5 +1,5 @@ -import type { MoltbotPluginApi } from "clawdbot/plugin-sdk"; -import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk"; import { telegramUserPlugin } from "./src/channel.js"; import { setTelegramUserRuntime } from "./src/runtime.js"; @@ -9,7 +9,7 @@ const plugin = { name: "Telegram User", description: "Telegram MTProto user channel plugin", configSchema: emptyPluginConfigSchema(), - register(api: MoltbotPluginApi) { + register(api: OpenClawPluginApi) { setTelegramUserRuntime(api.runtime); api.registerChannel({ plugin: telegramUserPlugin }); }, diff --git a/extensions/telegram-user/clawdbot.plugin.json b/extensions/telegram-user/openclaw.plugin.json similarity index 100% rename from extensions/telegram-user/clawdbot.plugin.json rename to extensions/telegram-user/openclaw.plugin.json diff --git a/extensions/telegram-user/package.json b/extensions/telegram-user/package.json index c312860cb..903286ee4 100644 --- a/extensions/telegram-user/package.json +++ b/extensions/telegram-user/package.json @@ -1,9 +1,9 @@ { - "name": "@moltbot/telegram-user", - "version": "2026.1.22", + "name": "@openclaw/telegram-user", + "version": "2026.1.29", "type": "module", - "description": "Moltbot Telegram user (MTProto) channel plugin", - "moltbot": { + "description": "OpenClaw Telegram user (MTProto) channel plugin", + "openclaw": { "extensions": [ "./index.ts" ], @@ -19,7 +19,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@moltbot/telegram-user", + "npmSpec": "@openclaw/telegram-user", "localPath": "extensions/telegram-user", "defaultChoice": "npm" } @@ -33,9 +33,9 @@ "zod": "^4.3.6" }, "devDependencies": { - "moltbot": "workspace:*" + "openclaw": "workspace:*" }, "peerDependencies": { - "moltbot": ">=2026.1.25" + "openclaw": ">=2026.1.29" } } diff --git a/extensions/telegram-user/src/accounts.ts b/extensions/telegram-user/src/accounts.ts index 6754a1052..2f6a29ac3 100644 --- a/extensions/telegram-user/src/accounts.ts +++ b/extensions/telegram-user/src/accounts.ts @@ -1,5 +1,5 @@ import type { CoreConfig, TelegramUserAccountConfig } from "./types.js"; -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk"; export type TelegramUserCredentials = { apiId?: number; diff --git a/extensions/telegram-user/src/active-client.ts b/extensions/telegram-user/src/active-client.ts index 9d28de517..d6639ab71 100644 --- a/extensions/telegram-user/src/active-client.ts +++ b/extensions/telegram-user/src/active-client.ts @@ -1,5 +1,5 @@ import type { TelegramClient } from "@mtcute/node"; -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk"; const activeClients = new Map(); diff --git a/extensions/telegram-user/src/channel.test.ts b/extensions/telegram-user/src/channel.test.ts index 00c7fd3b4..dc3161a36 100644 --- a/extensions/telegram-user/src/channel.test.ts +++ b/extensions/telegram-user/src/channel.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { MoltbotConfig, RuntimeEnv } from "clawdbot/plugin-sdk"; +import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk"; const sendMediaTelegramUser = vi.fn< typeof import("./send.js").sendMediaTelegramUser @@ -37,7 +37,7 @@ describe("telegram-user channel plugin", () => { mediaMaxMb: 7, }, }, - } satisfies Partial as unknown as MoltbotConfig; + } satisfies Partial as unknown as OpenClawConfig; const mod = await import("./channel.js"); await mod.telegramUserPlugin.outbound?.sendMedia?.({ @@ -60,7 +60,7 @@ describe("telegram-user channel plugin", () => { channels: { "telegram-user": {}, }, - } satisfies Partial as unknown as MoltbotConfig; + } satisfies Partial as unknown as OpenClawConfig; const mod = await import("./channel.js"); await mod.telegramUserPlugin.outbound?.sendMedia?.({ @@ -88,7 +88,7 @@ describe("telegram-user channel plugin", () => { }, }, }, - } satisfies Partial as unknown as MoltbotConfig; + } satisfies Partial as unknown as OpenClawConfig; const mod = await import("./channel.js"); const runtime = { diff --git a/extensions/telegram-user/src/channel.ts b/extensions/telegram-user/src/channel.ts index 372906a4d..ffd9fe1fc 100644 --- a/extensions/telegram-user/src/channel.ts +++ b/extensions/telegram-user/src/channel.ts @@ -13,9 +13,9 @@ import { type ChannelGroupContext, type ChannelPlugin, type ChannelSetupInput, - type MoltbotConfig, + type OpenClawConfig, type GroupToolPolicyConfig, -} from "clawdbot/plugin-sdk"; +} from "openclaw/plugin-sdk"; import { listTelegramUserAccountIds, @@ -338,7 +338,7 @@ export const telegramUserPlugin: ChannelPlugin = { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({ - cfg: cfg as MoltbotConfig, + cfg: cfg as OpenClawConfig, channelKey: "telegram-user", accountId, name, @@ -356,7 +356,7 @@ export const telegramUserPlugin: ChannelPlugin = { applyAccountConfig: ({ cfg, accountId, input }) => { const setupInput = input as TelegramUserSetupInput; const namedConfig = applyAccountNameToChannelSection({ - cfg: cfg as MoltbotConfig, + cfg: cfg as OpenClawConfig, channelKey: "telegram-user", accountId, name: setupInput.name, @@ -454,7 +454,7 @@ export const telegramUserPlugin: ChannelPlugin = { } } - const nextCfg = { ...cfg } as MoltbotConfig; + const nextCfg = { ...cfg } as OpenClawConfig; const nextSection = cfg.channels?.["telegram-user"] ? { ...cfg.channels["telegram-user"] } : undefined; diff --git a/extensions/telegram-user/src/client.ts b/extensions/telegram-user/src/client.ts index 708541e1b..bce7e3b4f 100644 --- a/extensions/telegram-user/src/client.ts +++ b/extensions/telegram-user/src/client.ts @@ -17,10 +17,10 @@ export async function createTelegramUserClient(params: { // the "import" condition (ESM), eliminating the warning. const { BaseTelegramClient, TelegramClient, NodePlatform } = await loadMtcuteNode(); - class MoltbotTelegramUserPlatform extends NodePlatform { + class OpenClawTelegramUserPlatform extends NodePlatform { // mtcute's default NodePlatform.beforeExit installs SIGINT/SIGTERM handlers that re-send the - // signal, which can race with Moltbot's graceful shutdown and close sqlite while writes are - // pending. We only hook into process exit events (no signal handlers) and rely on Moltbot to + // signal, which can race with OpenClaw's graceful shutdown and close sqlite while writes are + // pending. We only hook into process exit events (no signal handlers) and rely on OpenClaw to // stop cleanly. override beforeExit(fn: () => void): () => void { const onBeforeExit = () => fn(); @@ -38,7 +38,7 @@ export async function createTelegramUserClient(params: { apiId: params.apiId, apiHash: params.apiHash, storage: params.storagePath, - platform: new MoltbotTelegramUserPlatform(), + platform: new OpenClawTelegramUserPlatform(), }); return new TelegramClient({ client }); } diff --git a/extensions/telegram-user/src/config-schema.ts b/extensions/telegram-user/src/config-schema.ts index 0f42c0762..a844abae5 100644 --- a/extensions/telegram-user/src/config-schema.ts +++ b/extensions/telegram-user/src/config-schema.ts @@ -5,7 +5,7 @@ import { GroupPolicySchema, ToolPolicySchema, requireOpenAllowFrom, -} from "clawdbot/plugin-sdk"; +} from "openclaw/plugin-sdk"; const allowFromEntry = z.union([z.string(), z.number()]); diff --git a/extensions/telegram-user/src/directory-config.ts b/extensions/telegram-user/src/directory-config.ts index e252df6f1..182b7618f 100644 --- a/extensions/telegram-user/src/directory-config.ts +++ b/extensions/telegram-user/src/directory-config.ts @@ -1,10 +1,10 @@ -import type { ChannelDirectoryEntry, MoltbotConfig } from "clawdbot/plugin-sdk"; +import type { ChannelDirectoryEntry, OpenClawConfig } from "openclaw/plugin-sdk"; import { resolveTelegramUserAccount } from "./accounts.js"; import type { CoreConfig } from "./types.js"; export type TelegramUserDirectoryConfigParams = { - cfg: MoltbotConfig; + cfg: OpenClawConfig; accountId?: string | null; query?: string | null; limit?: number | null; diff --git a/extensions/telegram-user/src/login.ts b/extensions/telegram-user/src/login.ts index 1ba88c9de..778a56cf9 100644 --- a/extensions/telegram-user/src/login.ts +++ b/extensions/telegram-user/src/login.ts @@ -2,7 +2,7 @@ import qrcode from "qrcode-terminal"; import { createInterface } from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; import { isCancel, select } from "@clack/prompts"; -import type { RuntimeEnv } from "clawdbot/plugin-sdk"; +import type { RuntimeEnv } from "openclaw/plugin-sdk"; import { createTelegramUserClient } from "./client.js"; import { ensureTelegramUserSessionDir } from "./session.js"; diff --git a/extensions/telegram-user/src/monitor/handler.ts b/extensions/telegram-user/src/monitor/handler.ts index 56aa3d5ed..1c2545d97 100644 --- a/extensions/telegram-user/src/monitor/handler.ts +++ b/extensions/telegram-user/src/monitor/handler.ts @@ -1,6 +1,6 @@ import type { TelegramClient } from "@mtcute/node"; import type { MessageContext } from "@mtcute/dispatcher"; -import type { RuntimeEnv } from "clawdbot/plugin-sdk"; +import type { RuntimeEnv } from "openclaw/plugin-sdk"; import { formatLocationText, @@ -8,7 +8,7 @@ import { resolveMentionGatingWithBypass, toLocationContext, type NormalizedLocation, -} from "clawdbot/plugin-sdk"; +} from "openclaw/plugin-sdk"; import { getTelegramUserRuntime } from "../runtime.js"; import type { CoreConfig, TelegramUserAccountConfig } from "../types.js"; import { sendMediaTelegramUser, sendMessageTelegramUser } from "../send.js"; diff --git a/extensions/telegram-user/src/monitor/index.ts b/extensions/telegram-user/src/monitor/index.ts index bc334018a..4e3b2a5c3 100644 --- a/extensions/telegram-user/src/monitor/index.ts +++ b/extensions/telegram-user/src/monitor/index.ts @@ -1,5 +1,5 @@ import fs from "node:fs"; -import type { RuntimeEnv } from "clawdbot/plugin-sdk"; +import type { RuntimeEnv } from "openclaw/plugin-sdk"; import { createTelegramUserClient } from "../client.js"; import { resolveTelegramUserAccount } from "../accounts.js"; @@ -60,7 +60,7 @@ export async function monitorTelegramUserProvider(opts: MonitorTelegramUserOpts const storagePath = resolveTelegramUserSessionPath(account.accountId); if (!fs.existsSync(storagePath)) { throw new Error( - "Telegram user session missing. Run `moltbot channels login --channel telegram-user` first.", + "Telegram user session missing. Run `openclaw channels login --channel telegram-user` first.", ); } const client = await createTelegramUserClient({ apiId, apiHash, storagePath }); diff --git a/extensions/telegram-user/src/onboarding.ts b/extensions/telegram-user/src/onboarding.ts index 0c9f0933a..923ea709f 100644 --- a/extensions/telegram-user/src/onboarding.ts +++ b/extensions/telegram-user/src/onboarding.ts @@ -6,10 +6,10 @@ import { normalizeAccountId, type ChannelOnboardingAdapter, type ChannelOnboardingDmPolicy, - type MoltbotConfig, + type OpenClawConfig, type DmPolicy, type WizardPrompter, -} from "clawdbot/plugin-sdk"; +} from "openclaw/plugin-sdk"; import { listTelegramUserAccountIds, @@ -24,10 +24,10 @@ const channel = "telegram-user" as const; type TelegramUserChannelConfig = NonNullable["telegram-user"]; function setTelegramUserDmPolicy( - cfg: MoltbotConfig, + cfg: OpenClawConfig, policy: DmPolicy, accountId?: string, -): MoltbotConfig { +): OpenClawConfig { const resolvedAccountId = normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID; const current = cfg.channels?.["telegram-user"] as TelegramUserChannelConfig | undefined; const allowFrom = @@ -73,7 +73,7 @@ async function noteTelegramUserAuthHelp(prompter: WizardPrompter): Promise [ "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 `moltbot channels login --channel telegram-user`.", + "Login happens via `openclaw channels login --channel telegram-user`.", `Docs: ${formatDocsLink("/channels/telegram-user", "channels/telegram-user")}`, ].join("\n"), "Telegram user setup", @@ -94,10 +94,10 @@ function parseAllowFromInput(raw: string): string[] { } async function promptTelegramUserAllowFrom(params: { - cfg: MoltbotConfig; + cfg: OpenClawConfig; prompter: WizardPrompter; accountId?: string; -}): Promise { +}): Promise { const accountId = normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID; const resolved = resolveTelegramUserAccount({ cfg: params.cfg as CoreConfig, @@ -197,7 +197,7 @@ export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = { let accountId = override ? normalizeAccountId(override) : defaultAccountId; if (shouldPromptAccountIds && !override) { accountId = await promptAccountId({ - cfg: cfg as MoltbotConfig, + cfg: cfg as OpenClawConfig, prompter, label: "Telegram User", currentId: accountId ?? defaultAccountId, @@ -344,7 +344,7 @@ export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = { } catch (err) { runtime.error(`Telegram user login failed: ${String(err)}`); await prompter.note( - `Run \`moltbot channels login --channel telegram-user\` later to link.`, + `Run \`openclaw channels login --channel telegram-user\` later to link.`, "Telegram user login", ); } @@ -353,7 +353,7 @@ export const telegramUserOnboardingAdapter: ChannelOnboardingAdapter = { await prompter.note( [ "Next: link the account via QR or phone code.", - "Run: moltbot channels login --channel telegram-user", + "Run: openclaw channels login --channel telegram-user", ].join("\n"), "Telegram user login", ); diff --git a/extensions/telegram-user/src/runtime.ts b/extensions/telegram-user/src/runtime.ts index 464387f19..54b4733a0 100644 --- a/extensions/telegram-user/src/runtime.ts +++ b/extensions/telegram-user/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "clawdbot/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk"; let runtime: PluginRuntime | null = null; diff --git a/extensions/telegram-user/src/send.ts b/extensions/telegram-user/src/send.ts index 13e6558eb..126b88bc1 100644 --- a/extensions/telegram-user/src/send.ts +++ b/extensions/telegram-user/src/send.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import type { TelegramClient } from "@mtcute/node"; -import type { PollInput } from "clawdbot/plugin-sdk"; +import type { PollInput } from "openclaw/plugin-sdk"; import { getTelegramUserRuntime } from "./runtime.js"; import { resolveTelegramUserAccount } from "./accounts.js"; @@ -156,7 +156,7 @@ async function resolveClient(params: { const storagePath = resolveTelegramUserSessionPath(account.accountId); if (!fs.existsSync(storagePath)) { throw new Error( - "Telegram user session missing. Run `moltbot channels login --channel telegram-user` first.", + "Telegram user session missing. Run `openclaw channels login --channel telegram-user` first.", ); } const client = await createTelegramUserClient({ apiId, apiHash, storagePath }); diff --git a/extensions/telegram-user/src/session.ts b/extensions/telegram-user/src/session.ts index 23874658c..6761cf917 100644 --- a/extensions/telegram-user/src/session.ts +++ b/extensions/telegram-user/src/session.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -import { normalizeAccountId } from "clawdbot/plugin-sdk"; +import { normalizeAccountId } from "openclaw/plugin-sdk"; import { getTelegramUserRuntime } from "./runtime.js"; export function resolveTelegramUserSessionPath(accountId?: string | null): string { diff --git a/extensions/telegram-user/src/types.ts b/extensions/telegram-user/src/types.ts index 59a6e32a9..1d6d6953a 100644 --- a/extensions/telegram-user/src/types.ts +++ b/extensions/telegram-user/src/types.ts @@ -1,4 +1,4 @@ -import type { DmPolicy, GroupPolicy, GroupToolPolicyConfig } from "clawdbot/plugin-sdk"; +import type { DmPolicy, GroupPolicy, GroupToolPolicyConfig } from "openclaw/plugin-sdk"; export type TelegramUserTopicConfig = { requireMention?: boolean; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47acbe8f9..ebe17aee5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -436,7 +436,7 @@ importers: specifier: ^4.3.6 version: 4.3.6 devDependencies: - clawdbot: + openclaw: specifier: workspace:* version: link:../..