From 24473b7dd0936732ce3f5c5770bf5f09fe44dcb5 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sat, 21 Mar 2026 09:56:23 +0530 Subject: [PATCH] fix(telegram): honor lookup transport and local file paths --- extensions/telegram/src/api-fetch.test.ts | 24 ++++++++++++ extensions/telegram/src/api-fetch.ts | 17 ++++++++- .../bot/delivery.resolve-media-retry.test.ts | 32 ++++++++++++++++ .../src/bot/delivery.resolve-media.ts | 4 ++ extensions/telegram/src/setup-core.test.ts | 20 +++++++++- extensions/telegram/src/setup-core.ts | 12 +++++- src/commands/doctor-config-flow.test.ts | 38 +++++++++++++++++-- src/commands/doctor-config-flow.ts | 15 ++++++-- 8 files changed, 150 insertions(+), 12 deletions(-) diff --git a/extensions/telegram/src/api-fetch.test.ts b/extensions/telegram/src/api-fetch.test.ts index e65499ef25c..5de45f6ee75 100644 --- a/extensions/telegram/src/api-fetch.test.ts +++ b/extensions/telegram/src/api-fetch.test.ts @@ -54,4 +54,28 @@ describe("fetchTelegramChatId", () => { undefined, ); }); + + it("uses caller-provided fetch impl when present", async () => { + const customFetch = vi.fn(async () => ({ + ok: true, + json: async () => ({ ok: true, result: { id: 12345 } }), + })); + vi.stubGlobal( + "fetch", + vi.fn(async () => { + throw new Error("global fetch should not be called"); + }), + ); + + await fetchTelegramChatId({ + token: "abc", + chatId: "@user", + fetchImpl: customFetch as unknown as typeof fetch, + }); + + expect(customFetch).toHaveBeenCalledWith( + "https://api.telegram.org/botabc/getChat?chat_id=%40user", + undefined, + ); + }); }); diff --git a/extensions/telegram/src/api-fetch.ts b/extensions/telegram/src/api-fetch.ts index cbe325897b9..5389e7ed841 100644 --- a/extensions/telegram/src/api-fetch.ts +++ b/extensions/telegram/src/api-fetch.ts @@ -1,15 +1,28 @@ -import { resolveTelegramApiBase } from "./fetch.js"; +import type { TelegramNetworkConfig } from "../runtime-api.js"; +import { resolveTelegramApiBase, resolveTelegramFetch } from "./fetch.js"; +import { makeProxyFetch } from "./proxy.js"; + +export function resolveTelegramChatLookupFetch(params?: { + proxyUrl?: string; + network?: TelegramNetworkConfig; +}): typeof fetch { + const proxyUrl = params?.proxyUrl?.trim(); + const proxyFetch = proxyUrl ? makeProxyFetch(proxyUrl) : undefined; + return resolveTelegramFetch(proxyFetch, { network: params?.network }); +} export async function fetchTelegramChatId(params: { token: string; chatId: string; signal?: AbortSignal; apiRoot?: string; + fetchImpl?: typeof fetch; }): Promise { const apiBase = resolveTelegramApiBase(params.apiRoot); const url = `${apiBase}/bot${params.token}/getChat?chat_id=${encodeURIComponent(params.chatId)}`; + const fetchImpl = params.fetchImpl ?? fetch; try { - const res = await fetch(url, params.signal ? { signal: params.signal } : undefined); + const res = await fetchImpl(url, params.signal ? { signal: params.signal } : undefined); if (!res.ok) { return null; } diff --git a/extensions/telegram/src/bot/delivery.resolve-media-retry.test.ts b/extensions/telegram/src/bot/delivery.resolve-media-retry.test.ts index b1cd7eb4d8a..86d6e608dce 100644 --- a/extensions/telegram/src/bot/delivery.resolve-media-retry.test.ts +++ b/extensions/telegram/src/bot/delivery.resolve-media-retry.test.ts @@ -360,6 +360,38 @@ describe("resolveMedia getFile retry", () => { }), ); }); + + it("uses local absolute file paths directly for media downloads", async () => { + const getFile = vi.fn().mockResolvedValue({ file_path: "/var/lib/telegram-bot-api/file.pdf" }); + + const result = await resolveMedia(makeCtx("document", getFile), MAX_MEDIA_BYTES, BOT_TOKEN); + + expect(fetchRemoteMedia).not.toHaveBeenCalled(); + expect(saveMediaBuffer).not.toHaveBeenCalled(); + expect(result).toEqual( + expect.objectContaining({ + path: "/var/lib/telegram-bot-api/file.pdf", + placeholder: "", + }), + ); + }); + + it("uses local absolute file paths directly for sticker downloads", async () => { + const getFile = vi + .fn() + .mockResolvedValue({ file_path: "/var/lib/telegram-bot-api/sticker.webp" }); + + const result = await resolveMedia(makeCtx("sticker", getFile), MAX_MEDIA_BYTES, BOT_TOKEN); + + expect(fetchRemoteMedia).not.toHaveBeenCalled(); + expect(saveMediaBuffer).not.toHaveBeenCalled(); + expect(result).toEqual( + expect.objectContaining({ + path: "/var/lib/telegram-bot-api/sticker.webp", + placeholder: "", + }), + ); + }); }); describe("resolveMedia original filename preservation", () => { diff --git a/extensions/telegram/src/bot/delivery.resolve-media.ts b/extensions/telegram/src/bot/delivery.resolve-media.ts index 3199ac4c14f..2e552529dec 100644 --- a/extensions/telegram/src/bot/delivery.resolve-media.ts +++ b/extensions/telegram/src/bot/delivery.resolve-media.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import { GrammyError } from "grammy"; import { formatErrorMessage } from "openclaw/plugin-sdk/infra-runtime"; import { retryAsync } from "openclaw/plugin-sdk/infra-runtime"; @@ -143,6 +144,9 @@ async function downloadAndSaveTelegramFile(params: { telegramFileName?: string; apiRoot?: string; }) { + if (path.isAbsolute(params.filePath)) { + return { path: params.filePath, contentType: undefined }; + } const apiBase = resolveTelegramApiBase(params.apiRoot); const url = `${apiBase}/file/bot${params.token}/${params.filePath}`; const fetched = await fetchRemoteMedia({ diff --git a/extensions/telegram/src/setup-core.test.ts b/extensions/telegram/src/setup-core.test.ts index 42b1cd913fa..5cf316c54d6 100644 --- a/extensions/telegram/src/setup-core.test.ts +++ b/extensions/telegram/src/setup-core.test.ts @@ -3,25 +3,43 @@ import { resolveTelegramAllowFromEntries } from "./setup-core.js"; describe("resolveTelegramAllowFromEntries", () => { it("passes apiRoot through username lookups", async () => { + const globalFetch = vi.fn(async () => { + throw new Error("global fetch should not be called"); + }); const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ ok: true, result: { id: 12345 } }), })); - vi.stubGlobal("fetch", fetchMock); + vi.stubGlobal("fetch", globalFetch); + const proxyFetch = vi.fn(); + const fetchModule = await import("./fetch.js"); + const proxyModule = await import("./proxy.js"); + const resolveTelegramFetch = vi.spyOn(fetchModule, "resolveTelegramFetch"); + const makeProxyFetch = vi.spyOn(proxyModule, "makeProxyFetch"); + makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch); + resolveTelegramFetch.mockReturnValue(fetchMock as unknown as typeof fetch); try { const resolved = await resolveTelegramAllowFromEntries({ entries: ["@user"], credentialValue: "tok", apiRoot: "https://custom.telegram.test/root/", + proxyUrl: "http://127.0.0.1:8080", + network: { autoSelectFamily: false, dnsResultOrder: "ipv4first" }, }); expect(resolved).toEqual([{ input: "@user", resolved: true, id: "12345" }]); + expect(makeProxyFetch).toHaveBeenCalledWith("http://127.0.0.1:8080"); + expect(resolveTelegramFetch).toHaveBeenCalledWith(proxyFetch, { + network: { autoSelectFamily: false, dnsResultOrder: "ipv4first" }, + }); expect(fetchMock).toHaveBeenCalledWith( "https://custom.telegram.test/root/bottok/getChat?chat_id=%40user", undefined, ); } finally { + makeProxyFetch.mockRestore(); + resolveTelegramFetch.mockRestore(); vi.unstubAllGlobals(); } }); diff --git a/extensions/telegram/src/setup-core.ts b/extensions/telegram/src/setup-core.ts index 093b686cdde..63c4ab87437 100644 --- a/extensions/telegram/src/setup-core.ts +++ b/extensions/telegram/src/setup-core.ts @@ -9,8 +9,9 @@ import { } from "openclaw/plugin-sdk/setup"; import type { ChannelSetupAdapter, ChannelSetupDmPolicy } from "openclaw/plugin-sdk/setup"; import { formatCliCommand, formatDocsLink } from "openclaw/plugin-sdk/setup-tools"; +import type { TelegramNetworkConfig } from "../runtime-api.js"; import { resolveDefaultTelegramAccountId, resolveTelegramAccount } from "./accounts.js"; -import { fetchTelegramChatId } from "./api-fetch.js"; +import { fetchTelegramChatId, resolveTelegramChatLookupFetch } from "./api-fetch.js"; const channel = "telegram" as const; @@ -47,7 +48,13 @@ export async function resolveTelegramAllowFromEntries(params: { entries: string[]; credentialValue?: string; apiRoot?: string; + proxyUrl?: string; + network?: TelegramNetworkConfig; }) { + const fetchImpl = resolveTelegramChatLookupFetch({ + proxyUrl: params.proxyUrl, + network: params.network, + }); return await Promise.all( params.entries.map(async (entry) => { const numericId = parseTelegramAllowFromId(entry); @@ -63,6 +70,7 @@ export async function resolveTelegramAllowFromEntries(params: { token: params.credentialValue, chatId: username, apiRoot: params.apiRoot, + fetchImpl, }); return { input: entry, resolved: Boolean(id), id }; }), @@ -99,6 +107,8 @@ export async function promptTelegramAllowFromForAccount(params: { credentialValue: token, entries, apiRoot: resolved.config.apiRoot, + proxyUrl: resolved.config.proxy, + network: resolved.config.network, }), }); return patchChannelConfigForAccount({ diff --git a/src/commands/doctor-config-flow.test.ts b/src/commands/doctor-config-flow.test.ts index c42d8ad3012..7590402bde5 100644 --- a/src/commands/doctor-config-flow.test.ts +++ b/src/commands/doctor-config-flow.test.ts @@ -517,8 +517,11 @@ describe("doctor config flow", () => { }); it("resolves Telegram @username allowFrom entries to numeric IDs on repair", async () => { - const fetchSpy = vi.fn(async (url: string) => { - const u = String(url); + const globalFetch = vi.fn(async () => { + throw new Error("global fetch should not be called"); + }); + const fetchSpy = vi.fn(async (input: RequestInfo | URL) => { + const u = input instanceof URL ? input.href : typeof input === "string" ? input : input.url; const chatId = new URL(u).searchParams.get("chat_id") ?? ""; const id = chatId.toLowerCase() === "@testuser" @@ -535,7 +538,14 @@ describe("doctor config flow", () => { json: async () => (id != null ? { ok: true, result: { id } } : { ok: false }), } as unknown as Response; }); - vi.stubGlobal("fetch", fetchSpy); + vi.stubGlobal("fetch", globalFetch); + const proxyFetch = vi.fn(); + const telegramFetchModule = await import("../../extensions/telegram/src/fetch.js"); + const telegramProxyModule = await import("../../extensions/telegram/src/proxy.js"); + const resolveTelegramFetch = vi.spyOn(telegramFetchModule, "resolveTelegramFetch"); + const makeProxyFetch = vi.spyOn(telegramProxyModule, "makeProxyFetch"); + makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch); + resolveTelegramFetch.mockReturnValue(fetchSpy as unknown as typeof fetch); try { const result = await runDoctorConfigWithInput({ repair: true, @@ -581,6 +591,8 @@ describe("doctor config flow", () => { expect(cfg.channels.telegram.accounts.default.allowFrom).toEqual(["111"]); expect(cfg.channels.telegram.accounts.default.groupAllowFrom).toEqual(["222"]); } finally { + makeProxyFetch.mockRestore(); + resolveTelegramFetch.mockRestore(); vi.unstubAllGlobals(); } }); @@ -634,6 +646,9 @@ describe("doctor config flow", () => { }); it("uses account apiRoot when repairing Telegram allowFrom usernames", async () => { + const globalFetch = vi.fn(async () => { + throw new Error("global fetch should not be called"); + }); const fetchSpy = vi.fn(async (input: RequestInfo | URL) => { const url = input instanceof URL ? input.href : typeof input === "string" ? input : input.url; expect(url).toBe("https://custom.telegram.test/root/bottok/getChat?chat_id=%40testuser"); @@ -642,7 +657,14 @@ describe("doctor config flow", () => { json: async () => ({ ok: true, result: { id: 12345 } }), }; }); - vi.stubGlobal("fetch", fetchSpy); + vi.stubGlobal("fetch", globalFetch); + const proxyFetch = vi.fn(); + const telegramFetchModule = await import("../../extensions/telegram/src/fetch.js"); + const telegramProxyModule = await import("../../extensions/telegram/src/proxy.js"); + const resolveTelegramFetch = vi.spyOn(telegramFetchModule, "resolveTelegramFetch"); + const makeProxyFetch = vi.spyOn(telegramProxyModule, "makeProxyFetch"); + makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch); + resolveTelegramFetch.mockReturnValue(fetchSpy as unknown as typeof fetch); const resolveSecretsSpy = vi .spyOn(commandSecretGatewayModule, "resolveCommandSecretRefsViaGateway") .mockResolvedValue({ @@ -656,6 +678,8 @@ describe("doctor config flow", () => { work: { botToken: "tok", apiRoot: "https://custom.telegram.test/root/", + proxy: "http://127.0.0.1:8888", + network: { autoSelectFamily: false, dnsResultOrder: "ipv4first" }, allowFrom: ["@testuser"], }, }, @@ -690,8 +714,14 @@ describe("doctor config flow", () => { }; }; expect(cfg.channels?.telegram?.accounts?.work?.allowFrom).toEqual(["12345"]); + expect(makeProxyFetch).toHaveBeenCalledWith("http://127.0.0.1:8888"); + expect(resolveTelegramFetch).toHaveBeenCalledWith(proxyFetch, { + network: { autoSelectFamily: false, dnsResultOrder: "ipv4first" }, + }); expect(fetchSpy).toHaveBeenCalledTimes(1); } finally { + makeProxyFetch.mockRestore(); + resolveTelegramFetch.mockRestore(); resolveSecretsSpy.mockRestore(); vi.unstubAllGlobals(); } diff --git a/src/commands/doctor-config-flow.ts b/src/commands/doctor-config-flow.ts index 8cd8fda3b3c..4d426d91336 100644 --- a/src/commands/doctor-config-flow.ts +++ b/src/commands/doctor-config-flow.ts @@ -1,10 +1,11 @@ import { - fetchTelegramChatId, inspectTelegramAccount, isNumericTelegramUserId, listTelegramAccountIds, + lookupTelegramChatId, normalizeTelegramAllowFromEntry, } from "../../extensions/telegram/api.js"; +import type { TelegramNetworkConfig } from "../config/types.telegram.js"; import { normalizeChatChannelId } from "../channels/registry.js"; import { formatCliCommand } from "../cli/command-format.js"; import { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js"; @@ -87,6 +88,8 @@ type TelegramAllowFromListRef = { type ResolvedTelegramLookupAccount = { token: string; apiRoot?: string; + proxyUrl?: string; + network?: TelegramNetworkConfig; }; function asObjectRecord(value: unknown): Record | null { @@ -421,12 +424,14 @@ async function maybeRepairTelegramAllowFromUsernames(cfg: OpenClawConfig): Promi continue; } const apiRoot = account.config.apiRoot?.trim() || undefined; - const cacheKey = `${token}::${apiRoot ?? ""}`; + const proxyUrl = account.config.proxy?.trim() || undefined; + const network = account.config.network; + const cacheKey = `${token}::${apiRoot ?? ""}::${proxyUrl ?? ""}::${JSON.stringify(network ?? {})}`; if (seenLookupAccounts.has(cacheKey)) { continue; } seenLookupAccounts.add(cacheKey); - lookupAccounts.push({ token, apiRoot }); + lookupAccounts.push({ token, apiRoot, proxyUrl, network }); } if (lookupAccounts.length === 0) { @@ -461,11 +466,13 @@ async function maybeRepairTelegramAllowFromUsernames(cfg: OpenClawConfig): Promi const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 4000); try { - const id = await fetchTelegramChatId({ + const id = await lookupTelegramChatId({ token: account.token, chatId: username, signal: controller.signal, apiRoot: account.apiRoot, + proxyUrl: account.proxyUrl, + network: account.network, }); if (id) { return id;