Compare commits

...

2 Commits

Author SHA1 Message Date
Peter Steinberger
c457d8d70c fix: handle Telegram General topic thread params (#848) (thanks @azade-c) 2026-01-16 00:07:45 +00:00
Azade
69fe974d80 fix(telegram): separate thread params for typing vs messages
Telegram General topic (id=1) has inconsistent API behavior:
- sendMessage: rejects explicit message_thread_id=1
- sendChatAction: requires message_thread_id=1 for typing to show

Split into two helper functions:
- buildTelegramThreadParams: excludes General topic for messages
- buildTypingThreadParams: includes General topic for typing
2026-01-16 00:01:39 +00:00
8 changed files with 67 additions and 17 deletions

View File

@ -18,6 +18,7 @@
- Browser: extension mode recovers when only one tab is attached (stale targetId fallback). - Browser: extension mode recovers when only one tab is attached (stale targetId fallback).
- Browser: fix `tab not found` for extension relay snapshots/actions when Playwright blocks `newCDPSession` (use the single available Page). - Browser: fix `tab not found` for extension relay snapshots/actions when Playwright blocks `newCDPSession` (use the single available Page).
- Telegram: add bidirectional reaction support with configurable notifications and agent guidance. (#964) — thanks @bohdanpodvirnyi. - Telegram: add bidirectional reaction support with configurable notifications and agent guidance. (#964) — thanks @bohdanpodvirnyi.
- Telegram: skip `message_thread_id=1` for General topic sends while keeping typing indicators. (#848) — thanks @azade-c.
- Discord: allow allowlisted guilds without channel lists to receive messages when `groupPolicy="allowlist"`. — thanks @thewilloftheshadow. - Discord: allow allowlisted guilds without channel lists to receive messages when `groupPolicy="allowlist"`. — thanks @thewilloftheshadow.
## 2026.1.14-1 ## 2026.1.14-1

View File

@ -188,6 +188,7 @@ Disable with:
Telegram forum topics include a `message_thread_id` per message. Clawdbot: 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. - 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. - Sends typing indicators and replies with `message_thread_id` so responses stay in the topic.
- General topic (thread id `1`) is special: message sends omit `message_thread_id` (Telegram rejects it), but typing indicators still include it.
- Exposes `MessageThreadId` + `IsForum` in template context for routing/templating. - Exposes `MessageThreadId` + `IsForum` in template context for routing/templating.
- Topic-specific configuration is available under `channels.telegram.groups.<chatId>.topics.<threadId>` (skills, allowlists, auto-reply, system prompts, disable). - Topic-specific configuration is available under `channels.telegram.groups.<chatId>.topics.<threadId>` (skills, allowlists, auto-reply, system prompts, disable).

View File

@ -17,7 +17,7 @@ import {
buildSenderName, buildSenderName,
buildTelegramGroupFrom, buildTelegramGroupFrom,
buildTelegramGroupPeerId, buildTelegramGroupPeerId,
buildTelegramThreadParams, buildTypingThreadParams,
describeReplyTarget, describeReplyTarget,
extractTelegramLocation, extractTelegramLocation,
hasBotMention, hasBotMention,
@ -92,7 +92,7 @@ export const buildTelegramMessageContext = async ({
const sendTyping = async () => { const sendTyping = async () => {
try { try {
await bot.api.sendChatAction(chatId, "typing", buildTelegramThreadParams(resolvedThreadId)); await bot.api.sendChatAction(chatId, "typing", buildTypingThreadParams(resolvedThreadId));
} catch (err) { } catch (err) {
logVerbose(`telegram typing cue failed for chat ${chatId}: ${String(err)}`); logVerbose(`telegram typing cue failed for chat ${chatId}: ${String(err)}`);
} }

View File

@ -352,10 +352,8 @@ describe("createTelegramBot", () => {
getFile: async () => ({ download: async () => new Uint8Array() }), getFile: async () => ({ download: async () => new Uint8Array() }),
}); });
expect(sendMessageSpy).toHaveBeenCalledWith( expect(sendMessageSpy).toHaveBeenCalledTimes(1);
"-1001234567890", const sendParams = sendMessageSpy.mock.calls[0]?.[2] as { message_thread_id?: number };
expect.any(String), expect(sendParams?.message_thread_id).toBeUndefined();
expect.objectContaining({ message_thread_id: 1 }),
);
}); });
}); });

View File

@ -1710,11 +1710,9 @@ describe("createTelegramBot", () => {
getFile: async () => ({ download: async () => new Uint8Array() }), getFile: async () => ({ download: async () => new Uint8Array() }),
}); });
expect(sendMessageSpy).toHaveBeenCalledWith( expect(sendMessageSpy).toHaveBeenCalledTimes(1);
"-1001234567890", const sendParams = sendMessageSpy.mock.calls[0]?.[2] as { message_thread_id?: number };
expect.any(String), expect(sendParams?.message_thread_id).toBeUndefined();
expect.objectContaining({ message_thread_id: 1 }),
);
}); });
it("applies topic skill filters and system prompts", async () => { it("applies topic skill filters and system prompts", async () => {

View File

@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { buildTelegramThreadParams, buildTypingThreadParams } from "./helpers.js";
describe("buildTelegramThreadParams", () => {
it("omits General topic thread id for message sends", () => {
expect(buildTelegramThreadParams(1)).toBeUndefined();
});
it("includes non-General topic thread ids", () => {
expect(buildTelegramThreadParams(99)).toEqual({ message_thread_id: 99 });
});
it("normalizes thread ids to integers", () => {
expect(buildTelegramThreadParams(42.9)).toEqual({ message_thread_id: 42 });
});
});
describe("buildTypingThreadParams", () => {
it("returns undefined when no thread id is provided", () => {
expect(buildTypingThreadParams(undefined)).toBeUndefined();
});
it("includes General topic thread id for typing indicators", () => {
expect(buildTypingThreadParams(1)).toEqual({ message_thread_id: 1 });
});
it("normalizes thread ids to integers", () => {
expect(buildTypingThreadParams(42.9)).toEqual({ message_thread_id: 42 });
});
});

View File

@ -19,8 +19,31 @@ export function resolveTelegramForumThreadId(params: {
return params.messageThreadId ?? undefined; return params.messageThreadId ?? undefined;
} }
/**
* Build thread params for Telegram API calls (messages, media).
* General forum topic (id=1) must be treated like a regular supergroup send:
* Telegram rejects sendMessage/sendMedia with message_thread_id=1 ("thread not found").
*/
export function buildTelegramThreadParams(messageThreadId?: number) { export function buildTelegramThreadParams(messageThreadId?: number) {
return messageThreadId != null ? { message_thread_id: messageThreadId } : undefined; if (messageThreadId == null) {
return undefined;
}
const normalized = Math.trunc(messageThreadId);
if (normalized === TELEGRAM_GENERAL_TOPIC_ID) {
return undefined;
}
return { message_thread_id: normalized };
}
/**
* Build thread params for typing indicators (sendChatAction).
* Empirically, General topic (id=1) needs message_thread_id for typing to appear.
*/
export function buildTypingThreadParams(messageThreadId?: number) {
if (messageThreadId == null) {
return undefined;
}
return { message_thread_id: Math.trunc(messageThreadId) };
} }
export function resolveTelegramStreamMode( export function resolveTelegramStreamMode(

View File

@ -20,6 +20,7 @@ import { markdownToTelegramHtml } from "./format.js";
import { recordSentMessage } from "./sent-message-cache.js"; import { recordSentMessage } from "./sent-message-cache.js";
import { parseTelegramTarget, stripTelegramInternalPrefixes } from "./targets.js"; import { parseTelegramTarget, stripTelegramInternalPrefixes } from "./targets.js";
import { resolveTelegramVoiceSend } from "./voice.js"; import { resolveTelegramVoiceSend } from "./voice.js";
import { buildTelegramThreadParams } from "./bot/helpers.js";
type TelegramSendOpts = { type TelegramSendOpts = {
token?: string; token?: string;
@ -166,12 +167,10 @@ export async function sendMessageTelegram(
// Build optional params for forum topics and reply threading. // Build optional params for forum topics and reply threading.
// Only include these if actually provided to keep API calls clean. // Only include these if actually provided to keep API calls clean.
const threadParams: Record<string, number> = {};
const messageThreadId = const messageThreadId =
opts.messageThreadId != null ? opts.messageThreadId : target.messageThreadId; opts.messageThreadId != null ? opts.messageThreadId : target.messageThreadId;
if (messageThreadId != null) { const threadIdParams = buildTelegramThreadParams(messageThreadId);
threadParams.message_thread_id = Math.trunc(messageThreadId); const threadParams: Record<string, number> = threadIdParams ? { ...threadIdParams } : {};
}
if (opts.replyToMessageId != null) { if (opts.replyToMessageId != null) {
threadParams.reply_to_message_id = Math.trunc(opts.replyToMessageId); threadParams.reply_to_message_id = Math.trunc(opts.replyToMessageId);
} }