fix(telegram): honor outbound proxy config (#1774, thanks @radek-paclt)
Co-authored-by: Radek Paclt <developer@muj-partak.cz>
This commit is contained in:
@@ -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.
|
||||
|
||||
123
src/telegram/send.proxy.test.ts
Normal file
123
src/telegram/send.proxy.test.ts
Normal 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 }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user