fix(telegram): honor timeoutSeconds (thanks @Snaver) (#863)
This commit is contained in:
@@ -175,6 +175,7 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"channels.telegram.retry.minDelayMs": "Telegram Retry Min Delay (ms)",
|
||||
"channels.telegram.retry.maxDelayMs": "Telegram Retry Max Delay (ms)",
|
||||
"channels.telegram.retry.jitter": "Telegram Retry Jitter",
|
||||
"channels.telegram.timeoutSeconds": "Telegram API Timeout (seconds)",
|
||||
"channels.whatsapp.dmPolicy": "WhatsApp DM Policy",
|
||||
"channels.whatsapp.selfChatMode": "WhatsApp Self-Phone Mode",
|
||||
"channels.signal.dmPolicy": "Signal DM Policy",
|
||||
@@ -330,6 +331,8 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"Maximum retry delay cap in ms for Telegram outbound calls.",
|
||||
"channels.telegram.retry.jitter":
|
||||
"Jitter factor (0-1) applied to Telegram retry delays.",
|
||||
"channels.telegram.timeoutSeconds":
|
||||
"Max seconds before Telegram API requests are aborted (default: 500 per grammY).",
|
||||
"channels.whatsapp.dmPolicy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.whatsapp.allowFrom=["*"].',
|
||||
"channels.whatsapp.selfChatMode":
|
||||
|
||||
@@ -63,6 +63,8 @@ export type TelegramAccountConfig = {
|
||||
/** Draft streaming mode for Telegram (off|partial|block). Default: partial. */
|
||||
streamMode?: "off" | "partial" | "block";
|
||||
mediaMaxMb?: number;
|
||||
/** Telegram API client timeout in seconds (grammY ApiClientOptions). */
|
||||
timeoutSeconds?: number;
|
||||
/** Retry policy for outbound Telegram API calls. */
|
||||
retry?: OutboundRetryConfig;
|
||||
proxy?: string;
|
||||
|
||||
@@ -53,6 +53,7 @@ export const TelegramAccountSchemaBase = z.object({
|
||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||
streamMode: z.enum(["off", "partial", "block"]).optional().default("partial"),
|
||||
mediaMaxMb: z.number().positive().optional(),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
retry: RetryConfigSchema,
|
||||
proxy: z.string().optional(),
|
||||
webhookUrl: z.string().optional(),
|
||||
|
||||
@@ -80,7 +80,9 @@ vi.mock("grammy", () => ({
|
||||
command = commandSpy;
|
||||
constructor(
|
||||
public token: string,
|
||||
public options?: { client?: { fetch?: typeof fetch } },
|
||||
public options?: {
|
||||
client?: { fetch?: typeof fetch; timeoutSeconds?: number };
|
||||
},
|
||||
) {
|
||||
botCtorSpy(token, options);
|
||||
}
|
||||
@@ -195,6 +197,20 @@ describe("createTelegramBot", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
it("passes timeoutSeconds even without a custom fetch", () => {
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
telegram: { dmPolicy: "open", allowFrom: ["*"], timeoutSeconds: 60 },
|
||||
},
|
||||
});
|
||||
createTelegramBot({ token: "tok" });
|
||||
expect(botCtorSpy).toHaveBeenCalledWith(
|
||||
"tok",
|
||||
expect.objectContaining({
|
||||
client: expect.objectContaining({ timeoutSeconds: 60 }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
it("sequentializes updates by chat and thread", () => {
|
||||
createTelegramBot({ token: "tok" });
|
||||
expect(sequentializeSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -93,14 +93,30 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
throw new Error(`exit ${code}`);
|
||||
},
|
||||
};
|
||||
const cfg = opts.config ?? loadConfig();
|
||||
const account = resolveTelegramAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const telegramCfg = account.config;
|
||||
|
||||
const fetchImpl = resolveTelegramFetch(opts.proxyFetch);
|
||||
const isBun = "Bun" in globalThis || Boolean(process?.versions?.bun);
|
||||
const shouldProvideFetch = Boolean(opts.proxyFetch) || isBun;
|
||||
const client: ApiClientOptions | undefined = fetchImpl
|
||||
? shouldProvideFetch
|
||||
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
||||
: undefined
|
||||
: undefined;
|
||||
const timeoutSeconds =
|
||||
typeof telegramCfg?.timeoutSeconds === "number" &&
|
||||
Number.isFinite(telegramCfg.timeoutSeconds)
|
||||
? Math.max(1, Math.floor(telegramCfg.timeoutSeconds))
|
||||
: undefined;
|
||||
const client: ApiClientOptions | undefined =
|
||||
shouldProvideFetch || timeoutSeconds
|
||||
? {
|
||||
...(shouldProvideFetch && fetchImpl
|
||||
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
||||
: {}),
|
||||
...(timeoutSeconds ? { timeoutSeconds } : {}),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const bot = new Bot(opts.token, client ? { client } : undefined);
|
||||
bot.api.config.use(apiThrottler());
|
||||
@@ -138,12 +154,6 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
recordUpdateId(ctx);
|
||||
});
|
||||
|
||||
const cfg = opts.config ?? loadConfig();
|
||||
const account = resolveTelegramAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const telegramCfg = account.config;
|
||||
const historyLimit = Math.max(
|
||||
0,
|
||||
telegramCfg.historyLimit ??
|
||||
|
||||
@@ -21,7 +21,9 @@ vi.mock("grammy", () => ({
|
||||
api = botApi;
|
||||
constructor(
|
||||
public token: string,
|
||||
public options?: { client?: { fetch?: typeof fetch } },
|
||||
public options?: {
|
||||
client?: { fetch?: typeof fetch; timeoutSeconds?: number };
|
||||
},
|
||||
) {
|
||||
botCtorSpy(token, options);
|
||||
}
|
||||
@@ -29,6 +31,17 @@ vi.mock("grammy", () => ({
|
||||
InputFile: class {},
|
||||
}));
|
||||
|
||||
const { loadConfig } = vi.hoisted(() => ({
|
||||
loadConfig: vi.fn(() => ({})),
|
||||
}));
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig,
|
||||
};
|
||||
});
|
||||
|
||||
import { buildInlineKeyboard, sendMessageTelegram } from "./send.js";
|
||||
|
||||
describe("buildInlineKeyboard", () => {
|
||||
@@ -73,11 +86,25 @@ describe("buildInlineKeyboard", () => {
|
||||
|
||||
describe("sendMessageTelegram", () => {
|
||||
beforeEach(() => {
|
||||
loadConfig.mockReturnValue({});
|
||||
loadWebMedia.mockReset();
|
||||
botApi.sendMessage.mockReset();
|
||||
botCtorSpy.mockReset();
|
||||
});
|
||||
|
||||
it("passes timeoutSeconds to grammY client when configured", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
channels: { telegram: { timeoutSeconds: 60 } },
|
||||
});
|
||||
await sendMessageTelegram("123", "hi", { token: "tok" });
|
||||
expect(botCtorSpy).toHaveBeenCalledWith(
|
||||
"tok",
|
||||
expect.objectContaining({
|
||||
client: expect.objectContaining({ timeoutSeconds: 60 }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to plain text when Telegram rejects HTML", async () => {
|
||||
const chatId = "123";
|
||||
const parseErr = new Error(
|
||||
|
||||
@@ -148,9 +148,20 @@ export async function sendMessageTelegram(
|
||||
// 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 fetchImpl = resolveTelegramFetch();
|
||||
const client: ApiClientOptions | undefined = fetchImpl
|
||||
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
||||
: undefined;
|
||||
const timeoutSeconds =
|
||||
typeof account.config.timeoutSeconds === "number" &&
|
||||
Number.isFinite(account.config.timeoutSeconds)
|
||||
? Math.max(1, Math.floor(account.config.timeoutSeconds))
|
||||
: undefined;
|
||||
const client: ApiClientOptions | undefined =
|
||||
fetchImpl || timeoutSeconds
|
||||
? {
|
||||
...(fetchImpl
|
||||
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
||||
: {}),
|
||||
...(timeoutSeconds ? { timeoutSeconds } : {}),
|
||||
}
|
||||
: undefined;
|
||||
const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
|
||||
const mediaUrl = opts.mediaUrl?.trim();
|
||||
const replyMarkup = buildInlineKeyboard(opts.buttons);
|
||||
|
||||
Reference in New Issue
Block a user