Telegram user: preserve threads and update docs

This commit is contained in:
Muhammed Mukhthar CM 2026-01-23 17:17:18 +00:00
parent 39d9eb4589
commit 63929bd70c
8 changed files with 57 additions and 20 deletions

View File

@ -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. - [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups. - [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
- [Telegram User](/channels/telegram-user) — MTProto user account; DM-only for now (plugin, installed separately). - [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. - [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
- [Slack](/channels/slack) — Bolt SDK; workspace apps. - [Slack](/channels/slack) — Bolt SDK; workspace apps.
- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook. - [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook.

View File

@ -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
Telegram User connects Clawdbot to a **personal Telegram account** using MTProto. 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 ## Requirements
@ -64,5 +64,5 @@ See [Pairing](/start/pairing) for details.
## Limitations ## Limitations
- DM-only (no groups or channels yet). - Broadcast channels are not supported.
- Calls are not supported. - Calls are not supported.

View File

@ -14,7 +14,7 @@
"detailLabel": "Telegram User", "detailLabel": "Telegram User",
"docsPath": "/channels/telegram-user", "docsPath": "/channels/telegram-user",
"docsLabel": "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, "order": 12,
"quickstartAllowFrom": true "quickstartAllowFrom": true
}, },

View File

@ -181,20 +181,25 @@ export const telegramUserPlugin: ChannelPlugin<ResolvedTelegramUserAccount> = {
getTelegramUserRuntime().channel.text.chunkMarkdownText(text, limit), getTelegramUserRuntime().channel.text.chunkMarkdownText(text, limit),
textChunkLimit: 4000, textChunkLimit: 4000,
pollMaxOptions: 10, pollMaxOptions: 10,
sendText: async ({ to, text, accountId }) => { sendText: async ({ to, text, accountId, threadId }) => {
const result = await sendMessageTelegramUser(to, text, { accountId: accountId ?? undefined }); const result = await sendMessageTelegramUser(to, text, {
return { channel: "telegram-user", ...result };
},
sendMedia: async ({ to, text, mediaUrl, accountId }) => {
const result = await sendMediaTelegramUser(to, text, {
accountId: accountId ?? undefined, accountId: accountId ?? undefined,
mediaUrl, threadId,
}); });
return { channel: "telegram-user", ...result }; 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, { const result = await sendPollTelegramUser(to, poll, {
accountId: accountId ?? undefined, accountId: accountId ?? undefined,
threadId,
}); });
return { channel: "telegram-user", ...result }; return { channel: "telegram-user", ...result };
}, },

View File

@ -31,6 +31,7 @@ const TelegramUserAccountSchema = z
apiHash: z.string().optional(), apiHash: z.string().optional(),
dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(), dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
allowFrom: z.array(allowFromEntry).optional(), allowFrom: z.array(allowFromEntry).optional(),
replyToMode: z.enum(["off", "first", "all"]).optional(),
textChunkLimit: z.number().int().positive().optional(), textChunkLimit: z.number().int().positive().optional(),
mediaMaxMb: z.number().positive().optional(), mediaMaxMb: z.number().positive().optional(),
groupAllowFrom: z.array(allowFromEntry).optional(), groupAllowFrom: z.array(allowFromEntry).optional(),

View File

@ -661,6 +661,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara
client, client,
accountId, accountId,
replyToId, replyToId,
threadId,
mediaUrl, mediaUrl,
maxBytes: mediaMaxMb * 1024 * 1024, maxBytes: mediaMaxMb * 1024 * 1024,
}); });
@ -685,6 +686,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara
client, client,
accountId, accountId,
replyToId, replyToId,
threadId,
}); });
} catch (err) { } catch (err) {
if (isDestroyedClientError(err)) return; if (isDestroyedClientError(err)) return;

View File

@ -29,17 +29,37 @@ export type TelegramUserSendOpts = {
client?: TelegramClient; client?: TelegramClient;
accountId?: string; accountId?: string;
replyToId?: number; replyToId?: number;
threadId?: string | number | null;
mediaUrl?: string; mediaUrl?: string;
}; };
const normalizeTarget = (raw: string): string => { function normalizeTarget(raw: string): string {
const trimmed = raw.trim(); const trimmed = raw.trim();
if (!trimmed) throw new Error("Recipient is required for Telegram User sends"); if (!trimmed) throw new Error("Recipient is required for Telegram User sends");
const withoutProvider = trimmed.replace(/^(telegram-user|telegram|tg):/i, "").trim(); const withoutProvider = trimmed.replace(/^(telegram-user|telegram|tg):/i, "").trim();
const withoutPrefix = withoutProvider.replace(/^(user|group|channel|chat):/i, "").trim(); const withoutPrefix = withoutProvider.replace(/^(user|group|channel|chat):/i, "").trim();
const topicSplit = withoutPrefix.split(/:topic:/i); if (!withoutPrefix) throw new Error("Recipient is required for Telegram User sends");
return (topicSplit[0] ?? withoutPrefix).trim(); 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 { export function normalizeTelegramUserMessagingTarget(raw: string): string {
return normalizeTarget(raw); return normalizeTarget(raw);
@ -50,6 +70,7 @@ export function looksLikeTelegramUserTargetId(value: string): boolean {
if (!trimmed) return false; if (!trimmed) return false;
if (/^telegram-user:/i.test(trimmed)) return true; if (/^telegram-user:/i.test(trimmed)) return true;
if (/^(user|group|channel|chat):/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); return /^-?\d+$/.test(trimmed) || /^@?[a-z0-9_]{5,}$/i.test(trimmed);
} }
@ -125,11 +146,13 @@ export async function sendMessageTelegramUser(
accountId: opts.accountId, accountId: opts.accountId,
}); });
try { try {
const target = resolveTelegramUserPeer(normalizeTarget(to)); const resolved = resolveTargetAndThread(to, opts.threadId);
const target = resolveTelegramUserPeer(resolved.target);
let message: Awaited<ReturnType<TelegramClient["sendText"]>> | null = null; let message: Awaited<ReturnType<TelegramClient["sendText"]>> | null = null;
try { try {
message = await client.sendText(target, text, { message = await client.sendText(target, text, {
...(opts.replyToId ? { replyTo: opts.replyToId } : {}), ...(opts.replyToId ? { replyTo: opts.replyToId } : {}),
...(resolved.threadId ? { threadId: resolved.threadId } : {}),
}); });
} catch (err) { } catch (err) {
if (!isDestroyedClientError(err)) throw err; if (!isDestroyedClientError(err)) throw err;
@ -157,7 +180,8 @@ export async function sendMediaTelegramUser(
accountId: opts.accountId, accountId: opts.accountId,
}); });
try { 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 media = await getTelegramUserRuntime().media.loadWebMedia(opts.mediaUrl, opts.maxBytes);
const input = InputMedia.auto(media.buffer, { const input = InputMedia.auto(media.buffer, {
fileName: media.fileName ?? undefined, fileName: media.fileName ?? undefined,
@ -168,6 +192,7 @@ export async function sendMediaTelegramUser(
try { try {
message = await client.sendMedia(target, input, { message = await client.sendMedia(target, input, {
...(opts.replyToId ? { replyTo: opts.replyToId } : {}), ...(opts.replyToId ? { replyTo: opts.replyToId } : {}),
...(resolved.threadId ? { threadId: resolved.threadId } : {}),
}); });
} catch (err) { } catch (err) {
if (!isDestroyedClientError(err)) throw err; if (!isDestroyedClientError(err)) throw err;
@ -195,7 +220,8 @@ export async function sendPollTelegramUser(
accountId: opts.accountId, accountId: opts.accountId,
}); });
try { try {
const target = resolveTelegramUserPeer(normalizeTarget(to)); const resolved = resolveTargetAndThread(to, opts.threadId);
const target = resolveTelegramUserPeer(resolved.target);
const normalized = normalizePollInput(poll); const normalized = normalizePollInput(poll);
const input = InputMedia.poll({ const input = InputMedia.poll({
question: normalized.question, question: normalized.question,
@ -206,6 +232,7 @@ export async function sendPollTelegramUser(
try { try {
message = await client.sendMedia(target, input, { message = await client.sendMedia(target, input, {
...(opts.replyToId ? { replyTo: opts.replyToId } : {}), ...(opts.replyToId ? { replyTo: opts.replyToId } : {}),
...(resolved.threadId ? { threadId: resolved.threadId } : {}),
}); });
} catch (err) { } catch (err) {
if (!isDestroyedClientError(err)) throw err; if (!isDestroyedClientError(err)) throw err;

View File

@ -30,6 +30,8 @@ export type TelegramUserAccountConfig = {
dmPolicy?: DmPolicy; dmPolicy?: DmPolicy;
/** Allowlist for DM senders (user ids or usernames, or "*"). */ /** Allowlist for DM senders (user ids or usernames, or "*"). */
allowFrom?: Array<string | number>; allowFrom?: Array<string | number>;
/** Control reply threading when reply tags are present (off|first|all). */
replyToMode?: "off" | "first" | "all";
/** Outbound text chunk size (chars). Default: 4000. */ /** Outbound text chunk size (chars). Default: 4000. */
textChunkLimit?: number; textChunkLimit?: number;
/** Max outbound media size in MB. */ /** Max outbound media size in MB. */