fix: preserve telegram dm topic thread ids

This commit is contained in:
Ayaan Zaidi
2026-02-17 13:03:20 +05:30
committed by Ayaan Zaidi
parent f17b42d2f8
commit 9d9630c83a
2 changed files with 72 additions and 25 deletions

View File

@@ -198,17 +198,6 @@ function isTelegramMessageNotModifiedError(err: unknown): boolean {
return MESSAGE_NOT_MODIFIED_RE.test(formatErrorMessage(err));
}
/**
* Telegram private chats have positive numeric IDs.
* Groups and supergroups have negative IDs (typically -100… for supergroups).
* Private chats never support forum topics, so `message_thread_id` must
* not be included in API calls targeting them (#17242).
*/
function isTelegramPrivateChat(chatId: string): boolean {
const n = Number(chatId);
return Number.isFinite(n) && n > 0;
}
function hasMessageThreadIdParam(params?: Record<string, unknown>): boolean {
if (!params) {
return false;
@@ -241,13 +230,18 @@ function isTelegramHtmlParseError(err: unknown): boolean {
function buildTelegramThreadReplyParams(params: {
targetMessageThreadId?: number;
messageThreadId?: number;
chatType?: "direct" | "group" | "unknown";
replyToMessageId?: number;
quoteText?: string;
}): Record<string, unknown> {
const messageThreadId =
params.messageThreadId != null ? params.messageThreadId : params.targetMessageThreadId;
const threadScope = params.chatType === "direct" ? ("dm" as const) : ("forum" as const);
// Never blanket-strip DM message_thread_id by chat-id sign.
// Telegram supports DM topics; stripping silently misroutes topic replies.
// Keep thread id and rely on thread-not-found retry fallback for plain DMs.
const threadSpec =
messageThreadId != null ? { id: messageThreadId, scope: "forum" as const } : undefined;
messageThreadId != null ? { id: messageThreadId, scope: threadScope } : undefined;
const threadIdParams = buildTelegramThreadParams(threadSpec);
const threadParams: Record<string, unknown> = threadIdParams ? { ...threadIdParams } : {};
@@ -378,6 +372,8 @@ async function withTelegramThreadFallback<T>(
try {
return await attempt(params, label);
} catch (err) {
// AIDEV-NOTE: Do not widen this fallback to cover "chat not found".
// chat-not-found is routing/auth/membership/token; stripping thread IDs hides root cause.
if (!hasMessageThreadIdParam(params) || !isTelegramThreadNotFoundError(err)) {
throw err;
}
@@ -441,10 +437,10 @@ export async function sendMessageTelegram(
const mediaUrl = opts.mediaUrl?.trim();
const replyMarkup = buildInlineKeyboard(opts.buttons);
const isPrivate = isTelegramPrivateChat(chatId);
const threadParams = buildTelegramThreadReplyParams({
targetMessageThreadId: isPrivate ? undefined : target.messageThreadId,
messageThreadId: isPrivate ? undefined : opts.messageThreadId,
targetMessageThreadId: target.messageThreadId,
messageThreadId: opts.messageThreadId,
chatType: target.chatType,
replyToMessageId: opts.replyToMessageId,
quoteText: opts.quoteText,
});
@@ -933,10 +929,10 @@ export async function sendStickerTelegram(
const target = parseTelegramTarget(to);
const chatId = normalizeChatId(target.chatId);
const isPrivate = isTelegramPrivateChat(chatId);
const threadParams = buildTelegramThreadReplyParams({
targetMessageThreadId: isPrivate ? undefined : target.messageThreadId,
messageThreadId: isPrivate ? undefined : opts.messageThreadId,
targetMessageThreadId: target.messageThreadId,
messageThreadId: opts.messageThreadId,
chatType: target.chatType,
replyToMessageId: opts.replyToMessageId,
});
const hasThreadParams = Object.keys(threadParams).length > 0;
@@ -1012,10 +1008,10 @@ export async function sendPollTelegram(
// Normalize the poll input (validates question, options, maxSelections)
const normalizedPoll = normalizePollInput(poll, { maxOptions: 10 });
const isPrivate = isTelegramPrivateChat(chatId);
const threadParams = buildTelegramThreadReplyParams({
targetMessageThreadId: isPrivate ? undefined : target.messageThreadId,
messageThreadId: isPrivate ? undefined : opts.messageThreadId,
targetMessageThreadId: target.messageThreadId,
messageThreadId: opts.messageThreadId,
chatType: target.chatType,
replyToMessageId: opts.replyToMessageId,
});