fix(telegram): support forum topics
Co-authored-by: Daniel Griesser <HazAT@users.noreply.github.com> Co-authored-by: Nacho Iacovino <nachoiacovino@users.noreply.github.com> Co-authored-by: Randy Ventures <RandyVentures@users.noreply.github.com>
This commit is contained in:
@@ -33,6 +33,7 @@
|
|||||||
- Gmail: include tailscale command exit codes/output when hook setup fails (easier debugging).
|
- Gmail: include tailscale command exit codes/output when hook setup fails (easier debugging).
|
||||||
- Telegram: stop typing after tool results. Thanks @AbhisekBasu1 for PR #322.
|
- Telegram: stop typing after tool results. Thanks @AbhisekBasu1 for PR #322.
|
||||||
- Telegram: include sender identity in group envelope headers. (#336)
|
- Telegram: include sender identity in group envelope headers. (#336)
|
||||||
|
- Telegram: support forum topics with topic-isolated sessions and message_thread_id routing. Thanks @HazAT, @nachoiacovino, @RandyVentures for PR #321/#333/#334.
|
||||||
- Messages: stop defaulting ack reactions to 👀 when identity emoji is missing.
|
- Messages: stop defaulting ack reactions to 👀 when identity emoji is missing.
|
||||||
- Auto-reply: require slash for control commands to avoid false triggers in normal text.
|
- Auto-reply: require slash for control commands to avoid false triggers in normal text.
|
||||||
- Auto-reply: flag error payloads and improve Bun socket error messaging. Thanks @emanuelst for PR #331.
|
- Auto-reply: flag error payloads and improve Bun socket error messaging. Thanks @emanuelst for PR #331.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Clawdbot treats group chats consistently across surfaces: WhatsApp, Telegram, Di
|
|||||||
|
|
||||||
## Session keys
|
## Session keys
|
||||||
- Group sessions use `agent:<agentId>:<provider>:group:<id>` session keys (rooms/channels use `agent:<agentId>:<provider>:channel:<id>`).
|
- Group sessions use `agent:<agentId>:<provider>:group:<id>` session keys (rooms/channels use `agent:<agentId>:<provider>:channel:<id>`).
|
||||||
|
- Telegram forum topics add `:topic:<threadId>` to the group id so each topic has its own session.
|
||||||
- Direct chats use the main session (or per-sender if configured).
|
- Direct chats use the main session (or per-sender if configured).
|
||||||
- Heartbeats are skipped for group sessions.
|
- Heartbeats are skipped for group sessions.
|
||||||
|
|
||||||
@@ -118,6 +119,7 @@ Group inbound payloads set:
|
|||||||
- `GroupSubject` (if known)
|
- `GroupSubject` (if known)
|
||||||
- `GroupMembers` (if known)
|
- `GroupMembers` (if known)
|
||||||
- `WasMentioned` (mention gating result)
|
- `WasMentioned` (mention gating result)
|
||||||
|
- Telegram forum topics also include `MessageThreadId` and `IsForum`.
|
||||||
|
|
||||||
The agent system prompt includes a group intro on the first turn of a new group session.
|
The agent system prompt includes a group intro on the first turn of a new group session.
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Goal: deterministic replies per provider, while supporting multi-agent + multi-a
|
|||||||
- **Canonical direct session (per agent):** direct chats collapse to `agent:<agentId>:<mainKey>` (default `main`). Groups/channels stay isolated per agent:
|
- **Canonical direct session (per agent):** direct chats collapse to `agent:<agentId>:<mainKey>` (default `main`). Groups/channels stay isolated per agent:
|
||||||
- group: `agent:<agentId>:<provider>:group:<id>`
|
- group: `agent:<agentId>:<provider>:group:<id>`
|
||||||
- channel/room: `agent:<agentId>:<provider>:channel:<id>`
|
- channel/room: `agent:<agentId>:<provider>:channel:<id>`
|
||||||
|
- Telegram forum topics: `agent:<agentId>:telegram:group:<chatId>:topic:<threadId>`
|
||||||
- **Session store:** per-agent store lives under `~/.clawdbot/agents/<agentId>/sessions/sessions.json` (override via `session.store` with `{agentId}` templating). JSONL transcripts live next to it.
|
- **Session store:** per-agent store lives under `~/.clawdbot/agents/<agentId>/sessions/sessions.json` (override via `session.store` with `{agentId}` templating). JSONL transcripts live next to it.
|
||||||
- **WebChat:** attaches to the selected agent’s main session (so desktop reflects cross-provider history for that agent).
|
- **WebChat:** attaches to the selected agent’s main session (so desktop reflects cross-provider history for that agent).
|
||||||
- **Implementation hints:**
|
- **Implementation hints:**
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ All session state is **owned by the gateway** (the “master” Clawdbot). UI cl
|
|||||||
- Direct chats collapse to the per-agent primary key: `agent:<agentId>:<mainKey>`.
|
- Direct chats collapse to the per-agent primary key: `agent:<agentId>:<mainKey>`.
|
||||||
- Multiple phone numbers and providers can map to the same agent main key; they act as transports into one conversation.
|
- Multiple phone numbers and providers can map to the same agent main key; they act as transports into one conversation.
|
||||||
- Group chats isolate state: `agent:<agentId>:<provider>:group:<id>` (rooms/channels use `agent:<agentId>:<provider>:channel:<id>`).
|
- Group chats isolate state: `agent:<agentId>:<provider>:group:<id>` (rooms/channels use `agent:<agentId>:<provider>:channel:<id>`).
|
||||||
|
- Telegram forum topics append `:topic:<threadId>` to the group id for isolation.
|
||||||
- Legacy `group:<id>` keys are still recognized for migration.
|
- Legacy `group:<id>` keys are still recognized for migration.
|
||||||
- Other sources:
|
- Other sources:
|
||||||
- Cron jobs: `cron:<job.id>`
|
- Cron jobs: `cron:<job.id>`
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ Status: production-ready for bot DMs + groups via grammY. Long-polling by defaul
|
|||||||
- Group replies require a mention by default (native @mention or `routing.groupChat.mentionPatterns`).
|
- Group replies require a mention by default (native @mention or `routing.groupChat.mentionPatterns`).
|
||||||
- Replies always route back to the same Telegram chat.
|
- Replies always route back to the same Telegram chat.
|
||||||
|
|
||||||
|
## Topics (forum supergroups)
|
||||||
|
Telegram forum topics include a `message_thread_id` per message. Clawdbot:
|
||||||
|
- Appends `:topic:<threadId>` to the Telegram group session key so each topic is isolated.
|
||||||
|
- Sends typing indicators and replies with `message_thread_id` so responses stay in the topic.
|
||||||
|
- Exposes `MessageThreadId` + `IsForum` in template context for routing/templating.
|
||||||
|
|
||||||
## Access control (DMs + groups)
|
## Access control (DMs + groups)
|
||||||
- Default: `telegram.dmPolicy = "pairing"`. Unknown senders receive a pairing code; messages are ignored until approved.
|
- Default: `telegram.dmPolicy = "pairing"`. Unknown senders receive a pairing code; messages are ignored until approved.
|
||||||
- Approve via:
|
- Approve via:
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ export type MsgContext = {
|
|||||||
CommandAuthorized?: boolean;
|
CommandAuthorized?: boolean;
|
||||||
CommandSource?: "text" | "native";
|
CommandSource?: "text" | "native";
|
||||||
CommandTargetSessionKey?: string;
|
CommandTargetSessionKey?: string;
|
||||||
|
/** Telegram forum topic thread ID. */
|
||||||
|
MessageThreadId?: number;
|
||||||
|
/** Telegram forum supergroup marker. */
|
||||||
|
IsForum?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TemplateContext = MsgContext & {
|
export type TemplateContext = MsgContext & {
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ describe("createTelegramBot", () => {
|
|||||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(sendChatActionSpy).toHaveBeenCalledWith(42, "typing");
|
expect(sendChatActionSpy).toHaveBeenCalledWith(42, "typing", undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts group messages when mentionPatterns match (without @botUsername)", async () => {
|
it("accepts group messages when mentionPatterns match (without @botUsername)", async () => {
|
||||||
@@ -1315,4 +1315,95 @@ describe("createTelegramBot", () => {
|
|||||||
|
|
||||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("isolates forum topic sessions and carries thread metadata", async () => {
|
||||||
|
onSpy.mockReset();
|
||||||
|
sendChatActionSpy.mockReset();
|
||||||
|
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||||
|
typeof vi.fn
|
||||||
|
>;
|
||||||
|
replySpy.mockReset();
|
||||||
|
|
||||||
|
loadConfig.mockReturnValue({
|
||||||
|
telegram: { groups: { "*": { requireMention: false } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
createTelegramBot({ token: "tok" });
|
||||||
|
const handler = onSpy.mock.calls[0][1] as (
|
||||||
|
ctx: Record<string, unknown>,
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
await handler({
|
||||||
|
message: {
|
||||||
|
chat: {
|
||||||
|
id: -1001234567890,
|
||||||
|
type: "supergroup",
|
||||||
|
title: "Forum Group",
|
||||||
|
is_forum: true,
|
||||||
|
},
|
||||||
|
from: { id: 12345, username: "testuser" },
|
||||||
|
text: "hello",
|
||||||
|
date: 1736380800,
|
||||||
|
message_id: 42,
|
||||||
|
message_thread_id: 99,
|
||||||
|
},
|
||||||
|
me: { username: "clawdbot_bot" },
|
||||||
|
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
|
const payload = replySpy.mock.calls[0][0];
|
||||||
|
expect(payload.SessionKey).toContain(
|
||||||
|
"telegram:group:-1001234567890:topic:99",
|
||||||
|
);
|
||||||
|
expect(payload.From).toBe("group:-1001234567890:topic:99");
|
||||||
|
expect(payload.MessageThreadId).toBe(99);
|
||||||
|
expect(payload.IsForum).toBe(true);
|
||||||
|
expect(sendChatActionSpy).toHaveBeenCalledWith(-1001234567890, "typing", {
|
||||||
|
message_thread_id: 99,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes message_thread_id to topic replies", async () => {
|
||||||
|
onSpy.mockReset();
|
||||||
|
sendMessageSpy.mockReset();
|
||||||
|
const replySpy = replyModule.__replySpy as unknown as ReturnType<
|
||||||
|
typeof vi.fn
|
||||||
|
>;
|
||||||
|
replySpy.mockReset();
|
||||||
|
replySpy.mockResolvedValue({ text: "response" });
|
||||||
|
|
||||||
|
loadConfig.mockReturnValue({
|
||||||
|
telegram: { groups: { "*": { requireMention: false } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
createTelegramBot({ token: "tok" });
|
||||||
|
const handler = onSpy.mock.calls[0][1] as (
|
||||||
|
ctx: Record<string, unknown>,
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
await handler({
|
||||||
|
message: {
|
||||||
|
chat: {
|
||||||
|
id: -1001234567890,
|
||||||
|
type: "supergroup",
|
||||||
|
title: "Forum Group",
|
||||||
|
is_forum: true,
|
||||||
|
},
|
||||||
|
from: { id: 12345, username: "testuser" },
|
||||||
|
text: "hello",
|
||||||
|
date: 1736380800,
|
||||||
|
message_id: 42,
|
||||||
|
message_thread_id: 99,
|
||||||
|
},
|
||||||
|
me: { username: "clawdbot_bot" },
|
||||||
|
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendMessageSpy).toHaveBeenCalledWith(
|
||||||
|
"-1001234567890",
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({ message_thread_id: 99 }),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -195,6 +195,9 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const msg = primaryCtx.message;
|
const msg = primaryCtx.message;
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
||||||
|
const messageThreadId = (msg as { message_thread_id?: number })
|
||||||
|
.message_thread_id;
|
||||||
|
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
|
||||||
const effectiveDmAllow = normalizeAllowFrom([
|
const effectiveDmAllow = normalizeAllowFrom([
|
||||||
...(allowFrom ?? []),
|
...(allowFrom ?? []),
|
||||||
...storeAllowFrom,
|
...storeAllowFrom,
|
||||||
@@ -206,7 +209,13 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
|
|
||||||
const sendTyping = async () => {
|
const sendTyping = async () => {
|
||||||
try {
|
try {
|
||||||
await bot.api.sendChatAction(chatId, "typing");
|
await bot.api.sendChatAction(
|
||||||
|
chatId,
|
||||||
|
"typing",
|
||||||
|
messageThreadId != null
|
||||||
|
? { message_thread_id: messageThreadId }
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`telegram typing cue failed for chat ${chatId}: ${String(err)}`,
|
`telegram typing cue failed for chat ${chatId}: ${String(err)}`,
|
||||||
@@ -375,7 +384,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const body = formatAgentEnvelope({
|
const body = formatAgentEnvelope({
|
||||||
provider: "Telegram",
|
provider: "Telegram",
|
||||||
from: isGroup
|
from: isGroup
|
||||||
? buildGroupFromLabel(msg, chatId, senderId)
|
? buildGroupFromLabel(msg, chatId, senderId, messageThreadId)
|
||||||
: buildSenderLabel(msg, senderId || chatId),
|
: buildSenderLabel(msg, senderId || chatId),
|
||||||
timestamp: msg.date ? msg.date * 1000 : undefined,
|
timestamp: msg.date ? msg.date * 1000 : undefined,
|
||||||
body: `${bodyText}${replySuffix}`,
|
body: `${bodyText}${replySuffix}`,
|
||||||
@@ -386,12 +395,20 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
provider: "telegram",
|
provider: "telegram",
|
||||||
peer: {
|
peer: {
|
||||||
kind: isGroup ? "group" : "dm",
|
kind: isGroup ? "group" : "dm",
|
||||||
id: String(chatId),
|
id: isGroup
|
||||||
|
? messageThreadId != null
|
||||||
|
? `${chatId}:topic:${messageThreadId}`
|
||||||
|
: String(chatId)
|
||||||
|
: String(chatId),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const ctxPayload = {
|
const ctxPayload = {
|
||||||
Body: body,
|
Body: body,
|
||||||
From: isGroup ? `group:${chatId}` : `telegram:${chatId}`,
|
From: isGroup
|
||||||
|
? messageThreadId != null
|
||||||
|
? `group:${chatId}:topic:${messageThreadId}`
|
||||||
|
: `group:${chatId}`
|
||||||
|
: `telegram:${chatId}`,
|
||||||
To: `telegram:${chatId}`,
|
To: `telegram:${chatId}`,
|
||||||
SessionKey: route.sessionKey,
|
SessionKey: route.sessionKey,
|
||||||
AccountId: route.accountId,
|
AccountId: route.accountId,
|
||||||
@@ -418,6 +435,8 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
: undefined,
|
: undefined,
|
||||||
...(locationData ? toLocationContext(locationData) : undefined),
|
...(locationData ? toLocationContext(locationData) : undefined),
|
||||||
CommandAuthorized: commandAuthorized,
|
CommandAuthorized: commandAuthorized,
|
||||||
|
MessageThreadId: messageThreadId,
|
||||||
|
IsForum: isForum,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (replyTarget && shouldLogVerbose()) {
|
if (replyTarget && shouldLogVerbose()) {
|
||||||
@@ -445,8 +464,10 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const preview = body.slice(0, 200).replace(/\n/g, "\\n");
|
const preview = body.slice(0, 200).replace(/\n/g, "\\n");
|
||||||
const mediaInfo =
|
const mediaInfo =
|
||||||
allMedia.length > 1 ? ` mediaCount=${allMedia.length}` : "";
|
allMedia.length > 1 ? ` mediaCount=${allMedia.length}` : "";
|
||||||
|
const topicInfo =
|
||||||
|
messageThreadId != null ? ` topic=${messageThreadId}` : "";
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`telegram inbound: chatId=${chatId} from=${ctxPayload.From} len=${body.length}${mediaInfo} preview="${preview}"`,
|
`telegram inbound: chatId=${chatId} from=${ctxPayload.From} len=${body.length}${mediaInfo}${topicInfo} preview="${preview}"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,6 +483,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
bot,
|
bot,
|
||||||
replyToMode,
|
replyToMode,
|
||||||
textLimit,
|
textLimit,
|
||||||
|
messageThreadId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (err, info) => {
|
onError: (err, info) => {
|
||||||
@@ -504,6 +526,9 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
const isGroup =
|
const isGroup =
|
||||||
msg.chat.type === "group" || msg.chat.type === "supergroup";
|
msg.chat.type === "group" || msg.chat.type === "supergroup";
|
||||||
|
const messageThreadId = (msg as { message_thread_id?: number })
|
||||||
|
.message_thread_id;
|
||||||
|
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
|
||||||
|
|
||||||
if (isGroup && useAccessGroups) {
|
if (isGroup && useAccessGroups) {
|
||||||
const groupPolicy = cfg.telegram?.groupPolicy ?? "open";
|
const groupPolicy = cfg.telegram?.groupPolicy ?? "open";
|
||||||
@@ -575,12 +600,20 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
provider: "telegram",
|
provider: "telegram",
|
||||||
peer: {
|
peer: {
|
||||||
kind: isGroup ? "group" : "dm",
|
kind: isGroup ? "group" : "dm",
|
||||||
id: String(chatId),
|
id: isGroup
|
||||||
|
? messageThreadId != null
|
||||||
|
? `${chatId}:topic:${messageThreadId}`
|
||||||
|
: String(chatId)
|
||||||
|
: String(chatId),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const ctxPayload = {
|
const ctxPayload = {
|
||||||
Body: prompt,
|
Body: prompt,
|
||||||
From: isGroup ? `group:${chatId}` : `telegram:${chatId}`,
|
From: isGroup
|
||||||
|
? messageThreadId != null
|
||||||
|
? `group:${chatId}:topic:${messageThreadId}`
|
||||||
|
: `group:${chatId}`
|
||||||
|
: `telegram:${chatId}`,
|
||||||
To: `slash:${senderId || chatId}`,
|
To: `slash:${senderId || chatId}`,
|
||||||
ChatType: isGroup ? "group" : "direct",
|
ChatType: isGroup ? "group" : "direct",
|
||||||
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
|
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
|
||||||
@@ -595,6 +628,8 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
CommandSource: "native" as const,
|
CommandSource: "native" as const,
|
||||||
SessionKey: `telegram:slash:${senderId || chatId}`,
|
SessionKey: `telegram:slash:${senderId || chatId}`,
|
||||||
CommandTargetSessionKey: route.sessionKey,
|
CommandTargetSessionKey: route.sessionKey,
|
||||||
|
MessageThreadId: messageThreadId,
|
||||||
|
IsForum: isForum,
|
||||||
};
|
};
|
||||||
|
|
||||||
const replyResult = await getReplyFromConfig(
|
const replyResult = await getReplyFromConfig(
|
||||||
@@ -615,6 +650,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
bot,
|
bot,
|
||||||
replyToMode,
|
replyToMode,
|
||||||
textLimit,
|
textLimit,
|
||||||
|
messageThreadId,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -793,8 +829,17 @@ async function deliverReplies(params: {
|
|||||||
bot: Bot;
|
bot: Bot;
|
||||||
replyToMode: ReplyToMode;
|
replyToMode: ReplyToMode;
|
||||||
textLimit: number;
|
textLimit: number;
|
||||||
|
messageThreadId?: number;
|
||||||
}) {
|
}) {
|
||||||
const { replies, chatId, runtime, bot, replyToMode, textLimit } = params;
|
const {
|
||||||
|
replies,
|
||||||
|
chatId,
|
||||||
|
runtime,
|
||||||
|
bot,
|
||||||
|
replyToMode,
|
||||||
|
textLimit,
|
||||||
|
messageThreadId,
|
||||||
|
} = params;
|
||||||
let hasReplied = false;
|
let hasReplied = false;
|
||||||
for (const reply of replies) {
|
for (const reply of replies) {
|
||||||
if (!reply?.text && !reply?.mediaUrl && !(reply?.mediaUrls?.length ?? 0)) {
|
if (!reply?.text && !reply?.mediaUrl && !(reply?.mediaUrls?.length ?? 0)) {
|
||||||
@@ -817,6 +862,7 @@ async function deliverReplies(params: {
|
|||||||
replyToId && (replyToMode === "all" || !hasReplied)
|
replyToId && (replyToMode === "all" || !hasReplied)
|
||||||
? replyToId
|
? replyToId
|
||||||
: undefined,
|
: undefined,
|
||||||
|
messageThreadId,
|
||||||
});
|
});
|
||||||
if (replyToId && !hasReplied) {
|
if (replyToId && !hasReplied) {
|
||||||
hasReplied = true;
|
hasReplied = true;
|
||||||
@@ -841,30 +887,37 @@ async function deliverReplies(params: {
|
|||||||
replyToId && (replyToMode === "all" || !hasReplied)
|
replyToId && (replyToMode === "all" || !hasReplied)
|
||||||
? replyToId
|
? replyToId
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const threadParams =
|
||||||
|
messageThreadId != null ? { message_thread_id: messageThreadId } : {};
|
||||||
if (isGif) {
|
if (isGif) {
|
||||||
await bot.api.sendAnimation(chatId, file, {
|
await bot.api.sendAnimation(chatId, file, {
|
||||||
caption,
|
caption,
|
||||||
reply_to_message_id: replyToMessageId,
|
reply_to_message_id: replyToMessageId,
|
||||||
|
...threadParams,
|
||||||
});
|
});
|
||||||
} else if (kind === "image") {
|
} else if (kind === "image") {
|
||||||
await bot.api.sendPhoto(chatId, file, {
|
await bot.api.sendPhoto(chatId, file, {
|
||||||
caption,
|
caption,
|
||||||
reply_to_message_id: replyToMessageId,
|
reply_to_message_id: replyToMessageId,
|
||||||
|
...threadParams,
|
||||||
});
|
});
|
||||||
} else if (kind === "video") {
|
} else if (kind === "video") {
|
||||||
await bot.api.sendVideo(chatId, file, {
|
await bot.api.sendVideo(chatId, file, {
|
||||||
caption,
|
caption,
|
||||||
reply_to_message_id: replyToMessageId,
|
reply_to_message_id: replyToMessageId,
|
||||||
|
...threadParams,
|
||||||
});
|
});
|
||||||
} else if (kind === "audio") {
|
} else if (kind === "audio") {
|
||||||
await bot.api.sendAudio(chatId, file, {
|
await bot.api.sendAudio(chatId, file, {
|
||||||
caption,
|
caption,
|
||||||
reply_to_message_id: replyToMessageId,
|
reply_to_message_id: replyToMessageId,
|
||||||
|
...threadParams,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await bot.api.sendDocument(chatId, file, {
|
await bot.api.sendDocument(chatId, file, {
|
||||||
caption,
|
caption,
|
||||||
reply_to_message_id: replyToMessageId,
|
reply_to_message_id: replyToMessageId,
|
||||||
|
...threadParams,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (replyToId && !hasReplied) {
|
if (replyToId && !hasReplied) {
|
||||||
@@ -903,18 +956,25 @@ function buildSenderLabel(msg: TelegramMessage, senderId?: number | string) {
|
|||||||
return idPart ?? "id:unknown";
|
return idPart ?? "id:unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildGroupLabel(msg: TelegramMessage, chatId: number | string) {
|
function buildGroupLabel(
|
||||||
|
msg: TelegramMessage,
|
||||||
|
chatId: number | string,
|
||||||
|
messageThreadId?: number,
|
||||||
|
) {
|
||||||
const title = msg.chat?.title;
|
const title = msg.chat?.title;
|
||||||
if (title) return `${title} id:${chatId}`;
|
const topicSuffix =
|
||||||
return `group:${chatId}`;
|
messageThreadId != null ? ` topic:${messageThreadId}` : "";
|
||||||
|
if (title) return `${title} id:${chatId}${topicSuffix}`;
|
||||||
|
return `group:${chatId}${topicSuffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildGroupFromLabel(
|
function buildGroupFromLabel(
|
||||||
msg: TelegramMessage,
|
msg: TelegramMessage,
|
||||||
chatId: number | string,
|
chatId: number | string,
|
||||||
senderId?: number | string,
|
senderId?: number | string,
|
||||||
|
messageThreadId?: number,
|
||||||
) {
|
) {
|
||||||
const groupLabel = buildGroupLabel(msg, chatId);
|
const groupLabel = buildGroupLabel(msg, chatId, messageThreadId);
|
||||||
const senderLabel = buildSenderLabel(msg, senderId);
|
const senderLabel = buildSenderLabel(msg, senderId);
|
||||||
return `${groupLabel} from ${senderLabel}`;
|
return `${groupLabel} from ${senderLabel}`;
|
||||||
}
|
}
|
||||||
@@ -989,12 +1049,13 @@ async function sendTelegramText(
|
|||||||
chatId: string,
|
chatId: string,
|
||||||
text: string,
|
text: string,
|
||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
opts?: { replyToMessageId?: number },
|
opts?: { replyToMessageId?: number; messageThreadId?: number },
|
||||||
): Promise<number | undefined> {
|
): Promise<number | undefined> {
|
||||||
try {
|
try {
|
||||||
const res = await bot.api.sendMessage(chatId, text, {
|
const res = await bot.api.sendMessage(chatId, text, {
|
||||||
parse_mode: "Markdown",
|
parse_mode: "Markdown",
|
||||||
reply_to_message_id: opts?.replyToMessageId,
|
reply_to_message_id: opts?.replyToMessageId,
|
||||||
|
message_thread_id: opts?.messageThreadId,
|
||||||
});
|
});
|
||||||
return res.message_id;
|
return res.message_id;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1005,6 +1066,7 @@ async function sendTelegramText(
|
|||||||
);
|
);
|
||||||
const res = await bot.api.sendMessage(chatId, text, {
|
const res = await bot.api.sendMessage(chatId, text, {
|
||||||
reply_to_message_id: opts?.replyToMessageId,
|
reply_to_message_id: opts?.replyToMessageId,
|
||||||
|
message_thread_id: opts?.messageThreadId,
|
||||||
});
|
});
|
||||||
return res.message_id;
|
return res.message_id;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user