fix: force telegram native fetch under bun

This commit is contained in:
Peter Steinberger
2026-01-08 04:40:29 +01:00
parent 3178a3014d
commit ab98ffe9fe
7 changed files with 101 additions and 11 deletions

View File

@@ -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);

View File

@@ -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());

8
src/telegram/fetch.ts Normal file
View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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();
}