mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 14:07:27 +00:00
fix: preserve telegram dm topic thread ids
This commit is contained in:
@@ -850,8 +850,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("suppresses message_thread_id for private chat sends (#17242)", async () => {
|
||||
// Private chats have positive numeric IDs; they never support forum topics.
|
||||
it("keeps message_thread_id for private chat topic sends (#18974)", async () => {
|
||||
const chatId = "123456789";
|
||||
const sendMessage = vi.fn().mockResolvedValue({
|
||||
message_id: 56,
|
||||
@@ -867,10 +866,9 @@ describe("sendMessageTelegram", () => {
|
||||
messageThreadId: 271,
|
||||
});
|
||||
|
||||
// message_thread_id must NOT appear in private chats -- Telegram rejects it
|
||||
// with "400: Bad Request: message thread not found".
|
||||
expect(sendMessage).toHaveBeenCalledWith(chatId, "hello private", {
|
||||
parse_mode: "HTML",
|
||||
message_thread_id: 271,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -927,6 +925,36 @@ describe("sendMessageTelegram", () => {
|
||||
expect(res.messageId).toBe("58");
|
||||
});
|
||||
|
||||
it("retries private chat sends without message_thread_id on thread-not-found", async () => {
|
||||
const chatId = "123456789";
|
||||
const threadErr = new Error("400: Bad Request: message thread not found");
|
||||
const sendMessage = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(threadErr)
|
||||
.mockResolvedValueOnce({
|
||||
message_id: 59,
|
||||
chat: { id: chatId },
|
||||
});
|
||||
const api = { sendMessage } as unknown as {
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
|
||||
const res = await sendMessageTelegram(chatId, "hello private", {
|
||||
token: "tok",
|
||||
api,
|
||||
messageThreadId: 271,
|
||||
});
|
||||
|
||||
expect(sendMessage).toHaveBeenNthCalledWith(1, chatId, "hello private", {
|
||||
parse_mode: "HTML",
|
||||
message_thread_id: 271,
|
||||
});
|
||||
expect(sendMessage).toHaveBeenNthCalledWith(2, chatId, "hello private", {
|
||||
parse_mode: "HTML",
|
||||
});
|
||||
expect(res.messageId).toBe("59");
|
||||
});
|
||||
|
||||
it("does not retry thread-not-found when no message_thread_id was provided", async () => {
|
||||
const chatId = "123";
|
||||
const threadErr = new Error("400: Bad Request: message thread not found");
|
||||
@@ -944,6 +972,29 @@ describe("sendMessageTelegram", () => {
|
||||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not retry without message_thread_id on chat-not-found", async () => {
|
||||
const chatId = "123456789";
|
||||
const chatErr = new Error("400: Bad Request: chat not found");
|
||||
const sendMessage = vi.fn().mockRejectedValueOnce(chatErr);
|
||||
const api = { sendMessage } as unknown as {
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
|
||||
await expect(
|
||||
sendMessageTelegram(chatId, "hello private", {
|
||||
token: "tok",
|
||||
api,
|
||||
messageThreadId: 271,
|
||||
}),
|
||||
).rejects.toThrow(/chat not found/i);
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessage).toHaveBeenCalledWith(chatId, "hello private", {
|
||||
parse_mode: "HTML",
|
||||
message_thread_id: 271,
|
||||
});
|
||||
});
|
||||
|
||||
it("sets disable_notification when silent is true", async () => {
|
||||
const chatId = "123";
|
||||
const sendMessage = vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user