fix(telegram): honor lookup transport and local file paths

This commit is contained in:
Ayaan Zaidi 2026-03-21 09:56:23 +05:30
parent 59b9ed5eb4
commit 24473b7dd0
No known key found for this signature in database
8 changed files with 150 additions and 12 deletions

View File

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

View File

@ -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<string | null> {
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;
}

View File

@ -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: "<media:document>",
}),
);
});
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: "<media:sticker>",
}),
);
});
});
describe("resolveMedia original filename preservation", () => {

View File

@ -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({

View File

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

View File

@ -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({

View File

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

View File

@ -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<string, unknown> | 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;