fix(telegram): honor outbound proxy config (#1774, thanks @radek-paclt)

Co-authored-by: Radek Paclt <developer@muj-partak.cz>
This commit is contained in:
Peter Steinberger
2026-01-25 11:41:32 +00:00
parent 65e2d939e1
commit 7e9aa3c275
3 changed files with 147 additions and 28 deletions

View File

@@ -36,6 +36,7 @@ Docs: https://docs.clawd.bot
- TUI: reload history after gateway reconnect to restore session state. (#1663)
- Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)
- Telegram: set fetch duplex="half" for uploads on Node 22 to avoid sendPhoto failures. (#1684) Thanks @commdata2338.
- Telegram: honor per-account proxy for outbound API calls. (#1774) Thanks @radek-paclt.
- Signal: repair reaction sends (group/UUID targets + CLI author flags). (#1651) Thanks @vilkasdev.
- Signal: add configurable signal-cli startup timeout + external daemon mode docs. (#1677) https://docs.clawd.bot/channels/signal
- Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco.

View File

@@ -0,0 +1,123 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { botApi, botCtorSpy } = vi.hoisted(() => ({
botApi: {
sendMessage: vi.fn(),
setMessageReaction: vi.fn(),
deleteMessage: vi.fn(),
},
botCtorSpy: vi.fn(),
}));
const { loadConfig } = vi.hoisted(() => ({
loadConfig: vi.fn(() => ({})),
}));
const { makeProxyFetch } = vi.hoisted(() => ({
makeProxyFetch: vi.fn(),
}));
const { resolveTelegramFetch } = vi.hoisted(() => ({
resolveTelegramFetch: vi.fn(),
}));
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig,
};
});
vi.mock("./proxy.js", () => ({
makeProxyFetch,
}));
vi.mock("./fetch.js", () => ({
resolveTelegramFetch,
}));
vi.mock("grammy", () => ({
Bot: class {
api = botApi;
constructor(
public token: string,
public options?: { client?: { fetch?: typeof fetch; timeoutSeconds?: number } },
) {
botCtorSpy(token, options);
}
},
InputFile: class {},
}));
import { deleteMessageTelegram, reactMessageTelegram, sendMessageTelegram } from "./send.js";
describe("telegram proxy client", () => {
const proxyUrl = "http://proxy.test:8080";
beforeEach(() => {
botApi.sendMessage.mockResolvedValue({ message_id: 1, chat: { id: "123" } });
botApi.setMessageReaction.mockResolvedValue(undefined);
botApi.deleteMessage.mockResolvedValue(true);
botCtorSpy.mockReset();
loadConfig.mockReturnValue({
channels: { telegram: { accounts: { foo: { proxy: proxyUrl } } } },
});
makeProxyFetch.mockReset();
resolveTelegramFetch.mockReset();
});
it("uses proxy fetch for sendMessage", async () => {
const proxyFetch = vi.fn();
const fetchImpl = vi.fn();
makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch);
resolveTelegramFetch.mockReturnValue(fetchImpl as unknown as typeof fetch);
await sendMessageTelegram("123", "hi", { token: "tok", accountId: "foo" });
expect(makeProxyFetch).toHaveBeenCalledWith(proxyUrl);
expect(resolveTelegramFetch).toHaveBeenCalledWith(proxyFetch);
expect(botCtorSpy).toHaveBeenCalledWith(
"tok",
expect.objectContaining({
client: expect.objectContaining({ fetch: fetchImpl }),
}),
);
});
it("uses proxy fetch for reactions", async () => {
const proxyFetch = vi.fn();
const fetchImpl = vi.fn();
makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch);
resolveTelegramFetch.mockReturnValue(fetchImpl as unknown as typeof fetch);
await reactMessageTelegram("123", "456", "✅", { token: "tok", accountId: "foo" });
expect(makeProxyFetch).toHaveBeenCalledWith(proxyUrl);
expect(resolveTelegramFetch).toHaveBeenCalledWith(proxyFetch);
expect(botCtorSpy).toHaveBeenCalledWith(
"tok",
expect.objectContaining({
client: expect.objectContaining({ fetch: fetchImpl }),
}),
);
});
it("uses proxy fetch for deleteMessage", async () => {
const proxyFetch = vi.fn();
const fetchImpl = vi.fn();
makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch);
resolveTelegramFetch.mockReturnValue(fetchImpl as unknown as typeof fetch);
await deleteMessageTelegram("123", "456", { token: "tok", accountId: "foo" });
expect(makeProxyFetch).toHaveBeenCalledWith(proxyUrl);
expect(resolveTelegramFetch).toHaveBeenCalledWith(proxyFetch);
expect(botCtorSpy).toHaveBeenCalledWith(
"tok",
expect.objectContaining({
client: expect.objectContaining({ fetch: fetchImpl }),
}),
);
});
});

View File

@@ -17,7 +17,7 @@ import { createSubsystemLogger } from "../logging/subsystem.js";
import { mediaKindFromMime } from "../media/constants.js";
import { isGifMedia } from "../media/mime.js";
import { loadWebMedia } from "../web/media.js";
import { resolveTelegramAccount } from "./accounts.js";
import { type ResolvedTelegramAccount, resolveTelegramAccount } from "./accounts.js";
import { resolveTelegramFetch } from "./fetch.js";
import { makeProxyFetch } from "./proxy.js";
import { renderTelegramHtmlText } from "./format.js";
@@ -77,6 +77,25 @@ function createTelegramHttpLogger(cfg: ReturnType<typeof loadConfig>) {
};
}
function resolveTelegramClientOptions(
account: ResolvedTelegramAccount,
): ApiClientOptions | undefined {
const proxyUrl = account.config.proxy?.trim();
const proxyFetch = proxyUrl ? makeProxyFetch(proxyUrl) : undefined;
const fetchImpl = resolveTelegramFetch(proxyFetch);
const timeoutSeconds =
typeof account.config.timeoutSeconds === "number" &&
Number.isFinite(account.config.timeoutSeconds)
? Math.max(1, Math.floor(account.config.timeoutSeconds))
: undefined;
return fetchImpl || timeoutSeconds
? {
...(fetchImpl ? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] } : {}),
...(timeoutSeconds ? { timeoutSeconds } : {}),
}
: undefined;
}
function resolveToken(explicit: string | undefined, params: { accountId: string; token: string }) {
if (explicit?.trim()) return explicit.trim();
if (!params.token) {
@@ -163,21 +182,7 @@ export async function sendMessageTelegram(
const chatId = normalizeChatId(target.chatId);
// 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 proxyUrl = account.config.proxy;
const proxyFetch = proxyUrl ? makeProxyFetch(proxyUrl as string) : undefined;
const fetchImpl = resolveTelegramFetch(proxyFetch);
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 client = resolveTelegramClientOptions(account);
const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
const mediaUrl = opts.mediaUrl?.trim();
const replyMarkup = buildInlineKeyboard(opts.buttons);
@@ -419,12 +424,7 @@ export async function reactMessageTelegram(
const token = resolveToken(opts.token, account);
const chatId = normalizeChatId(String(chatIdInput));
const messageId = normalizeMessageId(messageIdInput);
const proxyUrl = account.config.proxy;
const proxyFetch = proxyUrl ? makeProxyFetch(proxyUrl as string) : undefined;
const fetchImpl = resolveTelegramFetch(proxyFetch);
const client: ApiClientOptions | undefined = fetchImpl
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
: undefined;
const client = resolveTelegramClientOptions(account);
const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
const request = createTelegramRetryRunner({
retry: opts.retry,
@@ -473,12 +473,7 @@ export async function deleteMessageTelegram(
const token = resolveToken(opts.token, account);
const chatId = normalizeChatId(String(chatIdInput));
const messageId = normalizeMessageId(messageIdInput);
const proxyUrl = account.config.proxy;
const proxyFetch = proxyUrl ? makeProxyFetch(proxyUrl as string) : undefined;
const fetchImpl = resolveTelegramFetch(proxyFetch);
const client: ApiClientOptions | undefined = fetchImpl
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
: undefined;
const client = resolveTelegramClientOptions(account);
const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
const request = createTelegramRetryRunner({
retry: opts.retry,