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.
|
- [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.
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 };
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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. */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user