From ab98ffe9fe87ac88ef6842702ee693d732066da1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 8 Jan 2026 04:40:29 +0100 Subject: [PATCH] fix: force telegram native fetch under bun --- CHANGELOG.md | 4 +--- src/telegram/bot.test.ts | 26 +++++++++++++++++++++- src/telegram/bot.ts | 8 ++++--- src/telegram/fetch.ts | 8 +++++++ src/telegram/send.test.ts | 44 +++++++++++++++++++++++++++++++++++++ src/telegram/send.ts | 13 +++++++++-- src/telegram/webhook-set.ts | 9 ++++++-- 7 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 src/telegram/fetch.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 712b2a226..9ad5ddd5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,6 @@ - Web UI: allow reconnect + password URL auth for the control UI and always scrub auth params from the URL. Thanks @oswalpalash for PR #414. - Web UI: add Connect button on Overview to apply connection changes. Thanks @wizaj for PR #385. - Web UI: keep Focus toggle on the top bar (swap with theme toggle) so it stays visible. Thanks @RobOK2050 for reporting. (#440) -- Web UI: add Logs tab for gateway file logs with filtering, auto-follow, and export. - ClawdbotKit: fix SwiftPM resource bundling path for `tool-display.json`. Thanks @fcatuhe for PR #398. - Tools: add Telegram/WhatsApp reaction tools (with per-provider gating). Thanks @zats for PR #353. - Tools: flatten literal-union schemas for Claude on Vertex AI. Thanks @carlulsoe for PR #409. @@ -70,7 +69,6 @@ - Agent: deliver final replies for non-streaming models when block chunking is enabled. Thank you @mneves75 for PR #369! - Agent: trim bootstrap context injections and keep group guidance concise (emoji reactions allowed). Thanks @tobiasbischoff for PR #370. - Agent: return a friendly context overflow response (413/request_too_large). Thanks @alejandroOPI for PR #395. -- Agent: auto-enable Z.AI GLM-4.x thinking params (preserved for 4.7, interleaved for 4.5/4.6) unless disabled. Thanks @mneves75 for PR #443. - Sub-agents: allow `sessions_spawn` model overrides and error on invalid models. Thanks @azade-c for PR #298. - Sub-agents: skip invalid model overrides with a warning and keep the run alive; tool exceptions now return tool errors instead of crashing the agent. - Sessions: forward explicit sessionKey through gateway/chat/node bridge to avoid sub-agent sessionId mixups. @@ -96,7 +94,7 @@ - Telegram: honor `/activation` session mode for group mention gating and clarify group activation docs. Thanks @julianengel for PR #377. - Telegram: isolate forum topic transcripts per thread and validate Gemini turn ordering in multi-topic sessions. Thanks @hsrvc for PR #407. - Telegram: render Telegram-safe HTML for outbound formatting and fall back to plain text on parse errors. Thanks @RandyVentures for PR #435. -- Telegram: add `[[audio_as_voice]]` tag to send audio as voice notes (audio files remain default); docs updated. Thanks @manmal for PR #188. +- Telegram: force grammY to use native fetch under Bun for BAN compatibility (avoids TLS chain errors). - iMessage: ignore disconnect errors during shutdown (avoid unhandled promise rejections). Thanks @antons for PR #359. - 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. diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index 85a590a09..8a98d6f27 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -44,6 +44,7 @@ const middlewareUseSpy = vi.fn(); const onSpy = vi.fn(); const stopSpy = vi.fn(); const commandSpy = vi.fn(); +const botCtorSpy = vi.fn(); const sendChatActionSpy = vi.fn(); const setMessageReactionSpy = vi.fn(async () => undefined); const setMyCommandsSpy = vi.fn(async () => undefined); @@ -76,7 +77,12 @@ vi.mock("grammy", () => ({ on = onSpy; stop = stopSpy; command = commandSpy; - constructor(public token: string) {} + constructor( + public token: string, + public options?: { client?: { fetch?: typeof fetch } }, + ) { + botCtorSpy(token, options); + } }, InputFile: class {}, webhookCallback: vi.fn(), @@ -118,6 +124,7 @@ describe("createTelegramBot", () => { setMyCommandsSpy.mockReset(); middlewareUseSpy.mockReset(); sequentializeSpy.mockReset(); + botCtorSpy.mockReset(); sequentializeKey = undefined; }); @@ -127,6 +134,23 @@ describe("createTelegramBot", () => { expect(useSpy).toHaveBeenCalledWith("throttler"); }); + it("forces native fetch for BAN compatibility", () => { + const originalFetch = globalThis.fetch; + const fetchSpy = vi.fn() as unknown as typeof fetch; + globalThis.fetch = fetchSpy; + try { + createTelegramBot({ token: "tok" }); + expect(botCtorSpy).toHaveBeenCalledWith( + "tok", + expect.objectContaining({ + client: expect.objectContaining({ fetch: fetchSpy }), + }), + ); + } finally { + globalThis.fetch = originalFetch; + } + }); + it("sequentializes updates by chat and thread", () => { createTelegramBot({ token: "tok" }); expect(sequentializeSpy).toHaveBeenCalledTimes(1); diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index d3de7c04c..94f2b35af 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -52,6 +52,7 @@ import type { RuntimeEnv } from "../runtime.js"; import { loadWebMedia } from "../web/media.js"; import { resolveTelegramAccount } from "./accounts.js"; import { createTelegramDraftStream } from "./draft-stream.js"; +import { resolveTelegramFetch } from "./fetch.js"; import { markdownToTelegramHtml } from "./format.js"; import { readTelegramAllowFromStore, @@ -150,9 +151,10 @@ export function createTelegramBot(opts: TelegramBotOptions) { throw new Error(`exit ${code}`); }, }; - const client: ApiClientOptions | undefined = opts.proxyFetch - ? { fetch: opts.proxyFetch as unknown as ApiClientOptions["fetch"] } - : undefined; + const fetchImpl = resolveTelegramFetch(opts.proxyFetch); + const client: ApiClientOptions = { + fetch: fetchImpl as unknown as ApiClientOptions["fetch"], + }; const bot = new Bot(opts.token, { client }); bot.api.config.use(apiThrottler()); diff --git a/src/telegram/fetch.ts b/src/telegram/fetch.ts new file mode 100644 index 000000000..8ff2a659e --- /dev/null +++ b/src/telegram/fetch.ts @@ -0,0 +1,8 @@ +// BAN compatibility: force native fetch to avoid grammY's node-fetch shim under Bun. +export function resolveTelegramFetch(proxyFetch?: typeof fetch): typeof fetch { + const fetchImpl = proxyFetch ?? globalThis.fetch; + if (!fetchImpl) { + throw new Error("fetch is not available; set telegram.proxy in config"); + } + return fetchImpl; +} diff --git a/src/telegram/send.test.ts b/src/telegram/send.test.ts index fc50cd669..0d20ed7be 100644 --- a/src/telegram/send.test.ts +++ b/src/telegram/send.test.ts @@ -1,5 +1,13 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +const { botApi, botCtorSpy } = vi.hoisted(() => ({ + botApi: { + sendMessage: vi.fn(), + setMessageReaction: vi.fn(), + }, + botCtorSpy: vi.fn(), +})); + const { loadWebMedia } = vi.hoisted(() => ({ loadWebMedia: vi.fn(), })); @@ -8,11 +16,26 @@ vi.mock("../web/media.js", () => ({ loadWebMedia, })); +vi.mock("grammy", () => ({ + Bot: class { + api = botApi; + constructor( + public token: string, + public options?: { client?: { fetch?: typeof fetch } }, + ) { + botCtorSpy(token, options); + } + }, + InputFile: class {}, +})); + import { reactMessageTelegram, sendMessageTelegram } from "./send.js"; describe("sendMessageTelegram", () => { beforeEach(() => { loadWebMedia.mockReset(); + botApi.sendMessage.mockReset(); + botCtorSpy.mockReset(); }); it("falls back to plain text when Telegram rejects HTML", async () => { @@ -45,6 +68,27 @@ describe("sendMessageTelegram", () => { expect(res.messageId).toBe("42"); }); + it("uses native fetch for BAN compatibility when api is omitted", async () => { + const originalFetch = globalThis.fetch; + const fetchSpy = vi.fn() as unknown as typeof fetch; + globalThis.fetch = fetchSpy; + botApi.sendMessage.mockResolvedValue({ + message_id: 1, + chat: { id: "123" }, + }); + try { + await sendMessageTelegram("123", "hi", { token: "tok" }); + expect(botCtorSpy).toHaveBeenCalledWith( + "tok", + expect.objectContaining({ + client: expect.objectContaining({ fetch: fetchSpy }), + }), + ); + } finally { + globalThis.fetch = originalFetch; + } + }); + it("normalizes chat ids with internal prefixes", async () => { const sendMessage = vi.fn().mockResolvedValue({ message_id: 1, diff --git a/src/telegram/send.ts b/src/telegram/send.ts index a2181655c..c20cf811a 100644 --- a/src/telegram/send.ts +++ b/src/telegram/send.ts @@ -8,6 +8,7 @@ import { mediaKindFromMime } from "../media/constants.js"; import { isGifMedia } from "../media/mime.js"; import { loadWebMedia } from "../web/media.js"; import { resolveTelegramAccount } from "./accounts.js"; +import { resolveTelegramFetch } from "./fetch.js"; import { markdownToTelegramHtml } from "./format.js"; type TelegramSendOpts = { @@ -111,7 +112,11 @@ export async function sendMessageTelegram( const chatId = normalizeChatId(to); // Use provided api or create a new Bot instance. The nullish coalescing // operator ensures api is always defined (Bot.api is always non-null). - const api = opts.api ?? new Bot(token).api; + const api = + opts.api ?? + new Bot(token, { + client: { fetch: resolveTelegramFetch() }, + }).api; const mediaUrl = opts.mediaUrl?.trim(); // Build optional params for forum topics and reply threading. @@ -265,7 +270,11 @@ export async function reactMessageTelegram( const token = resolveToken(opts.token, account); const chatId = normalizeChatId(String(chatIdInput)); const messageId = normalizeMessageId(messageIdInput); - const api = opts.api ?? new Bot(token).api; + const api = + opts.api ?? + new Bot(token, { + client: { fetch: resolveTelegramFetch() }, + }).api; const request = createTelegramRetryRunner({ retry: opts.retry, configRetry: account.config.retry, diff --git a/src/telegram/webhook-set.ts b/src/telegram/webhook-set.ts index 5c1811b5d..002f70b37 100644 --- a/src/telegram/webhook-set.ts +++ b/src/telegram/webhook-set.ts @@ -1,4 +1,5 @@ import { Bot } from "grammy"; +import { resolveTelegramFetch } from "./fetch.js"; export async function setTelegramWebhook(opts: { token: string; @@ -6,7 +7,9 @@ export async function setTelegramWebhook(opts: { secret?: string; dropPendingUpdates?: boolean; }) { - const bot = new Bot(opts.token); + const bot = new Bot(opts.token, { + client: { fetch: resolveTelegramFetch() }, + }); await bot.api.setWebhook(opts.url, { secret_token: opts.secret, drop_pending_updates: opts.dropPendingUpdates ?? false, @@ -14,6 +17,8 @@ export async function setTelegramWebhook(opts: { } export async function deleteTelegramWebhook(opts: { token: string }) { - const bot = new Bot(opts.token); + const bot = new Bot(opts.token, { + client: { fetch: resolveTelegramFetch() }, + }); await bot.api.deleteWebhook(); }