Telegram user: preserve threads and update docs
This commit is contained in:
parent
39d9eb4589
commit
63929bd70c
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
},
|
||||
|
||||
@ -181,20 +181,25 @@ export const telegramUserPlugin: ChannelPlugin<ResolvedTelegramUserAccount> = {
|
||||
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 };
|
||||
},
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<ReturnType<TelegramClient["sendText"]>> | 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;
|
||||
|
||||
@ -30,6 +30,8 @@ export type TelegramUserAccountConfig = {
|
||||
dmPolicy?: DmPolicy;
|
||||
/** Allowlist for DM senders (user ids or usernames, or "*"). */
|
||||
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. */
|
||||
textChunkLimit?: number;
|
||||
/** Max outbound media size in MB. */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user